Skip to content

Commit e098cff

Browse files
authored
[Refactor] 컴포넌트 로직분리
[Refactor] 컴포넌트 로직분리
2 parents fefadde + 74770ef commit e098cff

File tree

12 files changed

+612
-457
lines changed

12 files changed

+612
-457
lines changed

src/components/modalDashboard/CardDetailModal.tsx

Lines changed: 24 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
1-
import { useMemo, useRef, useState } from "react";
1+
import { useRef } from "react";
22
import clsx from "clsx";
33
import { MoreVertical, X } from "lucide-react";
44
import CardDetail from "./CardDetail";
55
import CommentList from "./CommentList";
66
import CommentInput from "@/components/modalInput/CardInput";
77
import { Representative } from "@/components/modalDashboard/Representative";
8-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
9-
import { createComment } from "@/api/comment";
10-
import { deleteCard, getDashboardMembers } from "@/api/card";
11-
import type { CardDetailType } from "@/types/cards";
128
import TaskModal from "@/components/modalInput/TaskModal";
139
import { DeleteModal } from "@/components/modal/DeleteModal";
14-
import { useClosePopup } from "@/hooks/useClosePopup";
15-
import { getColumn } from "@/api/columns";
1610
import { toast } from "react-toastify";
1711
import { TEAM_ID } from "@/constants/team";
1812
import { useDashboardPermission } from "@/hooks/useDashboardPermission";
13+
import { useCardDetailState } from "@/hooks/useCardDetailState";
14+
import { useCardDetail } from "@/hooks/useCardDetail";
15+
import type { CardDetailType } from "@/types/cards";
1916

