From 4471d59f3afe3ca982b6c104a181455152a24c15 Mon Sep 17 00:00:00 2001 From: ARON-Y Date: Thu, 17 Apr 2025 20:21:01 +0900 Subject: [PATCH] refactor: Add loading spinner --- package-lock.json | 11 ++++++ package.json | 1 + .../[dashboardid]/_components/Column.tsx | 10 +++-- .../mypage/_components/ProfileSection.tsx | 5 +++ src/components/common/input/ImageInput.tsx | 39 +++++++------------ .../modal/create-task/CreateTaskModal.tsx | 32 +++++++++++++-- .../modal/edit-task/EditTaskModal.tsx | 33 ++++++++++++++-- 7 files changed, 94 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index d45ab46..5b8f7f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "react-datepicker": "^8.2.1", "react-dom": "^18", "react-hook-form": "^7.54.2", + "react-spinners": "^0.16.1", "tailwind-merge": "^3.0.2", "tailwind-scrollbar-hide": "^2.0.0", "zod": "^3.24.2", @@ -4579,6 +4580,16 @@ "dev": true, "license": "MIT" }, + "node_modules/react-spinners": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.16.1.tgz", + "integrity": "sha512-hYDQp2mmmv3a3JZZZYOi3+jW7C/ro51Ny71TfkRXhoPBb6wZuK9BgdvYbTnSAUrQCrVnOLSpWfZatncUTb6n5w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index dcb52b8..c88b1a1 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "react-datepicker": "^8.2.1", "react-dom": "^18", "react-hook-form": "^7.54.2", + "react-spinners": "^0.16.1", "tailwind-merge": "^3.0.2", "tailwind-scrollbar-hide": "^2.0.0", "zod": "^3.24.2", diff --git a/src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx b/src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx index 9505002..da24722 100644 --- a/src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx +++ b/src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx @@ -4,9 +4,10 @@ import { useEffect, useState, useRef } from "react"; import { useIntersection } from "@/lib/hooks/useIntersection"; import { DashboardColumn, TaskCardList } from "@/lib/types"; import { fetchTaskCardList } from "@/lib/apis/cardsApi"; -import EditColumnButton from "./EditColumnButton"; -import AddTaskButton from "./AddTaskButton"; -import TaskCard from "./TaskCard"; +import EditColumnButton from "@/app/(after-login)/dashboard/[dashboardid]/_components/EditColumnButton"; +import AddTaskButton from "@/app/(after-login)/dashboard/[dashboardid]/_components/AddTaskButton"; +import TaskCard from "@/app/(after-login)/dashboard/[dashboardid]/_components/TaskCard"; +import Cookies from "js-cookie"; const PAGE_SIZE = 3; @@ -17,7 +18,8 @@ export default function Column({ id, title }: DashboardColumn) { const [isLoading, setIsLoading] = useState(false); const [isLast, setIsLast] = useState(false); const observerRef = useRef(null); - const accessToken = localStorage.getItem("accessToken") ?? ""; + // const accessToken = localStorage.getItem("accessToken") ?? ""; + const accessToken = Cookies.get("accessToken") ?? ""; const handleLoad = async () => { if (isLoading || isLast) return; diff --git a/src/app/(after-login)/mypage/_components/ProfileSection.tsx b/src/app/(after-login)/mypage/_components/ProfileSection.tsx index d6f374e..6853cf8 100644 --- a/src/app/(after-login)/mypage/_components/ProfileSection.tsx +++ b/src/app/(after-login)/mypage/_components/ProfileSection.tsx @@ -30,6 +30,7 @@ export default function ProfileSection() { const [profileImageUrl, setProfileImageUrl] = useState(null); const [imageError, setImageError] = useState(null); const [initialData, setInitialData] = useState(null); + const [isImageUploading, setIsImageUploading] = useState(false); const accessToken = Cookies.get("accessToken") ?? ""; const { openAlert } = useAlertStore(); @@ -73,6 +74,7 @@ export default function ProfileSection() { if (!file) return; setImageError(null); + setIsImageUploading(true); try { const res = await uploadProfileImage({ token: accessToken, image: file }); setProfileImageUrl(res.profileImageUrl); @@ -86,6 +88,8 @@ export default function ProfileSection() { } else { setImageError("이미지 업로드에 실패했습니다."); } + } finally { + setIsImageUploading(false); } }; @@ -125,6 +129,7 @@ export default function ProfileSection() { variant="profile" initialImageUrl={profileImageUrl} onChange={handleImageUpload} + isLoading={isImageUploading} /> {imageError &&

{imageError}

} diff --git a/src/components/common/input/ImageInput.tsx b/src/components/common/input/ImageInput.tsx index 1715e81..ac7c41b 100644 --- a/src/components/common/input/ImageInput.tsx +++ b/src/components/common/input/ImageInput.tsx @@ -3,7 +3,7 @@ import { ChangeEvent, useState, useEffect } from "react"; import Image from "next/image"; import { twMerge } from "tailwind-merge"; import clsx from "clsx"; -import { postImage } from "@/lib/apis/imageApi"; +import { HashLoader } from "react-spinners"; interface BaseImageInputProps extends React.InputHTMLAttributes { @@ -12,6 +12,7 @@ interface BaseImageInputProps initialImageUrl?: string | null; onImageUrlChange?: (url: string) => void; token?: string; + isLoading?: boolean; } interface TaskImageInputProps extends BaseImageInputProps { @@ -29,6 +30,7 @@ const ImageInput = ({ label, variant, initialImageUrl, + isLoading = false, ...props }: ImageInputProps) => { const [uploadImgUrl, setUploadImgUrl] = useState( @@ -40,7 +42,7 @@ const ImageInput = ({ setUploadImgUrl(initialImageUrl || ""); }, [initialImageUrl]); - const handleFileChange = async (e: ChangeEvent) => { + const handleFileChange = (e: ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -50,32 +52,9 @@ const ImageInput = ({ return; } - const localUrl = URL.createObjectURL(file); - setUploadImgUrl(localUrl); setError(null); - if (props.onChange) { props.onChange(e); - return; - } - - try { - const columnId = - variant === "task" - ? (props as TaskImageInputProps).columnId - : undefined; - const imageUrl = await postImage(variant, columnId, file, props.token); - setUploadImgUrl(imageUrl); - - if (props.onImageUrlChange) { - props.onImageUrlChange(imageUrl); - } - } catch (error: unknown) { - if (error instanceof Error) { - setError(error.message || "이미지 업로드에 실패했습니다."); - } else { - setError("알 수 없는 오류가 발생했습니다."); - } } }; @@ -105,7 +84,15 @@ const ImageInput = ({ )}