Skip to content

Commit 1423cf3

Browse files
authored
Merge pull request #139 from hyeonjiroh/refactor/#133/loading
refactor: #133/로딩 스피너 추가
2 parents 1599ad7 + bed28cc commit 1423cf3

7 files changed

Lines changed: 94 additions & 37 deletions

File tree

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"react-datepicker": "^8.2.1",
2121
"react-dom": "^18",
2222
"react-hook-form": "^7.54.2",
23+
"react-spinners": "^0.16.1",
2324
"tailwind-merge": "^3.0.2",
2425
"tailwind-scrollbar-hide": "^2.0.0",
2526
"zod": "^3.24.2",

src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { useEffect, useState, useRef } from "react";
44
import { useIntersection } from "@/lib/hooks/useIntersection";
55
import { DashboardColumn, TaskCardList } from "@/lib/types";
66
import { fetchTaskCardList } from "@/lib/apis/cardsApi";
7-
import EditColumnButton from "./EditColumnButton";
8-
import AddTaskButton from "./AddTaskButton";
9-
import TaskCard from "./TaskCard";
7+
import EditColumnButton from "@/app/(after-login)/dashboard/[dashboardid]/_components/EditColumnButton";
8+
import AddTaskButton from "@/app/(after-login)/dashboard/[dashboardid]/_components/AddTaskButton";
9+
import TaskCard from "@/app/(after-login)/dashboard/[dashboardid]/_components/TaskCard";
10+
import Cookies from "js-cookie";
1011

1112
const PAGE_SIZE = 3;
1213