2017
interface CardDetailModalProps {
2118
card: CardDetailType;
@@ -26,12 +23,6 @@ interface CardDetailModalProps {
2623
onChangeCard?: () => void;
2724
}
2825

29-
interface ColumnType {
30-
id: number;
31-
title: string;
32-
status: string;
33-
}
34-
3526
export default function CardDetailPage({
3627
card,
3728
currentUserId,
@@ -41,75 +32,32 @@ export default function CardDetailPage({
4132
onChangeCard,
4233
}: CardDetailModalProps) {
4334
const { canEditCards } = useDashboardPermission(dashboardId, createdByMe);
44-
45-
const [cardData, setCardData] = useState<CardDetailType>(card);
46-
const [commentText, setCommentText] = useState("");
47-
const [showMenu, setShowMenu] = useState(false);
48-
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
49-
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
50-
51-
const queryClient = useQueryClient();
52-
const popupRef = useRef(null);
53-
useClosePopup(popupRef, () => setShowMenu(false));
54-
55-
const { data: columns = [] } = useQuery<ColumnType[]>({
56-
queryKey: ["columns", dashboardId],
57-
queryFn: () => getColumn({ dashboardId, columnId: cardData.columnId }),
58-
});
59-
60-
const { data: members = [] } = useQuery({
61-
queryKey: ["dashboardMembers", dashboardId],
62-
queryFn: () => getDashboardMembers({ dashboardId }),
63-
});
64-
65-
const columnName = useMemo(() => {
66-
return (
67-
columns.find((col) => col.id === cardData.columnId)?.title || "알 수 없음"
68-
);
69-
}, [columns, cardData.columnId]);
70-
71-
const { mutate: createCommentMutate } = useMutation({
72-
mutationFn: createComment,
73-
onSuccess: () => {
74-
setCommentText("");
75-
queryClient.invalidateQueries({ queryKey: ["comments", cardData.id] });
76-
},
77-
});
78-
79-
const { mutate: deleteCardMutate } = useMutation({
80-
mutationFn: () => deleteCard(cardData.id),
81-
onSuccess: () => {
82-
queryClient.invalidateQueries({ queryKey: ["cards"] });
83-
if (onChangeCard) onChangeCard();
84-
setIsDeleteModalOpen(true);
85-
},
86-
});
87-
88-
const handleClose = () => {
89-
onClose();
90-
};
91-
92-
const handleCommentSubmit = () => {
93-
if (!commentText.trim()) return;
94-
createCommentMutate({
95-
content: commentText,
96-
cardId: cardData.id,
97-
columnId: cardData.columnId,
98-
dashboardId,
99-
});
100-
};
35+
const { cardData, setCardData, columnName, columns, members } =
36+
useCardDetailState(card, dashboardId);
37+
38+
const {
39+
commentText,
40+
setCommentText,
41+
isEditModalOpen,
42+
setIsEditModalOpen,
43+
isDeleteModalOpen,
44+
setIsDeleteModalOpen,
45+
showMenu,
46+
setShowMenu,
47+
popupRef,
48+
handleCommentSubmit,
49+
handleConfirmDelete,
50+
} = useCardDetail(card, dashboardId, onChangeCard, onClose);
10151

10252
return (
10353
<>
104-
{/* 모달 고정 div */}
10554
<div
10655
className={clsx(
10756
"fixed inset-0 z-50",
10857
"flex items-center justify-center px-4 sm:px-6",
10958
"bg-black/35"
11059
)}
11160
>
112-
{/* 모달창 */}
11361
<div
11462
className={clsx(
11563
"relative flex flex-col overflow-y-auto",
@@ -119,20 +67,15 @@ export default function CardDetailPage({
11967
)}
12068
>
12169
<div className="flex items-center justify-center px-6 pt-6 pb-2">
122-
{/* 내부 아이템 컨테이너 */}
12370
<div className="flex flex-col lg:w-[674px] sm:w-[614px] w-[295px]">
124-
{/* 헤더 컨테이너 */}
12571
<div className="flex justify-between sm:mb-4 mb-2">
126-
{/* 제목 */}
12772
<h2 className="text-black3 font-bold sm:text-[20px] text-[16px]">
12873
{cardData.title}
12974
</h2>
130-
{/* 버튼 컨테이너 */}
13175
<div
13276
className="relative flex items-center sm:gap-[24px] gap-[16px]"
13377
ref={popupRef}
13478
>
135-
{/* 메뉴 버튼 */}
13679
<button
13780
onClick={() => setShowMenu((prev) => !prev)}
13881
className="sm:w-[28px] sm:h-[28px] w-[20px] h-[20px] flex items-center justify-center hover:cursor-pointer"
@@ -141,7 +84,6 @@ export default function CardDetailPage({
14184
>
14285
<MoreVertical className="w-8 h-8 text-black3 cursor-pointer" />
14386
</button>
144-
{/* 수정/삭제 드롭다운 메뉴 */}
14587
{showMenu && (
14688
<div className="absolute right-0 top-9.5 p-2 z-40 flex flex-col items-center justify-center sm:gap-[6px] gap-[11px] sm:w-28 w-20 sm:h-24 bg-white border border-[#D9D9D9] rounded-lg">
14789
<button
@@ -173,21 +115,19 @@ export default function CardDetailPage({
173115
</button>
174116
</div>
175117
)}
176-
<button onClick={handleClose} title="닫기">
118+
<button onClick={onClose} title="닫기">
177119
<X className="sm:w-[28px] sm:h-[28px] w-[20px] h-[20px] flex items-center justify-center hover:cursor-pointer" />
178120
</button>
179121
</div>
180122
</div>
181123

182-
{/* 카드 내용 */}
183124
<div className="flex flex-col-reverse sm:flex-row gap-4">
184125
<CardDetail card={cardData} columnName={columnName} />
185126
<div>
186127
<Representative card={cardData} />
187128
</div>
188129
</div>
189130

190-
{/* 댓글 */}
191131
<div className="mt-4 w-full lg:max-w-[445px] md:max-w-[420px]">
192132
<p className="mb-1 text-black3 font-medium sm:text-[16px] text-[14px]">
193133
댓글
@@ -223,8 +163,7 @@ export default function CardDetailPage({
223163
members={members}
224164
onClose={() => setIsEditModalOpen(false)}
225165
onSubmit={(updatedData) => {
226-
if (onChangeCard) onChangeCard();
227-
queryClient.invalidateQueries({ queryKey: ["cards"] });
166+
onChangeCard?.();
228167
setCardData((prev) => ({
229168
...prev,
230169
title: updatedData.title,
@@ -253,25 +192,12 @@ export default function CardDetailPage({
253192
}}
254193
/>
255194
)}
195+
256196
<DeleteModal
257197
title="카드를"
258198
isOpen={isDeleteModalOpen}
259199
onCancel={() => setIsDeleteModalOpen(false)}
260-
onConfirm={() => {
261-
deleteCardMutate(undefined, {
262-
onSuccess: () => {
263-
queryClient.invalidateQueries({ queryKey: ["cards"] });
264-
if (onChangeCard) onChangeCard();
265-
setIsDeleteModalOpen(false);
266-
onClose();
267-
toast.success("카드가 삭제되었습니다.");
268-
},
269-
onError: () => {
270-
toast.error("카드 삭제에 실패했습니다.");
271-
setIsDeleteModalOpen(false);
272-
},
273-
});
274-
}}
200+
onConfirm={handleConfirmDelete}
275201
/>
276202
</>
277203
);

src/components/modalInput/TaskModal.tsx

Lines changed: 20 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import { useState, useMemo } from "react";
2-
import { useQuery, useQueryClient } from "@tanstack/react-query";
3-
import { createCard, editCard } from "@/api/card";
4-
import { getColumn } from "@/api/columns";
51
import ModalInput from "@/components/modalInput/ModalInput";
62
import ModalTextarea from "@/components/modalInput/ModalTextarea";
73
import ModalImage from "@/components/modalInput/ModalImage";
84
import TextButton from "@/components/modalInput/TextButton";
95
import StatusSelect from "@/components/modalInput/StatusSelect";
106
import AssigneeSelect from "@/components/modalInput/AssigneeSelect";
11-
import { toast } from "react-toastify";
7+
import { useTaskForm } from "@/hooks/useTaskForm";
8+
import { useColumnStatus } from "@/hooks/useColumnStatus";
129

1310
interface TaskModalProps {
1411
mode?: "create" | "edit";
@@ -23,7 +20,7 @@ interface TaskModalProps {
2320
}[];
2421
columnId: number;
2522
dashboardId: number;
26-
cardId?: number; // 수정 모드일 때만 사용
23+
cardId?: number;
2724
}
2825

2926
export interface TaskData {
@@ -36,12 +33,6 @@ export interface TaskData {
3633
image?: string;
3734
}
3835

39-
interface ColumnType {
40-
id: number;
41-
title: string;
42-
status: string;
43-
}
44-
4536
export default function TaskModal({
4637
mode = "create",
4738
onClose,
@@ -52,94 +43,24 @@ export default function TaskModal({
5243
dashboardId,
5344
cardId,
5445
}: TaskModalProps) {
55-
const queryClient = useQueryClient();
56-
57-
const [formData, setFormData] = useState<TaskData>({
58-
status: initialData.status || "",
59-
assignee: initialData.assignee || "",
60-
title: initialData.title || "",
61-
description: initialData.description || "",
62-
deadline: initialData.deadline ?? "",
63-
tags: initialData.tags || [],
64-
image: initialData.image || "",
65-
});
46+
const updatedColumnId = useColumnStatus(
47+
dashboardId,
48+
columnId,
49+
initialData.status || ""
50+
);
6651

67-
const { data: columns = [] } = useQuery<ColumnType[]>({
68-
queryKey: ["columns", dashboardId],
69-
queryFn: () => getColumn({ dashboardId, columnId }),
52+
const { formData, handleChange, isFormValid, handleSubmit } = useTaskForm({
53+
mode,
54+
initialData,
55+
members,
56+
dashboardId,
57+
columnId,
58+
cardId,
59+
updatedColumnId,
60+
onClose,
61+
onSubmit,
7062
});
7163

72-
const matchedColumn = useMemo(() => {
73-
if (!columns.length) return undefined;
74-
return columns.find((col) => col.title === formData.status);
75-
}, [columns, formData.status]);
76-
77-
const updatedColumnId = matchedColumn?.id ?? columnId;
78-
79-
const handleChange = (field: keyof TaskData, value: string | string[]) => {
80-
setFormData((prev) => ({
81-
...prev,
82-
[field]: value,
83-
}));
84-
};
85-
86-
const isFormValid =
87-
formData.assignee &&
88-
formData.status &&
89-
formData.title.trim() &&
90-
formData.description.trim();
91-
92-
const handleSubmit = async () => {
93-
try {
94-
const selectedAssignee = members.find(
95-
(m) => m.nickname === formData.assignee
96-
);
97-
const assigneeUserId = selectedAssignee?.userId;
98-
99-
if (!assigneeUserId) {
100-
toast.error("담당자를 선택해 주세요.");
101-
return;
102-
}
103-
104-
if (mode === "create") {
105-
await createCard({
106-
assigneeUserId,
107-
dashboardId,
108-
columnId: updatedColumnId,
109-
title: formData.title,
110-
description: formData.description,
111-
dueDate: formData.deadline.trim() ? formData.deadline : undefined,
112-
tags: formData.tags,
113-
imageUrl: formData.image || undefined,
114-
});
115-
toast.success("카드가 생성되었습니다.");
116-
} else {
117-
if (!cardId) {
118-
toast.error("카드 ID가 없습니다.");
119-
return;
120-
}
121-
122-
await editCard(cardId, {
123-
assigneeUserId,
124-
columnId: updatedColumnId,
125-
title: formData.title,
126-
description: formData.description,
127-
dueDate: formData.deadline.trim() ? formData.deadline : undefined,
128-
tags: formData.tags,
129-
imageUrl: formData.image || undefined,
130-
});
131-
queryClient.invalidateQueries({ queryKey: ["cards"] });
132-
toast.success("카드가 수정되었습니다.");
133-
}
134-
135-
onSubmit?.(formData);
136-
onClose();
137-
} catch (err) {
138-
console.error("카드 처리 실패:", err);
139-
toast.error(`카드 ${mode === "edit" ? "수정" : "생성"}에 실패했습니다.`);
140-
}
141-
};
142-
14364
return (
14465
<div className="fixed inset-0 flex items-center justify-center bg-black/35 z-50">
14566
<div className="sm:w-[584px] w-[327px] h-[calc(var(--vh)_*_90)] rounded-lg bg-white p-4 sm:p-8 shadow-lg flex flex-col gap-4 sm:gap-8 overflow-y-auto">
@@ -148,11 +69,7 @@ export default function TaskModal({
14869
</h2>
14970

15071
<div className="flex flex-col gap-4 sm:gap-8">
151-
{/* 상태 및 담당자 */}
152-
<div
153-
className="flex flex-col sm:flex-row gap-4
154-
text-black3 font-normal text-[14px] sm:text-[16px]"
155-
>
72+
<div className="flex flex-col sm:flex-row gap-4 text-black3 font-normal text-[14px] sm:text-[16px]">
15673
<StatusSelect
15774
label="상태"
15875
value={formData.status}
@@ -210,8 +127,7 @@ export default function TaskModal({
210127
color="third"
211128
buttonSize="md"
212129
onClick={onClose}
213-
className="sm:w-[256px] w-[144px] h-[54px] border border-[var(--color-gray3)] bg-white
214-
text-[var(--color-gray1)] font-16m rounded-lg cursor-pointer"
130+
className="sm:w-[256px] w-[144px] h-[54px] border border-[var(--color-gray3)] bg-white text-[var(--color-gray1)] font-16m rounded-lg cursor-pointer"
215131
>
216132
취소
217133
</TextButton>

0 commit comments

Comments
 (0)