From 4227560c4d6ddc7a05c942a6c0aefb2b8c42ceed Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 16:28:07 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=BB=A8=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80,=20=ED=9B=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/context/columnListContext.tsx | 4 ++++ src/hooks/useColumnListContext.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/context/columnListContext.tsx create mode 100644 src/hooks/useColumnListContext.ts diff --git a/src/context/columnListContext.tsx b/src/context/columnListContext.tsx new file mode 100644 index 0000000..83114a3 --- /dev/null +++ b/src/context/columnListContext.tsx @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import type { ColumnsResponse } from '@/types/column'; + +export const ColumnListContext = createContext(null); diff --git a/src/hooks/useColumnListContext.ts b/src/hooks/useColumnListContext.ts new file mode 100644 index 0000000..cf62b51 --- /dev/null +++ b/src/hooks/useColumnListContext.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react'; +import { ColumnListContext } from '@/context/columnListContext'; + +export const useColumnListContext = () => { + const context = useContext(ColumnListContext); + + if (!context) { + throw new Error('ColumnListContext 안에서 사용하세요.'); + } + + return context; +}; From 0455e8daca8ace62e8726d846fa0d79ba32fc881 Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 16:59:18 +0900 Subject: [PATCH 02/11] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=94=84=EB=A1=9C=EB=B0=94?= =?UTF-8?q?=EC=9D=B4=EB=8D=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard-detail/ColumnListProvider.tsx | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/components/dashboard-detail/ColumnListProvider.tsx diff --git a/src/components/dashboard-detail/ColumnListProvider.tsx b/src/components/dashboard-detail/ColumnListProvider.tsx new file mode 100644 index 0000000..26aa950 --- /dev/null +++ b/src/components/dashboard-detail/ColumnListProvider.tsx @@ -0,0 +1,82 @@ +import { ColumnListContext } from '@/context/columnListContext'; +import useQuery from '@/hooks/useQuery'; +import { + changeColumn, + createColumn, + deleteColumn, + getColumnList, + type ChangeColumnVariables, + type CreateColumnType, +} from '@/lib/apis/columns'; +import type { ColumnsResponse } from '@/types/column'; + +interface ColumnListProviderProps { + dashboardId: string; + children: React.ReactNode; +} + +export default function ColumnListProvider({ dashboardId, children }: ColumnListProviderProps) { + const columnQuery = useQuery({ + fetchFn: () => getColumnList(dashboardId || ''), + params: { dashboardId }, + }); + + const createColumnFn = async (reqBody: CreateColumnType) => { + const { data } = await createColumn(reqBody); + + if (!data) { + return; + } + columnQuery.setData((prev) => { + if (!prev) { + return { + result: 'SUCCESS', + data: [data], + }; + } + + return { + ...prev, + data: [...prev.data, data], + }; + }); + + return data; + }; + + const updateColumnFn = async ({ columnId, body }: ChangeColumnVariables) => { + const { data: updated } = await changeColumn(columnId, body); + + columnQuery.setData((prev) => { + if (!prev) { + return prev; + } + + return { + ...prev, + data: prev.data.map((col) => + col.id === updated.id + ? { ...col, title: updated.title, updatedAt: updated.updatedAt } + : col + ), + }; + }); + + return updated; + }; + + const deleteColumnFn = async (columnId: number) => { + await deleteColumn(columnId); + columnQuery.refetch(); + }; + + const value = { + columnList: columnQuery.data?.data ?? [], + isLoading: columnQuery.isLoading, + createColumn: createColumnFn, + updateColumn: updateColumnFn, + deleteColumn: deleteColumnFn, + }; + + return {children}; +} From 78acfce21d7b3fc9741d5dcc12a028d814827ab4 Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 16:59:45 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Chore:=20changeColum?= =?UTF-8?q?nVariables=20=ED=83=80=EC=9E=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/apis/columns.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/apis/columns.ts b/src/lib/apis/columns.ts index e1e4f64..48978ef 100644 --- a/src/lib/apis/columns.ts +++ b/src/lib/apis/columns.ts @@ -6,6 +6,11 @@ export interface CreateColumnType { dashboardId: number; } +export interface ChangeColumnVariables { + columnId: number; + body: ChangeColumnType; +} + export interface ChangeColumnType { title: string; } From dfa22ca8111c7841e230fdd86c938e2b77bcd6fb Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 17:01:59 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=F0=9F=9A=9A=20Rename:=20change=20->=20up?= =?UTF-8?q?date=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/apis/columns.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/apis/columns.ts b/src/lib/apis/columns.ts index 48978ef..f9d0c2a 100644 --- a/src/lib/apis/columns.ts +++ b/src/lib/apis/columns.ts @@ -6,12 +6,12 @@ export interface CreateColumnType { dashboardId: number; } -export interface ChangeColumnVariables { +export interface UpdateColumnVariables { columnId: number; - body: ChangeColumnType; + body: UpdateColumnType; } -export interface ChangeColumnType { +export interface UpdateColumnType { title: string; } @@ -28,7 +28,7 @@ export const createColumn = async (reqBody: CreateColumnType) => { }; /** 컬럼 수정 api */ -export const changeColumn = async (columnId: number, reqBody: ChangeColumnType) => { +export const updateColumn = async (columnId: number, reqBody: UpdateColumnType) => { const res = await api.put(`/columns/${columnId}`, reqBody); return res; }; From 95ee7945b4ac98785ca5172d1e27318df44a622c Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 17:04:48 +0900 Subject: [PATCH 05/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=83=80=EC=9E=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/context/columnListContext.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/context/columnListContext.tsx b/src/context/columnListContext.tsx index 83114a3..baebafd 100644 --- a/src/context/columnListContext.tsx +++ b/src/context/columnListContext.tsx @@ -1,4 +1,13 @@ import { createContext } from 'react'; -import type { ColumnsResponse } from '@/types/column'; +import type { UpdateColumnVariables, CreateColumnType } from '@/lib/apis/columns'; +import type { ColumnsData } from '@/types/column'; -export const ColumnListContext = createContext(null); +interface ColumnListContextType { + columnList: ColumnsData[] | []; + isLoading: boolean; + createColumn: (body: CreateColumnType) => Promise; + updateColumn: (vars: UpdateColumnVariables) => Promise; + deleteColumn: (columnId: number) => Promise; +} + +export const ColumnListContext = createContext(null); From c64e09fb66cc480534152885cb2d693c9395379a Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 17:05:24 +0900 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=9A=9A=20Rename:=20change=20->=20up?= =?UTF-8?q?date=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/dashboard-detail/ColumnListProvider.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/dashboard-detail/ColumnListProvider.tsx b/src/components/dashboard-detail/ColumnListProvider.tsx index 26aa950..8f4f879 100644 --- a/src/components/dashboard-detail/ColumnListProvider.tsx +++ b/src/components/dashboard-detail/ColumnListProvider.tsx @@ -1,11 +1,11 @@ import { ColumnListContext } from '@/context/columnListContext'; import useQuery from '@/hooks/useQuery'; import { - changeColumn, + updateColumn, createColumn, deleteColumn, getColumnList, - type ChangeColumnVariables, + type UpdateColumnVariables, type CreateColumnType, } from '@/lib/apis/columns'; import type { ColumnsResponse } from '@/types/column'; @@ -44,8 +44,8 @@ export default function ColumnListProvider({ dashboardId, children }: ColumnList return data; }; - const updateColumnFn = async ({ columnId, body }: ChangeColumnVariables) => { - const { data: updated } = await changeColumn(columnId, body); + const updateColumnFn = async ({ columnId, body }: UpdateColumnVariables) => { + const { data: updated } = await updateColumn(columnId, body); columnQuery.setData((prev) => { if (!prev) { From b8aed650815d15e1e5b210c8900a50ce6b097efa Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 17:26:07 +0900 Subject: [PATCH 07/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?if=EB=AC=B8=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard-detail/ColumnListProvider.tsx | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/components/dashboard-detail/ColumnListProvider.tsx b/src/components/dashboard-detail/ColumnListProvider.tsx index 8f4f879..ff4933e 100644 --- a/src/components/dashboard-detail/ColumnListProvider.tsx +++ b/src/components/dashboard-detail/ColumnListProvider.tsx @@ -1,3 +1,4 @@ +import type { AxiosResponse } from 'axios'; import { ColumnListContext } from '@/context/columnListContext'; import useQuery from '@/hooks/useQuery'; import { @@ -8,7 +9,7 @@ import { type UpdateColumnVariables, type CreateColumnType, } from '@/lib/apis/columns'; -import type { ColumnsResponse } from '@/types/column'; +import type { ColumnsData, ColumnsResponse } from '@/types/column'; interface ColumnListProviderProps { dashboardId: string; @@ -21,53 +22,51 @@ export default function ColumnListProvider({ dashboardId, children }: ColumnList params: { dashboardId }, }); - const createColumnFn = async (reqBody: CreateColumnType) => { - const { data } = await createColumn(reqBody); + const createColumnFn = async (reqBody: CreateColumnType): Promise> => { + const res = await createColumn(reqBody); - if (!data) { - return; + if (!res.data) { + return res; } + columnQuery.setData((prev) => { if (!prev) { - return { - result: 'SUCCESS', - data: [data], - }; + return { result: 'SUCCESS', data: [res.data] }; } - - return { - ...prev, - data: [...prev.data, data], - }; + return { ...prev, data: [...prev.data, res.data] }; }); - return data; + return res; }; - const updateColumnFn = async ({ columnId, body }: UpdateColumnVariables) => { - const { data: updated } = await updateColumn(columnId, body); + const updateColumnFn = async ({ + columnId, + body, + }: UpdateColumnVariables): Promise> => { + const res = await updateColumn(columnId, body); columnQuery.setData((prev) => { if (!prev) { return prev; } - return { ...prev, - data: prev.data.map((col) => - col.id === updated.id - ? { ...col, title: updated.title, updatedAt: updated.updatedAt } - : col - ), + data: prev.data.map((col) => { + if (col.id === res.data.id) { + return { ...col, title: res.data.title, updatedAt: res.data.updatedAt }; + } + return col; + }), }; }); - return updated; + return res; }; - const deleteColumnFn = async (columnId: number) => { - await deleteColumn(columnId); + const deleteColumnFn = async (columnId: number): Promise> => { + const res = await deleteColumn(columnId); columnQuery.refetch(); + return res; }; const value = { From b11a3bd02fd7e88e8757b806a509fdd248ffa996 Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 17:26:27 +0900 Subject: [PATCH 08/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/context/columnListContext.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/context/columnListContext.tsx b/src/context/columnListContext.tsx index baebafd..ff525c6 100644 --- a/src/context/columnListContext.tsx +++ b/src/context/columnListContext.tsx @@ -1,3 +1,4 @@ +import type { AxiosResponse } from 'axios'; import { createContext } from 'react'; import type { UpdateColumnVariables, CreateColumnType } from '@/lib/apis/columns'; import type { ColumnsData } from '@/types/column'; @@ -5,9 +6,9 @@ import type { ColumnsData } from '@/types/column'; interface ColumnListContextType { columnList: ColumnsData[] | []; isLoading: boolean; - createColumn: (body: CreateColumnType) => Promise; - updateColumn: (vars: UpdateColumnVariables) => Promise; - deleteColumn: (columnId: number) => Promise; + createColumn: (body: CreateColumnType) => Promise>; + updateColumn: (vars: UpdateColumnVariables) => Promise>; + deleteColumn: (columnId: number) => Promise>; } export const ColumnListContext = createContext(null); From 4003796012d614607971409c3f781286317afef4 Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 17:27:32 +0900 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=ED=99=95=EC=9E=A5?= =?UTF-8?q?=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/context/{columnListContext.tsx => columnListContext.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/context/{columnListContext.tsx => columnListContext.ts} (100%) diff --git a/src/context/columnListContext.tsx b/src/context/columnListContext.ts similarity index 100% rename from src/context/columnListContext.tsx rename to src/context/columnListContext.ts From 58f095648b901e530f662f97bf47f0e4cc7f4b62 Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 17:36:03 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DashboardDetailContent.tsx | 217 ++++++++++++++ src/pages/DashboardDetail.tsx | 269 +----------------- 2 files changed, 223 insertions(+), 263 deletions(-) create mode 100644 src/components/dashboard-detail/DashboardDetailContent.tsx diff --git a/src/components/dashboard-detail/DashboardDetailContent.tsx b/src/components/dashboard-detail/DashboardDetailContent.tsx new file mode 100644 index 0000000..ef2ecf7 --- /dev/null +++ b/src/components/dashboard-detail/DashboardDetailContent.tsx @@ -0,0 +1,217 @@ +import { useCallback, useRef, useState } from 'react'; +import { useParams } from 'react-router'; +import CreateButton from '@/components/dashboard/CreateButton'; +import ColumnCardList from '@/components/dashboard-detail/card/ColumnCardList'; +import ColumnContainer from '@/components/dashboard-detail/column/ColumnContainer'; +import ChangeColumnModal from '@/components/dashboard-detail/modal/ChangeColumnModal'; +import CreateColumnModal from '@/components/dashboard-detail/modal/CreateColumnModal'; +import DeleteColumnModal from '@/components/dashboard-detail/modal/DeleteColumnModal'; +import ColumnSkeleton from '@/components/skeleton/ColumnSkeleton'; +import { CHANGE_COLUMN, CREATE_COLUMN, DELETE_COLUMN } from '@/constants/modalName'; +import { useColumnListContext } from '@/hooks/useColumnListContext'; +import { useModal } from '@/hooks/useModal'; +import useMutation from '@/hooks/useMutation'; +import useQuery from '@/hooks/useQuery'; +import type { CreateColumnType, UpdateColumnVariables } from '@/lib/apis/columns'; +import { getMemberList } from '@/lib/apis/members'; +import type { ColumnsData } from '@/types/column'; +import type { MembersResponse } from '@/types/members'; +import { cn } from '@/utils/cn'; + +export default function DashboardDetailContent() { + const { dashboardId } = useParams<{ dashboardId: string }>(); + const isCollapsed = localStorage.getItem('sidebar-collapsed') === 'true'; + + const { columnList, isLoading, createColumn, updateColumn, deleteColumn } = + useColumnListContext(); + const [selectedColumn, setSelectedColumn] = useState(null); + + const columnRefetchMap = useRef void>>({}); + + // 컬럼 모달 + const createColumnModal = useModal(CREATE_COLUMN); + const changeColumnModal = useModal(CHANGE_COLUMN); + const deleteColumnModal = useModal(DELETE_COLUMN); + + const memberQuery = useQuery({ + fetchFn: () => getMemberList({ dashboardId: dashboardId || '' }), + params: { dashboardId }, + }); + + // column mutation + const createColumnMutation = useMutation({ + mutationFn: (body: CreateColumnType) => createColumn(body), + onSuccess: () => { + createColumnModal.handleModalClose(); + }, + }); + + const updateColumnMutation = useMutation({ + mutationFn: (variables: UpdateColumnVariables) => updateColumn(variables), + onSuccess: () => { + setSelectedColumn(null); + changeColumnModal.handleModalClose(); + }, + }); + + const deleteColumnMutation = useMutation({ + mutationFn: (id: number) => deleteColumn(id), + onSuccess: () => { + deleteColumnModal.handleModalClose(); + }, + }); + + const registerRefetch = useCallback((columnId: number, fn: () => void) => { + columnRefetchMap.current[columnId] = fn; + }, []); + + const handleCardMoved = (fromColumnId: number, toColumnId: number) => { + if (fromColumnId === toColumnId) { + return; + } + + columnRefetchMap.current[fromColumnId]?.(); + columnRefetchMap.current[toColumnId]?.(); + }; + + if (!dashboardId) { + // TODO: 나중에 404 페이지로 리턴 + return
유효하지 않은 대시보드입니다.
; + } + + if (isLoading || !columnList) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ); + } + + // column handler + const handleSubmitCreateColumn = async (columnName: string) => { + const isDuplicate = columnList.some((col) => col.title.trim() === columnName.trim()); + + if (isDuplicate) { + throw new Error('중복된 컬럼 이름입니다.'); + } + + await createColumnMutation.mutate({ + title: columnName, + dashboardId: Number(dashboardId), + }); + }; + + const handleSubmitChangeColumn = async (nextTitle: string) => { + if (!selectedColumn) { + return; + } + + const isDuplicate = columnList.some( + (col) => col.title.trim() === nextTitle.trim() && col.id !== selectedColumn.id + ); + + if (isDuplicate) { + throw new Error('중복된 컬럼 이름입니다.'); + } + + await updateColumnMutation.mutate({ + columnId: selectedColumn.id, + body: { title: nextTitle }, + }); + }; + + const handleSubmitDeleteColumn = async () => { + if (!selectedColumn) { + return; + } + + await deleteColumnMutation.mutate(selectedColumn.id); + }; + + const canAddColumn = columnList.length < 10; + + return ( + <> +
+
+ {columnList.map((column) => ( + + { + setSelectedColumn(column); + updateColumnMutation.reset(); + changeColumnModal.handleModalOpen(); + }} + onRegisterRefetch={registerRefetch} + onCardMoved={handleCardMoved} + /> + + ))} +
+ {canAddColumn && ( + <> + {/* 데스크탑 버튼 */} + { + createColumnMutation.reset(); + createColumnModal.handleModalOpen(); + }}> + 새로운 컬럼 추가하기 + + + {/* 태블릿 ~ 모바일 버튼 */} +
+ { + createColumnMutation.reset(); + createColumnModal.handleModalOpen(); + }}> + 새로운 컬럼 추가하기 + +
+ + )} +
+ {/* 컬럼 생성 모달 */} + {createColumnModal.isOpen && ( + + )} + + {/* 컬럼 수정 모달 */} + {changeColumnModal.isOpen && selectedColumn && ( + { + deleteColumnMutation.reset(); + deleteColumnModal.handleModalOpenOnly(); + }} + /> + )} + + {/* 컬럼 삭제 모달 */} + {deleteColumnModal.isOpen && selectedColumn && ( + + )} + + ); +} diff --git a/src/pages/DashboardDetail.tsx b/src/pages/DashboardDetail.tsx index bb3fe9c..300ce40 100644 --- a/src/pages/DashboardDetail.tsx +++ b/src/pages/DashboardDetail.tsx @@ -1,274 +1,17 @@ -import { useCallback, useRef, useState } from 'react'; import { useParams } from 'react-router'; -import CreateButton from '@/components/dashboard/CreateButton'; -import ColumnCardList from '@/components/dashboard-detail/card/ColumnCardList'; -import ColumnContainer from '@/components/dashboard-detail/column/ColumnContainer'; -import ChangeColumnModal from '@/components/dashboard-detail/modal/ChangeColumnModal'; -import CreateColumnModal from '@/components/dashboard-detail/modal/CreateColumnModal'; -import DeleteColumnModal from '@/components/dashboard-detail/modal/DeleteColumnModal'; -import ColumnSkeleton from '@/components/skeleton/ColumnSkeleton'; -import { CHANGE_COLUMN, CREATE_COLUMN, DELETE_COLUMN } from '@/constants/modalName'; -import { useModal } from '@/hooks/useModal'; -import useMutation from '@/hooks/useMutation'; -import useQuery from '@/hooks/useQuery'; -import { - changeColumn, - createColumn, - deleteColumn, - getColumnList, - type ChangeColumnType, - type CreateColumnType, -} from '@/lib/apis/columns'; -import { getMemberList } from '@/lib/apis/members'; -import type { ColumnsData, ColumnsResponse } from '@/types/column'; -import type { MembersResponse } from '@/types/members'; -import { cn } from '@/utils/cn'; - -interface ChangeColumnVariables { - columnId: number; - body: ChangeColumnType; -} +import ColumnListProvider from '@/components/dashboard-detail/ColumnListProvider'; +import DashboardDetailContent from '@/components/dashboard-detail/DashboardDetailContent'; export default function DashboardDetail() { const { dashboardId } = useParams<{ dashboardId: string }>(); - const isCollapsed = localStorage.getItem('sidebar-collapsed') === 'true'; - const [selectedColumn, setSelectedColumn] = useState(null); - - const columnRefetchMap = useRef void>>({}); - - // 컬럼 모달 - const createColumnModal = useModal(CREATE_COLUMN); - const changeColumnModal = useModal(CHANGE_COLUMN); - const deleteColumnModal = useModal(DELETE_COLUMN); - - const columnQuery = useQuery({ - fetchFn: () => getColumnList(dashboardId || ''), - params: { dashboardId }, - }); - - const memberQuery = useQuery({ - fetchFn: () => getMemberList({ dashboardId: dashboardId || '' }), - params: { dashboardId }, - }); - - // column mutation - const createColumnMutation = useMutation({ - mutationFn: (reqBody) => createColumn(reqBody), - onSuccess: (response) => { - if (!response) { - return; - } - columnQuery.setData((prev) => { - if (!prev) { - return { - result: 'SUCCESS', - data: [response], - }; - } - - return { - ...prev, - data: [...prev.data, response], - }; - }); - - createColumnModal.handleModalClose(); - }, - }); - - const updateColumnMutation = useMutation({ - mutationFn: ({ columnId, body }) => changeColumn(columnId, body), - onSuccess: (updated) => { - if (!updated) { - return; - } - - columnQuery.setData((prev) => { - if (!prev) { - return prev; - } - - return { - ...prev, - data: prev.data.map((col) => - col.id === updated.id - ? { - ...col, - title: updated.title, - updatedAt: updated.updatedAt, - } - : col - ), - }; - }); - - setSelectedColumn(null); - changeColumnModal.handleModalClose(); - }, - }); - - const deleteColumnMutation = useMutation({ - mutationFn: (columnId: number) => deleteColumn(columnId), - onSuccess: () => { - columnQuery.refetch(); - deleteColumnModal.handleModalClose(); - }, - }); - - const registerRefetch = useCallback((columnId: number, fn: () => void) => { - columnRefetchMap.current[columnId] = fn; - }, []); - - const handleCardMoved = (fromColumnId: number, toColumnId: number) => { - if (fromColumnId === toColumnId) { - return; - } - - columnRefetchMap.current[fromColumnId]?.(); - columnRefetchMap.current[toColumnId]?.(); - }; if (!dashboardId) { - // TODO: 나중에 404 페이지로 리턴 - return
유효하지 않은 대시보드입니다.
; - } - - if (columnQuery.isLoading || !columnQuery.data) { - return ( -
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
- ); + return null; } - // column handler - const handleSubmitCreateColumn = async (columnName: string) => { - const isDuplicate = columnQuery.data?.data.some( - (col) => col.title.trim() === columnName.trim() - ); - - if (isDuplicate) { - throw new Error('중복된 컬럼 이름입니다.'); - } - - await createColumnMutation.mutate({ - title: columnName, - dashboardId: Number(dashboardId), - }); - }; - - const handleSubmitChangeColumn = async (nextTitle: string) => { - if (!selectedColumn) { - return; - } - - const isDuplicate = columnQuery.data?.data.some( - (col) => col.title.trim() === nextTitle.trim() && col.id !== selectedColumn.id - ); - - if (isDuplicate) { - throw new Error('중복된 컬럼 이름입니다.'); - } - - await updateColumnMutation.mutate({ - columnId: selectedColumn.id, - body: { title: nextTitle }, - }); - }; - - const handleSubmitDeleteColumn = async () => { - if (!selectedColumn) { - return; - } - - await deleteColumnMutation.mutate(selectedColumn.id); - }; - - const canAddColumn = columnQuery.data.data.length < 10; - return ( - <> -
-
- {columnQuery.data.data.map((column) => ( - - { - setSelectedColumn(column); - updateColumnMutation.reset(); - changeColumnModal.handleModalOpen(); - }} - onRegisterRefetch={registerRefetch} - onCardMoved={handleCardMoved} - /> - - ))} -
- {canAddColumn && ( - <> - {/* 데스크탑 버튼 */} - { - createColumnMutation.reset(); - createColumnModal.handleModalOpen(); - }}> - 새로운 컬럼 추가하기 - - - {/* 태블릿 ~ 모바일 버튼 */} -
- { - createColumnMutation.reset(); - createColumnModal.handleModalOpen(); - }}> - 새로운 컬럼 추가하기 - -
- - )} -
- {/* 컬럼 생성 모달 */} - {createColumnModal.isOpen && ( - - )} - - {/* 컬럼 수정 모달 */} - {changeColumnModal.isOpen && selectedColumn && ( - { - deleteColumnMutation.reset(); - deleteColumnModal.handleModalOpenOnly(); - }} - /> - )} - - {/* 컬럼 삭제 모달 */} - {deleteColumnModal.isOpen && selectedColumn && ( - - )} - + + + ); } From abf35fbc3fe885f79e8b1f2e967c924265f6a670 Mon Sep 17 00:00:00 2001 From: aahreum Date: Thu, 4 Dec 2025 17:44:21 +0900 Subject: [PATCH 11/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20props=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard-detail/card/ColumnCardList.tsx | 43 +++++++++---------- .../dashboard-detail/card/DashboardCard.tsx | 4 -- .../modal/ChangeCardModal.tsx | 11 +++-- .../card-detail-modal/CardDetailModal.tsx | 8 ++-- 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/src/components/dashboard-detail/card/ColumnCardList.tsx b/src/components/dashboard-detail/card/ColumnCardList.tsx index eca550e..1fcb79b 100644 --- a/src/components/dashboard-detail/card/ColumnCardList.tsx +++ b/src/components/dashboard-detail/card/ColumnCardList.tsx @@ -10,14 +10,13 @@ import useMutation from '@/hooks/useMutation'; import useUserContext from '@/hooks/useUserContext'; import { createCard, getCardListData, type CreateCardType } from '@/lib/apis/cards'; import type { CardDetailResponse, CardInitialValueType, CardsResponse } from '@/types/card'; -import type { ColumnsData, ColumnsResponse } from '@/types/column'; +import type { ColumnsData } from '@/types/column'; import type { MembersResponse } from '@/types/members'; import { createCardRequestBody } from '@/utils/card/createCardRequestBody'; import { uploadCardImage } from '@/utils/card/uploadCardImage'; interface ColumnCardListProps { column: ColumnsData; - columnListData: ColumnsResponse | null; dashboardId: string; memberData: MembersResponse | null; onHeaderClick: () => void; @@ -32,7 +31,6 @@ export default function ColumnCardList({ dashboardId, memberData, onHeaderClick, - columnListData, onRegisterRefetch, onCardMoved, }: ColumnCardListProps) { @@ -84,24 +82,6 @@ export default function ColumnCardList({ }, }); - if (isLoading && !infiniteData) { - return ( -
- - -
- ); - } - - if (!infiniteData || !memberData) { - return null; - } - - // TODO: 오류 컴포넌트 구현 - if (error) { - return
오류가 발생했습니다.
; - } - // card handler const handleSubmitCreateCard = async ( formValue: CardInitialValueType, @@ -117,6 +97,7 @@ export default function ColumnCardList({ ); await createCardMutation.mutate(reqBody); }; + const handleUpdateCard = (updated: CardDetailResponse) => { let fromColumnId: number | null = null; let toColumnId: number | null = null; @@ -166,6 +147,24 @@ export default function ColumnCardList({ }); }; + if (isLoading && !infiniteData) { + return ( +
+ + +
+ ); + } + + if (!infiniteData || !memberData) { + return null; + } + + // TODO: 오류 컴포넌트 구현 + if (error) { + return
오류가 발생했습니다.
; + } + const { cards, totalCount } = infiniteData; return ( @@ -190,13 +189,11 @@ export default function ColumnCardList({ onDeleteCard={() => handleDeleteCard(card.id)} onUpdateCard={handleUpdateCard} memberData={memberData} - columnListData={columnListData} /> ); })} - {/* 할 일 생성 모달 */} {createCardModal.isOpen && ( void; onUpdateCard: (updated: CardDetailResponse) => void; @@ -24,7 +22,6 @@ export default function DashboardCard({ cardData, columnId, columnTitle, - columnListData, memberData, onDeleteCard, onUpdateCard, @@ -86,7 +83,6 @@ export default function DashboardCard({ onDeleteCard={onDeleteCard} onUpdateCard={onUpdateCard} memberData={memberData} - columnListData={columnListData} /> )} diff --git a/src/components/dashboard-detail/modal/ChangeCardModal.tsx b/src/components/dashboard-detail/modal/ChangeCardModal.tsx index d9d23f9..eedb8e9 100644 --- a/src/components/dashboard-detail/modal/ChangeCardModal.tsx +++ b/src/components/dashboard-detail/modal/ChangeCardModal.tsx @@ -13,16 +13,15 @@ import Combobox, { import CardStatusBadge from '@/components/dashboard-detail/card/CardStatusBadge'; import TagInput from '@/components/dashboard-detail/modal/TagInput'; import { DUE_DATE, IMAGE_URL } from '@/constants/requestCardData'; +import { useColumnListContext } from '@/hooks/useColumnListContext'; import { useModal } from '@/hooks/useModal'; import type { CardEditFormValue } from '@/types/card'; -import type { ColumnsResponse } from '@/types/column'; import type { MembersResponse } from '@/types/members'; interface ChangeCardModalProps { memberData: MembersResponse; modalName: string; initialValue: CardEditFormValue; - columnListData: ColumnsResponse | null; serverErrorMessage: string | null; onSubmit: (formValue: CardEditFormValue, imageFile: File | null) => Promise; } @@ -31,11 +30,11 @@ export default function ChangeCardModal({ memberData, modalName, initialValue, - columnListData, serverErrorMessage, onSubmit, }: ChangeCardModalProps) { const { handleModalClose } = useModal(modalName); + const { columnList } = useColumnListContext(); const [formValue, setFormValue] = useState(initialValue); const [imageFile, setImageFile] = useState(null); const [defaultImageUrl, setDefaultImageUrl] = useState(initialValue.imageUrl); @@ -93,7 +92,7 @@ export default function ChangeCardModal({ } }; - if (!columnListData) { + if (!columnList) { return null; } @@ -111,14 +110,14 @@ export default function ChangeCardModal({ ({ id: col.id, title: col.title })) .find((col) => col.id === formValue.columnId) ?? null } setValue={(value) => handleComboboxChange('columnId', value)}> - {columnListData.data.map((col) => ( + {columnList.map((col) => ( diff --git a/src/components/dashboard-detail/modal/card-detail-modal/CardDetailModal.tsx b/src/components/dashboard-detail/modal/card-detail-modal/CardDetailModal.tsx index 3246061..2edbf61 100644 --- a/src/components/dashboard-detail/modal/card-detail-modal/CardDetailModal.tsx +++ b/src/components/dashboard-detail/modal/card-detail-modal/CardDetailModal.tsx @@ -7,13 +7,13 @@ import ChangeCardModal from '@/components/dashboard-detail/modal/ChangeCardModal import useCardDetail from '@/hooks/dashboard-detail/useCardDetail'; import useCommentActions from '@/hooks/dashboard-detail/useCommentActions'; import useUpdateCard from '@/hooks/dashboard-detail/useUpdateCard'; +import { useColumnListContext } from '@/hooks/useColumnListContext'; import { useModal } from '@/hooks/useModal'; import useMutation from '@/hooks/useMutation'; import { useResponsiveValue } from '@/hooks/useResponsiveValue'; import useUserContext from '@/hooks/useUserContext'; import { deleteCard } from '@/lib/apis/cards'; import type { CardDetailResponse, CardEditFormValue } from '@/types/card'; -import type { ColumnsResponse } from '@/types/column'; import type { CommentListResponse } from '@/types/comment'; import type { InfiniteScrollReturn } from '@/types/infiniteScroll'; import type { MembersResponse } from '@/types/members'; @@ -42,7 +42,6 @@ interface CardDetailModalProps { onDeleteCard: (id: number) => void; onUpdateCard: (updated: CardDetailResponse) => void; memberData: MembersResponse; - columnListData: ColumnsResponse | null; } export default function CardDetailModal({ @@ -53,7 +52,6 @@ export default function CardDetailModal({ onDeleteCard, onUpdateCard, memberData, - columnListData, }: CardDetailModalProps) { const { dashboardId } = useParams(); const { userProfile } = useUserContext(); @@ -63,6 +61,7 @@ export default function CardDetailModal({ const detailModal = useModal(`cardDetail_${cardId}`); const cardDetailQuery = useCardDetail(cardId); + const { columnList } = useColumnListContext(); const { commentList, submitComment, updateComment, deleteComment } = useCommentActions( cardId, columnId, @@ -127,7 +126,7 @@ export default function CardDetailModal({ const currentColumnId = cardDetailQuery.data?.columnId ?? columnId; const currentColumnTitle = - columnListData?.data.find((col) => col.id === currentColumnId)?.title ?? columnTitle; + columnList.find((col) => col.id === currentColumnId)?.title ?? columnTitle; return ( <> @@ -171,7 +170,6 @@ export default function CardDetailModal({