diff --git a/src/app/(protected)/dashboard/[id]/page.tsx b/src/app/(protected)/dashboard/[id]/page.tsx index ac542c9..7b53da4 100644 --- a/src/app/(protected)/dashboard/[id]/page.tsx +++ b/src/app/(protected)/dashboard/[id]/page.tsx @@ -6,21 +6,17 @@ import Chip from "@/components/common/chip/Chip"; import Column from "@/components/column/Column"; import MyButton from "@/components/common/Button"; import { getColumns } from "@/features/columns/api"; -import { useColumnId } from "@/features/columns/store"; import { ColumnData } from "@/features/dashboard/types"; -import CreateCardModal from "@/components/modal/cardModal/CreateCardModal"; import CreateColumnModal from "@/components/modal/columnModal/CreateColumnModal"; export default function DashboardId() { const { id } = useParams(); const dashboardId = Number(id); - const [modal, setModal] = useState(null); + const [modal, setModal] = useState(null); const [isKebabOpen, setIsKebabOpen] = useState(null); const [columns, setColumns] = useState([]); const [isLoading, setIsLoading] = useState(true); - // Zustand 스토어에서 함수 가져오기 - const { setColumnIdData, setStatus } = useColumnId(); useEffect(() => { if (!dashboardId) return; @@ -30,13 +26,9 @@ export default function DashboardId() { setIsLoading(true); const response = await getColumns(dashboardId); const columnsData = Array.isArray(response) ? response : response?.data || []; - setColumns(columnsData); - - const statusMap = Object.fromEntries(columnsData.map((c) => [c.title, c.id])); - setStatus(statusMap); } catch (error) { - console.error("컬럼 목록 조회 실패:", error); + console.error("컬럼 목록 조회 실패", error); setColumns([]); } finally { setIsLoading(false); @@ -44,17 +36,6 @@ export default function DashboardId() { })(); }, [dashboardId]); - // 카드 추가 버튼 클릭 시 - zustand에 정보 저장하고 모달 열기 - const handleAddCard = (columnId: number, columnTitle: string) => { - console.log("카드 추가 클릭 - 컬럼 ID:", columnId, "대시보드 ID:", dashboardId); - - // Zustand에 현재 선택된 컬럼과 대시보드 정보 저장 - setColumnIdData(dashboardId, columnId, columnTitle); - - // 카드 생성 모달 열기 - setModal("card"); - }; - const handleAddColumn = () => { setModal("column"); }; @@ -74,12 +55,12 @@ export default function DashboardId() { key={item.id} status={item.title} cards={item.cards ?? []} - onAddCard={() => handleAddCard(item.id, item.title)} kebabIndex={isKebabOpen === i} isKebabOpen={() => setIsKebabOpen((prev) => (prev === i ? null : i))} dashboardId={dashboardId} columnId={item.id} setColumns={setColumns} + columns={columns} /> ))} @@ -93,12 +74,6 @@ export default function DashboardId() { - - {/* 모달들 */} - {modal === "card" && ( - - )} - {modal === "column" && ( )} diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index b42b835..4d1a06f 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -18,8 +18,12 @@ type CardWithAssignee = Omit & { nickname: string; profileImageUrl?: string; }; + columns?: ColumnData[]; setColumns?: React.Dispatch>; + dashboardId?: number; columnId?: number; + cardId?: number; + status: string; }; export default function Card({ @@ -29,7 +33,11 @@ export default function Card({ imageUrl, assignee, setColumns, + dashboardId, columnId, + cardId, + status, + columns, }: CardWithAssignee) { const [isOpen, setIsOpen] = useState(false); @@ -130,7 +138,11 @@ export default function Card({ isOpen setIsOpen={setIsOpen} setColumns={setColumns} + dashboardId={dashboardId} columnId={columnId} + cardId={cardId} + status={status} + columns={columns} /> )} diff --git a/src/components/column/Column.tsx b/src/components/column/Column.tsx index 3ea415e..12cce40 100644 --- a/src/components/column/Column.tsx +++ b/src/components/column/Column.tsx @@ -4,13 +4,13 @@ import { useState, useEffect, useRef } from "react"; import DeleteColumnModal from "@/components/modal/columnModal/DeleteColumnModal"; import ManageColumnModal from "@/components/modal/columnModal/ManageColumnModal"; +import CreateCardModal from "@/components/modal/cardModal/CardModal"; import Card from "@/components/card/Card"; import Chip from "@/components/common/chip/Chip"; import KebabModal from "@/components/modal/KebabModal"; import MyButton from "@/components/common/Button"; import Button from "@/components/common/Button"; import { ColumnProps } from "@/features/dashboard/types"; -import { useColumnId } from "@/features/columns/store"; import { getCards } from "@/features/cards/api"; import type { CardData } from "@/features/dashboard/types"; @@ -23,10 +23,9 @@ export default function Column({ columnId, dashboardId, setColumns, + columns, }: ColumnProps) { - const [modal, setModal] = useState(null); - const { setColumnIdData } = useColumnId(); - + const [modal, setModal] = useState(null); const loader = useRef(null); const [loading, setLoading] = useState(false); const [cursorId, setCursorId] = useState(undefined); @@ -87,7 +86,6 @@ export default function Column({ const handleClickCard = (cardId: number) => { if (dashboardId == null || columnId == null) return; - setColumnIdData(dashboardId, columnId, status, cardId); }; return ( @@ -102,7 +100,6 @@ export default function Column({ // pc "pc:w-[354px] pc:flex-shrink-0 pc:border-r pc:border-b-0", )} - onClick={() => console.log("컬럼클릭시 id 숫자", columnId)} > {/* 컬럼 헤더 */}
@@ -134,7 +131,12 @@ export default function Column({ {/* 카드 추가 버튼 */}
{})} + onClick={ + onAddCard ?? + (() => { + setModal("card"); + }) + } color="buttonBasic" className="flex h-10 w-full items-center justify-center" > @@ -146,7 +148,15 @@ export default function Column({
{cards?.map((card) => (
card.id && handleClickCard(card.id)}> - +
))} {hasMore &&
} @@ -154,6 +164,16 @@ export default function Column({
{/* 수정하기, 삭제하기 모달 관리 */} + {modal === "card" && ( + setModal(null)} + setColumns={setColumns} + dashboardId={dashboardId} + columnId={columnId} + mode="create" + /> + )} {modal === "manage" && columnId !== null && ( >; + columns?: ColumnData[]; + setColumns?: React.Dispatch>; + dashboardId?: number; + columnId?: number; + mode?: "create" | "edit"; + cardData?: Card | null; + cardId?: number; +}; + +export default function CardModal({ + isOpen, + setIsOpen, + setColumns, + dashboardId, + columnId, + columns, + mode = "create", + cardData = null, + cardId, +}: CardModalProps) { + // 폼 상태 + const [formData, setFormData] = useState({ + title: "", + description: "", + dueDate: null as Date | null, + tags: [] as Tag[], + imageFile: null as File | null, + imageUrl: "", + assigneeId: null as number | null, + selectedColumnId: null as number | null, + }); + + const [members, setMembers] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const isEditMode = mode === "edit"; + const isDisabled = formData.title.trim() === "" || formData.description.trim() === ""; + + // 상태 선택 옵션 (수정 모드용) + const statusOptions = + columns?.map((col) => ({ + value: String(col.id), + label: col.title, + chip: , + })) || []; + + // 멤버 목록 로드 + useEffect(() => { + if (!isOpen || !dashboardId) return; + + const loadMembers = async () => { + try { + const res = await getMembers(dashboardId, { page: 1, size: 20 }); + const memberOpts = res.members.map((m) => ({ + value: String(m.userId), + label: m.nickname, + chip: ( + {m.nickname} + ), + })); + setMembers(memberOpts); + } catch (err) { + console.error("멤버 목록 불러오기 실패:", err); + } + }; + + loadMembers(); + }, [isOpen, dashboardId]); + + // 카드 데이터로 폼 초기화 (수정 시) + useEffect(() => { + if (cardData && isOpen && isEditMode) { + setFormData({ + title: cardData.title || "", + description: cardData.description || "", + dueDate: cardData.dueDate ? new Date(cardData.dueDate) : null, + tags: (cardData.tags || []).map((t) => ({ + label: t, + color: getColorForTag(t), + })), + imageUrl: cardData.imageUrl || "", + imageFile: null, + assigneeId: cardData.assignee?.id || null, + selectedColumnId: columnId || null, + }); + } else if (!isEditMode && isOpen) { + // 생성 모드일 때 초기화 + setFormData({ + title: "", + description: "", + dueDate: null, + tags: [], + imageFile: null, + imageUrl: "", + assigneeId: null, + selectedColumnId: columnId || null, // undefined를 null로 변환 + }); + } + }, [cardData, isOpen, isEditMode, columnId]); + + // 폼 데이터 업데이트 + const updateFormData = (updates: Partial) => { + setFormData((prev) => ({ ...prev, ...updates })); + }; + + // 담당자 선택 + const handleAssigneeSelect = (opt: Option) => { + updateFormData({ assigneeId: Number(opt.value) }); + }; + + // 상태 선택 (수정시) + const handleStatusSelect = (opt: Option) => { + updateFormData({ selectedColumnId: Number(opt.value) }); + }; + + // 이미지 업로드 처리 + const uploadImage = async (): Promise => { + if (!formData.imageFile) { + return formData.imageUrl && !formData.imageUrl.startsWith("blob:") + ? formData.imageUrl + : undefined; + } + + // columnId가 undefined인 경우 에러 처리 + if (!columnId) { + console.error("columnId가 없습니다."); + return undefined; + } + + try { + const formDataObj = new FormData(); + formDataObj.append("image", formData.imageFile); + const uploadResult = await uploadCardImage(columnId, formDataObj); + + // 응답 구조에 따른 이미지 URL 추출 + const direct = (uploadResult as { imageUrl?: unknown }).imageUrl; + const nested = (uploadResult as { data?: { imageUrl?: unknown } }).data?.imageUrl; + + return typeof direct === "string" ? direct : typeof nested === "string" ? nested : undefined; + } catch (uploadError) { + console.error("이미지 업로드 실패", uploadError); + return undefined; + } + }; + + // 카드 생성/수정 처리 + const handleSubmit = async () => { + if (isDisabled || isLoading) return; + + if (!formData.assigneeId) { + alert("담당자를 선택해주세요."); + return; + } + + if (isEditMode && !cardId) { + alert("카드 정보를 찾을 수 없습니다."); + return; + } + + // 필수 값 검증 + if (!dashboardId || !columnId) { + alert("필수 정보가 누락되었습니다."); + return; + } + + setIsLoading(true); + + try { + // 이미지 업로드 + const imageUrl = await uploadImage(); + + // 카드 데이터 준비 + const targetColumnId = isEditMode ? formData.selectedColumnId || columnId : columnId; + + const cardPayload = { + assigneeUserId: formData.assigneeId, + dashboardId, + columnId: targetColumnId, + title: formData.title.trim(), + description: formData.description.trim(), + dueDate: formData.dueDate ? dayjs(formData.dueDate).format("YYYY-MM-DD HH:mm") : "", + tags: formData.tags.map((tag) => tag.label), + imageUrl, + }; + + if (isEditMode) { + await handleUpdateCard(cardPayload, targetColumnId); + } else { + await handleCreateCard(cardPayload); + } + + handleClose(); + } catch (error) { + console.error(`카드 ${isEditMode ? "수정" : "생성"} 오류:`, error); + alert( + (error as Error).message || `카드 ${isEditMode ? "수정" : "생성"} 중 오류가 발생했습니다.`, + ); + } finally { + setIsLoading(false); + } + }; + + // 카드 생성 처리 + const handleCreateCard = async (cardPayload: any) => { + if (!setColumns) return; + + const createResult = await createCard(cardPayload); + + // API 응답에서 카드 데이터 추출 + let createdCard: Card; + if (typeof createResult === "object" && createResult !== null && "data" in createResult) { + createdCard = createResult.data as Card; + } else { + createdCard = createResult as Card; + } + + // Card를 CardData 형식으로 변환 + const cardData = { + ...createdCard, + tags: createdCard.tags || [], // undefined인 경우 빈 배열로 처리 + }; + + setColumns((prevColumns) => + prevColumns.map((col) => + col.id === createdCard.columnId + ? { + ...col, + cards: [cardData, ...(col.cards ?? [])], + } + : col, + ), + ); + }; + + // 카드 수정 처리 + const handleUpdateCard = async (cardPayload: any, targetColumnId: number) => { + if (!setColumns || !cardId) return; + + const updateResult = await updateCard(cardId, cardPayload); + + // API 응답에서 카드 데이터 추출 + let updatedCard: Card; + if (typeof updateResult === "object" && updateResult !== null && "data" in updateResult) { + updatedCard = updateResult.data as Card; + } else { + updatedCard = updateResult as Card; + } + + // Card를 CardData 형식으로 변환 + const cardData = { + ...updatedCard, + tags: updatedCard.tags || [], // undefined인 경우 빈 배열로 처리 + }; + + setColumns((prevColumns) => { + return prevColumns.map((col) => { + // 원래 컬럼에서 카드 제거 (컬럼이 변경된 경우) + if (col.id === columnId && targetColumnId !== columnId) { + return { + ...col, + cards: col.cards?.filter((card) => card.id !== cardId) || [], + }; + } + + // 새 컬럼에 카드 추가 또는 기존 카드 업데이트 + if (col.id === targetColumnId) { + if (targetColumnId !== columnId) { + // 다른 컬럼으로 이동 + return { + ...col, + cards: [...(col.cards || []), cardData], + }; + } else { + // 같은 컬럼 내에서 업데이트 + return { + ...col, + cards: col.cards?.map((card) => (card.id === cardId ? cardData : card)) || [], + }; + } + } + + return col; + }); + }); + }; + + // 모달 닫기 및 폼 초기화 + const handleClose = () => { + setFormData({ + title: "", + description: "", + dueDate: null, + tags: [], + imageFile: null, + imageUrl: "", + assigneeId: null, + selectedColumnId: null, + }); + + setIsOpen(false); + }; + + // 현재 선택된 상태 값 찾기 + const getCurrentStatusValue = () => { + if (!formData.selectedColumnId) return undefined; + return String(formData.selectedColumnId); + }; + + if (!isOpen) return null; + + return ( + + + + +
+ {/* 수정 모드일 때만 상태 선택 표시 */} + {isEditMode && ( + + + +
+ + updateFormData({ title: e.currentTarget.value })} + placeholder="제목을 입력해주세요" + /> + + + +