Skip to content

Commit 59b5d65

Browse files
authored
Merge pull request #123 from rover1523/dashmodal
[Feat] 모달 대시보드 생성
2 parents 674c399 + d143e45 commit 59b5d65

File tree

17 files changed

+1014
-288
lines changed

17 files changed

+1014
-288
lines changed

package-lock.json

Lines changed: 342 additions & 266 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
@@ -22,6 +22,7 @@
2222
"react-datetime": "^3.3.1",
2323
"react-dom": "^19.0.0",
2424
"react-hook-form": "^7.54.2",
25+
"react-intersection-observer": "^9.16.0",
2526
"react-toastify": "^11.0.5",
2627
"tostify": "^0.0.1",
2728
"zustand": "^5.0.3"

src/api/card.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import axiosInstance from "./axiosInstance";
2+
import type { CardType } from "@/types/cards"; // Dashboard 타입 import
3+
import { apiRoutes } from "@/api/apiRoutes";
24

35
/** 1. 카드 이미지 업로드 */
46
export const uploadCardImage = async ({
@@ -119,3 +121,22 @@ export const updateCard = async ({
119121

120122
return response.data;
121123
};
124+
125+
//카드조회
126+
export async function getCardDetail(cardId: number): Promise<CardType> {
127+
try {
128+
// apiRoutes를 사용하여 URL 동적 생성
129+
const url = apiRoutes.CardDetail(cardId);
130+
const response = await axiosInstance.get(url);
131+
return response.data as CardType;
132+
} catch (error) {
133+
console.error("대시보드 데이터를 불러오는 데 실패했습니다.", error);
134+
throw error;
135+
}
136+
}
137+
//카드 삭제 api
138+
export const deleteCard = async (teamId: string, cardId: number) => {
139+
const url = apiRoutes.CardDetail(cardId);
140+
const response = await axiosInstance.delete(url);
141+
return response.data;
142+
};

src/api/comment.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { apiRoutes } from "./apiRoutes";
2+
import axiosInstance from "./axiosInstance";
3+
import {
4+
CreateCommentType,
5+
UpdateCommenttype,
6+
DeleteCommentParams,
7+
} from "../types/comments";
8+
//댓글 생성
9+
export const createComment = async (data: CreateCommentType) => {
10+
const response = await axiosInstance.post(apiRoutes.Comments(), data);
11+
return response.data;
12+
};
13+
//댓글 목록
14+
export async function getComments({
15+
cardId,
16+
pageParam,
17+
}: {
18+
cardId: number;
19+
pageParam: number;
20+
}) {
21+
const response = await axiosInstance.get(apiRoutes.Comments(), {
22+
params: {
23+
cardId,
24+
page: pageParam,
25+
},
26+
});
27+
28+
return {
29+
comments: response.data.comments,
30+
nextPage: response.data.nextPage,
31+
};
32+
}
33+
34+
//댓글 수정
35+
export const updateComment = async (
36+
commentId: number,
37+
data: UpdateCommenttype
38+
) => {
39+
const response = await axiosInstance.put(
40+
apiRoutes.CommentsDetail(commentId),
41+
data
42+
);
43+
return response.data;
44+
};
45+
//댓글 삭제
46+
export const deleteComment = async ({ commentId }: DeleteCommentParams) => {
47+
const response = await axiosInstance.delete(
48+
apiRoutes.CommentsDetail(commentId)
49+
);
50+
return response.data;
51+
};

src/api/userprofile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const updateProfile = async (data: UpdateUser) => {
1717
return res.data;
1818
};
1919

20-
// 프로필 이미지 업로드 (POST)
20+
// 프로필 이미지 업로드 (POST)
2121
export const uploadProfileImage = async (
2222
formData: FormData
2323
): Promise<UserMeImage> => {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import Image from "next/image";
2+
import { CardType } from "@/types/cards";
3+
import { ProfileIcon } from "./profelicon";
4+
5+
interface CardDetailProps {
6+
card: CardType;
7+
columnName: string;
8+
}
9+
10+
export default function CardDetail({ card }: CardDetailProps) {
11+
return (
12+
<div className="p-4 ">
13+
<h2 className="text-xl font-bold mb-2">{card.title}</h2>
14+
{/* 작성자 정보 추가 */}
15+
<div className="absolute top-20 right-10 w-40 rounded-lg p-4 bg-white shadow-sm">
16+
<div className="mb-3">
17+
<p className="text-sm font-semibold text-gray-800 mb-1">담당자</p>
18+
<div className="flex items-center gap-2">
19+
<ProfileIcon
20+
userId={card.assignee.id}
21+
nickname={card.assignee.nickname}
22+
profileImageUrl={card.assignee.profileImageUrl}
23+
id={card.assignee.id}
24+
imgClassName="w-6 h-6"
25+
fontClassName="text-sm"
26+
/>
27+
<span className="text-sm text-gray-700">
28+
{card.assignee.nickname}
29+
</span>
30+
</div>
31+
32+
<div>
33+
<p className="text-sm font-semibold text-gray-800 mb-1 mt-3">
34+
마감일
35+
</p>
36+
<p className="text-sm text-gray-700">
37+
{new Date(card.dueDate).toLocaleString("ko-KR", {
38+
year: "numeric",
39+
month: "2-digit",
40+
day: "2-digit",
41+
hour: "2-digit",
42+
minute: "2-digit",
43+
})}
44+
</p>
45+
</div>
46+
</div>
47+
</div>
48+
<div className="flex flex-wrap gap-2 mb-4">
49+
<span
50+
className="rounded-full bg-violet-200 px-3 py-1 text-sm text-violet-800"
51+
title={`상태: ${card.status}`}
52+
>
53+
{card.status}
54+
</span>
55+
{card.tags.map((tag, idx) => (
56+
<span
57+
key={idx}
58+
className="rounded-full bg-gray-200 px-3 py-1 text-sm text-gray-700"
59+
>
60+
{}
61+
{tag}
62+
</span>
63+
))}
64+
</div>
65+
<p
66+
className="text-gray-700 mb-4 break-words overflow-auto"
67+
style={{
68+
maxWidth: "70%", // 부모 기준 너비 제한
69+
maxHeight: "200px", // 최대 높이
70+
whiteSpace: "pre-wrap", // 줄바꿈 유지 + 자동 줄바꿈
71+
wordBreak: "break-word", // 긴 단어도 줄바꿈
72+
}}
73+
>
74+
{card.description}
75+
</p>
76+
{card.imageUrl && (
77+
<div className="w-full mb-4">
78+
<Image
79+
src={card.imageUrl}
80+
alt="카드 이미지"
81+
width={400}
82+
height={400}
83+
className="rounded-lg object-cover"
84+
/>
85+
</div>
86+
)}
87+
</div>
88+
);
89+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { useState } from "react";
2+
import { MoreVertical, X } from "lucide-react";
3+
import CardDetail from "./CardDetail";
4+
import CommentList from "./CommentList";
5+
import CardInput from "@/components/modalInput/CardInput";
6+
import { useMutation, useQueryClient } from "@tanstack/react-query";
7+
import { createComment } from "@/api/comment";
8+
import { deleteCard } from "@/api/card";
9+
import type { CardType } from "@/types/cards";
10+
import TaskModal from "@/components/modalInput/TaskModal"; // 경로는 실제 위치에 맞게 조정하세요
11+
12+
interface CardDetailModalProps {
13+
card: CardType;
14+
currentUserId: number;
15+
teamId: string;
16+
dashboardId: number;
17+
onClose: () => void;
18+
}
19+
20+
export default function CardDetailPage({
21+
card,
22+
currentUserId,
23+
teamId,
24+
dashboardId,
25+
onClose,
26+
}: CardDetailModalProps) {
27+
const [cardData, setCardData] = useState<CardType>(card);
28+
const [commentText, setCommentText] = useState("");
29+
const [showMenu, setShowMenu] = useState(false);
30+
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
31+
const queryClient = useQueryClient();
32+
33+
const { mutate: createCommentMutate } = useMutation({
34+
mutationFn: createComment,
35+
onSuccess: () => {
36+
setCommentText("");
37+
queryClient.invalidateQueries({ queryKey: ["comments", card.id] });
38+
},
39+
});
40+
41+
const { mutate: deleteCardMutate } = useMutation({
42+
mutationFn: () => deleteCard(teamId, card.id),
43+
onSuccess: () => {
44+
queryClient.invalidateQueries({ queryKey: ["cards"] });
45+
onClose();
46+
},
47+
});
48+
49+
const handleCommentSubmit = () => {
50+
if (!commentText.trim()) return;
51+
createCommentMutate({
52+
content: commentText,
53+
cardId: card.id,
54+
columnId: card.columnId,
55+
dashboardId,
56+
});
57+
};
58+
59+
return (
60+
<>
61+
<div className="fixed inset-0 bg-black/30 z-50 flex items-center justify-center">
62+
<div className="relative bg-white rounded-lg shadow-lg w-[730px] h-[763px] flex flex-col">
63+
{/* 오른쪽 상단 메뉴 */}
64+
<div className="absolute top-2 right-6 w-[50px] h-[50px] z-30 flex items-center gap-2">
65+
<div className="relative">
66+
<button onClick={() => setShowMenu((prev) => !prev)}>
67+
<MoreVertical className="w-8 h-8 text-gray-500 hover:text-black" />
68+
</button>
69+
{showMenu && (
70+
<div className="absolute right-0 mt-2 w-24 bg-white border rounded shadow z-40">
71+
<button
72+
className="block w-full px-3 py-2 text-sm text-violet-600 hover:bg-gray-100"
73+
onClick={() => {
74+
setIsEditModalOpen(true);
75+
setShowMenu(false);
76+
}}
77+
>
78+
수정하기
79+
</button>
80+
<button
81+
className="block w-full px-3 py-2 text-sm text-red-500 hover:bg-gray-100"
82+
onClick={() => deleteCardMutate()}
83+
>
84+
삭제하기
85+
</button>
86+
</div>
87+
)}
88+
</div>
89+
<button onClick={onClose}>
90+
<X className="w-8 h-8 text-gray-500 hover:text-black" />
91+
</button>
92+
</div>
93+
94+
{/* 모달 내부 콘텐츠 */}
95+
<div className="p-6 flex gap-6 overflow-y-auto">
96+
<CardDetail card={cardData} columnName={""} />
97+
</div>
98+
99+
{/* 댓글 입력창 */}
100+
<div className="p-4">
101+
<CardInput
102+
hasButton
103+
small
104+
value={commentText}
105+
onTextChange={setCommentText}
106+
onButtonClick={handleCommentSubmit}
107+
/>
108+
</div>
109+
110+
{/* 댓글 목록 */}
111+
<div className="px-6 space-y-4 max-h-[200px] overflow-y-auto">
112+
<CommentList
113+
cardId={card.id}
114+
currentUserId={currentUserId}
115+
teamId={""}
116+
/>
117+
</div>
118+
</div>
119+
</div>
120+
121+
{/* TaskModal 수정 모드 */}
122+
{isEditModalOpen && (
123+
<TaskModal
124+
mode="edit"
125+
onClose={() => setIsEditModalOpen(false)}
126+
onSubmit={(data) => {
127+
setCardData((prev) => ({
128+
...prev,
129+
status: data.status as "todo" | "in-progress" | "done",
130+
assignee: { ...prev.assignee, nickname: data.assignee },
131+
title: data.title,
132+
description: data.description,
133+
dueDate: data.deadline,
134+
tags: data.tags,
135+
imageUrl: data.image ?? "",
136+
}));
137+
setIsEditModalOpen(false);
138+
}}
139+
initialData={{
140+
status: cardData.status,
141+
assignee: cardData.assignee.nickname,
142+
title: cardData.title,
143+
description: cardData.description,
144+
deadline: cardData.dueDate,
145+
tags: cardData.tags,
146+
image: cardData.imageUrl ?? "",
147+
}}
148+
members={[{ nickname: cardData.assignee.nickname }]}
149+
/>
150+
)}
151+
</>
152+
);
153+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useEffect } from "react";
2+
import { useInView } from "react-intersection-observer";
3+
import { useInfiniteQuery } from "@tanstack/react-query";
4+
import { getComments } from "@/api/comment";
5+
import type { Comment as CommentType } from "@/types/comments";
6+
import UpdateComment from "./UpdateComment";
7+
8+
interface CommentListProps {
9+
cardId: number;
10+
currentUserId: number;
11+
teamId: string;
12+
}
13+
14+
export default function CommentList({
15+
cardId,
16+
currentUserId,
17+
}: CommentListProps) {
18+
const { ref, inView } = useInView();
19+
20+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
21+
useInfiniteQuery({
22+
queryKey: ["comments", cardId],
23+
queryFn: ({ pageParam = 1 }) => getComments({ cardId, pageParam }),
24+
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
25+
initialPageParam: 1,
26+
enabled: !!cardId,
27+
retry: false,
28+
});
29+
30+
useEffect(() => {
31+
if (inView && hasNextPage && !isFetchingNextPage) {
32+
fetchNextPage();
33+
}
34+
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
35+
36+
const allComments: CommentType[] =
37+
data?.pages.flatMap((page) => page.comments) ?? [];
38+
39+
return (
40+
<div className="flex flex-col gap-4 mt-4">
41+
{allComments.map((comment) => (
42+
<UpdateComment
43+
key={comment.id}
44+
comment={comment}
45+
currentUserId={currentUserId}
46+
teamId={""}
47+
/>
48+
))}
49+
<div ref={ref} className="h-6" />
50+
</div>
51+
);
52+
}

0 commit comments

Comments
 (0)