@@ -17,7 +18,8 @@ export default function Column({ id, title }: DashboardColumn) {
1718
const [isLoading, setIsLoading] = useState(false);
1819
const [isLast, setIsLast] = useState(false);
1920
const observerRef = useRef<HTMLDivElement | null>(null);
20-
const accessToken = localStorage.getItem("accessToken") ?? "";
21+
// const accessToken = localStorage.getItem("accessToken") ?? "";
22+
const accessToken = Cookies.get("accessToken") ?? "";
2123

2224
const handleLoad = async () => {
2325
if (isLoading || isLast) return;

src/app/(after-login)/mypage/_components/ProfileSection.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default function ProfileSection() {
3030
const [profileImageUrl, setProfileImageUrl] = useState<string | null>(null);
3131
const [imageError, setImageError] = useState<string | null>(null);
3232
const [initialData, setInitialData] = useState<UserInfo | null>(null);
33+
const [isImageUploading, setIsImageUploading] = useState(false);
3334
const accessToken = Cookies.get("accessToken") ?? "";
3435
const { openAlert } = useAlertStore();
3536

@@ -73,6 +74,7 @@ export default function ProfileSection() {
7374
if (!file) return;
7475

7576
setImageError(null);
77+
setIsImageUploading(true);
7678
try {
7779
const res = await uploadProfileImage({ token: accessToken, image: file });
7880
setProfileImageUrl(res.profileImageUrl);
@@ -86,6 +88,8 @@ export default function ProfileSection() {
8688
} else {
8789
setImageError("이미지 업로드에 실패했습니다.");
8890
}
91+
} finally {
92+
setIsImageUploading(false);
8993
}
9094
};
9195

@@ -125,6 +129,7 @@ export default function ProfileSection() {
125129
variant="profile"
126130
initialImageUrl={profileImageUrl}
127131
onChange={handleImageUpload}
132+
isLoading={isImageUploading}
128133
/>
129134
{imageError && <p className="text-red text-sm">{imageError}</p>}
130135
</div>

src/components/common/input/ImageInput.tsx

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ChangeEvent, useState, useEffect } from "react";
33
import Image from "next/image";
44
import { twMerge } from "tailwind-merge";
55
import clsx from "clsx";
6-
import { postImage } from "@/lib/apis/imageApi";
6+
import { HashLoader } from "react-spinners";
77

88
interface BaseImageInputProps
99
extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -12,6 +12,7 @@ interface BaseImageInputProps
1212
initialImageUrl?: string | null;
1313
onImageUrlChange?: (url: string) => void;
1414
token?: string;
15+
isLoading?: boolean;
1516
}
1617

1718
interface TaskImageInputProps extends BaseImageInputProps {
@@ -29,6 +30,7 @@ const ImageInput = ({
2930
label,
3031
variant,
3132
initialImageUrl,
33+
isLoading = false,
3234
...props
3335
}: ImageInputProps) => {
3436
const [uploadImgUrl, setUploadImgUrl] = useState<string>(
@@ -40,7 +42,7 @@ const ImageInput = ({
4042
setUploadImgUrl(initialImageUrl || "");
4143
}, [initialImageUrl]);
4244

43-
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
45+
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
4446
const file = e.target.files?.[0];
4547
if (!file) return;
4648

@@ -50,32 +52,9 @@ const ImageInput = ({
5052
return;
5153
}
5254

53-
const localUrl = URL.createObjectURL(file);
54-
setUploadImgUrl(localUrl);
5555
setError(null);
56-
5756
if (props.onChange) {
5857
props.onChange(e);
59-
return;
60-
}
61-
62-
try {
63-
const columnId =
64-
variant === "task"
65-
? (props as TaskImageInputProps).columnId
66-
: undefined;
67-
const imageUrl = await postImage(variant, columnId, file, props.token);
68-
setUploadImgUrl(imageUrl);
69-
70-
if (props.onImageUrlChange) {
71-
props.onImageUrlChange(imageUrl);
72-
}
73-
} catch (error: unknown) {
74-
if (error instanceof Error) {
75-
setError(error.message || "이미지 업로드에 실패했습니다.");
76-
} else {
77-
setError("알 수 없는 오류가 발생했습니다.");
78-
}
7958
}
8059
};
8160

@@ -105,7 +84,15 @@ const ImageInput = ({
10584
</span>
10685
)}
10786
<label htmlFor={label || "file"} className={imageWrapperStyles}>
108-
{uploadImgUrl ? (
87+
{isLoading ? (
88+
<div className="absolute inset-0 flex items-center justify-center bg-gray-100 bg-opacity-75 rounded-md">
89+
<HashLoader
90+
color="#5534DA"
91+
size={variant === "task" ? 30 : 50}
92+
className="z-20"
93+
/>
94+
</div>
95+
) : uploadImgUrl ? (
10996
<Image
11097
src={uploadImgUrl}
11198
alt="업로드 이미지 미리보기"

src/components/modal/create-task/CreateTaskModal.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import DateInput from "@/components/common/input/DateInput";
77
import ImageInput from "@/components/common/input/ImageInput";
88
import Textarea from "@/components/common/textarea/Textarea";
99
import UserIcon from "@/components/common/user-icon/UserIcon";
10-
import dropdownIcon from "../../../../public/icon/dropdown_icon.svg";
10+
import dropdownIcon from "public/icon/dropdown_icon.svg";
1111
import { fetchDashboardMember } from "@/lib/apis/membersApi";
1212
import { DashboardMember } from "@/lib/types";
1313
import { useDashboardStore } from "@/lib/store/useDashboardStore";
1414
import { useColumnStore } from "@/lib/store/useColumnStore";
15-
import checkItem from "../../../../public/icon/check_icon.svg";
15+
import checkItem from "public/icon/check_icon.svg";
1616
import { createCard } from "@/lib/apis/cardsApi";
17+
import { postImage } from "@/lib/apis/imageApi";
18+
import Cookies from "js-cookie";
1719

1820
export default function CreateDashboardModal() {
1921
const { dashboardId } = useDashboardStore();
@@ -26,12 +28,13 @@ export default function CreateDashboardModal() {
2628
const [dueDate, setDueDate] = useState<string>("");
2729
const [tags, setTags] = useState<string[]>([]);
2830
const [imageUrl, setImageUrl] = useState<string | null>(null);
31+
const [isImageUploading, setIsImageUploading] = useState(false);
2932
const [items, setItems] = useState<DashboardMember[]>([]);
3033

3134
const [isFormValid, setIsFormValid] = useState(false);
3235
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
3336

34-
const accessToken = localStorage.getItem("accessToken") ?? "";
37+
const accessToken = Cookies.get("accessToken") ?? "";
3538

3639
const fetchMembers = async () => {
3740
if (!dashboardId) return;
@@ -112,6 +115,26 @@ export default function CreateDashboardModal() {
112115
window.location.reload();
113116
};
114117

118+
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
119+
const file = e.target.files?.[0];
120+
if (!file || !selectedColumnId) return;
121+
122+
setIsImageUploading(true);
123+
try {
124+
const imageUrl = await postImage(
125+
"task",
126+
selectedColumnId,
127+
file,
128+
accessToken
129+
);
130+
setImageUrl(imageUrl);
131+
} catch (error) {
132+
console.error("이미지 업로드 에러:", error);
133+
} finally {
134+
setIsImageUploading(false);
135+
}
136+
};
137+
115138
if (!selectedColumnId) return;
116139

117140
return (
@@ -211,7 +234,10 @@ export default function CreateDashboardModal() {
211234
variant="task"
212235
columnId={selectedColumnId}
213236
token={accessToken}
237+
initialImageUrl={imageUrl}
214238
onImageUrlChange={(url) => setImageUrl(url)}
239+
onChange={handleImageUpload}
240+
isLoading={isImageUploading}
215241
/>
216242
</div>
217243
</Modal>

src/components/modal/edit-task/EditTaskModal.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ import { useState, useEffect, ChangeEvent } from "react";
22
import { useTaskStore } from "@/lib/store/useTaskStore";
33
import { useModalStore } from "@/lib/store/useModalStore";
44
import { fetchTaskCardDetail, putCard } from "@/lib/apis/cardsApi";
5+
import { postImage } from "@/lib/apis/imageApi";
56
import Modal from "@/components/common/modal/Modal";
67
import Input from "@/components/common/input/Input";
78
import DateInput from "@/components/common/input/DateInput";
89
import Textarea from "@/components/common/textarea/Textarea";
910
import TagInput from "@/components/common/input/TagInput";
1011
import ImageInput from "@/components/common/input/ImageInput";
11-
import ColumnDropdown from "./ColumnDropdown";
12-
import AssigneeDropdown from "./AssigneeDropdown";
12+
import ColumnDropdown from "@/components/modal/edit-task/ColumnDropdown";
13+
import AssigneeDropdown from "@/components/modal/edit-task/AssigneeDropdown";
1314
import { useDashboardStore } from "@/lib/store/useDashboardStore";
15+
import Cookies from "js-cookie";
1416

1517
export default function EditTaskModal() {
1618
const { openModal } = useModalStore();
1719
const { selectedTaskId } = useTaskStore();
1820
const { dashboardId } = useDashboardStore();
1921
const [isLoading, setIsLoading] = useState(false);
20-
const accessToken = localStorage.getItem("accessToken") ?? "";
22+
const accessToken = Cookies.get("accessToken") ?? "";
2123

2224
const [isFormValid, setIsFormValid] = useState(false);
2325
const [selectedColumn, setSelectedColumn] = useState(0);
@@ -30,6 +32,7 @@ export default function EditTaskModal() {
3032
const [tagValues, setTagValues] = useState<string[]>([]);
3133
const [dueDate, setDueDate] = useState("");
3234
const [cardImg, setItemImg] = useState<string | null>(null);
35+
const [isImageUploading, setIsImageUploading] = useState(false);
3336

3437
const [initialValues, setInitialValues] = useState<{
3538
title: string;
@@ -104,6 +107,26 @@ export default function EditTaskModal() {
104107
}
105108
};
106109

110+
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
111+
const file = e.target.files?.[0];
112+
if (!file) return;
113+
114+
setIsImageUploading(true);
115+
try {
116+
const imageUrl = await postImage(
117+
"task",
118+
selectedColumn,
119+
file,
120+
accessToken
121+
);
122+
setItemImg(imageUrl);
123+
} catch (error) {
124+
console.error("이미지 업로드 에러:", error);
125+
} finally {
126+
setIsImageUploading(false);
127+
}
128+
};
129+
107130
useEffect(() => {
108131
handleLoad();
109132
}, []);
@@ -184,9 +207,11 @@ export default function EditTaskModal() {
184207
label="이미지"
185208
variant="task"
186209
columnId={selectedColumn}
210+
token={accessToken}
187211
initialImageUrl={cardImg}
188212
onImageUrlChange={(url) => setItemImg(url)}
189-
token={accessToken}
213+
onChange={handleImageUpload}
214+
isLoading={isImageUploading}
190215
/>
191216
</div>
192217
</Modal>

0 commit comments

Comments
 (0)