From fb9c17a4dcef844af52fb576288fd3bb4f3f8030 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 07:15:52 +0900 Subject: [PATCH 01/11] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/board/BoardAdminSkeleton.tsx | 33 +++++++++++++++++++ .../admin/board/BoardPageContent.tsx | 32 ++---------------- 2 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 src/components/admin/board/BoardAdminSkeleton.tsx diff --git a/src/components/admin/board/BoardAdminSkeleton.tsx b/src/components/admin/board/BoardAdminSkeleton.tsx new file mode 100644 index 00000000..4f695389 --- /dev/null +++ b/src/components/admin/board/BoardAdminSkeleton.tsx @@ -0,0 +1,33 @@ +import { Skeleton } from '@/components/ui'; +import { BoardCardSkeleton } from './BoardCardSkeleton'; + +export function BoardAdminSkeleton() { + return ( +
+ {/* Toolbar skeleton */} +
+ + +
+ + {/* Board card skeletons */} +
+
+ +
+ +
+ +
+ +
+ + + +
+ + +
+
+ ); +} diff --git a/src/components/admin/board/BoardPageContent.tsx b/src/components/admin/board/BoardPageContent.tsx index a66129fe..b615adf8 100644 --- a/src/components/admin/board/BoardPageContent.tsx +++ b/src/components/admin/board/BoardPageContent.tsx @@ -39,6 +39,7 @@ import { MAX_CUSTOM_BOARDS } from '@/constants/admin/board.constants'; import type { Board, BoardListCache } from '@/types/admin/board'; import type { BoardFormData } from '@/components/admin/board/modal/constants'; import { SortableBoardCard } from './SortableBoardCard'; +import { BoardAdminSkeleton } from './BoardAdminSkeleton'; function subscribeMounted() { return () => {}; @@ -114,36 +115,7 @@ function BoardPageContent() { useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ); - if (isLoading || !data) { - return ( -
- {/* Toolbar skeleton */} -
- - -
- - {/* Board card skeletons */} -
-
- -
- -
- -
- -
- - - -
- - -
-
- ); - } + if (isLoading || !data) return ; const { boards } = data; From 2af1f36e77ad90773e28f2d0f903f1b5e7748404 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 07:20:33 +0900 Subject: [PATCH 02/11] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=8C=93=EA=B8=80=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=EC=8A=A4=EC=9C=84=EC=B9=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/board/BoardCard.tsx | 3 ++- src/components/admin/board/BoardPageContent.tsx | 5 ++--- src/hooks/queries/admin/useAdminBoardsQuery.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/admin/board/BoardCard.tsx b/src/components/admin/board/BoardCard.tsx index b0136160..5acc865e 100644 --- a/src/components/admin/board/BoardCard.tsx +++ b/src/components/admin/board/BoardCard.tsx @@ -31,7 +31,8 @@ function BoardCard({ ref, ...props }: BoardCardProps) { - const { name, description, visibility, postCount, commentEnabled, editable } = board; + const { name, description, visibility, postCount, commentEnabled, editable, kind } = board; + const showCommentToggle = kind !== 'ALL'; const [deleteOpen, setDeleteOpen] = useState(false); const handleDeleteConfirm = () => { diff --git a/src/components/admin/board/BoardPageContent.tsx b/src/components/admin/board/BoardPageContent.tsx index b615adf8..6079f304 100644 --- a/src/components/admin/board/BoardPageContent.tsx +++ b/src/components/admin/board/BoardPageContent.tsx @@ -16,10 +16,9 @@ import { } from '@dnd-kit/sortable'; import { useQueryClient } from '@tanstack/react-query'; -import { Icon, Skeleton } from '@/components/ui'; +import { Icon } from '@/components/ui'; import { InfoCircleIcon } from '@/assets/icons'; import { BoardCard } from '@/components/admin/board/BoardCard'; -import { BoardCardSkeleton } from '@/components/admin/board/BoardCardSkeleton'; import { BoardToolbar } from '@/components/admin/board/BoardToolbar'; import { CreateBoardModal } from '@/components/admin/board/modal/CreateBoardModal'; import { EditBoardModal } from '@/components/admin/board/modal/EditBoardModal'; @@ -283,7 +282,7 @@ function BoardPageContent() {

- 추가 게시판은 최대 {MAX_CUSTOM_BOARDS}개입니다. + 추가 게시판은 최대 {MAX_CUSTOM_BOARDS}개까지 가능 합니다.

diff --git a/src/hooks/queries/admin/useAdminBoardsQuery.ts b/src/hooks/queries/admin/useAdminBoardsQuery.ts index 7108f5b8..a25a97a4 100644 --- a/src/hooks/queries/admin/useAdminBoardsQuery.ts +++ b/src/hooks/queries/admin/useAdminBoardsQuery.ts @@ -31,7 +31,7 @@ export function useAdminBoardsQuery() { }, enabled: !!clubId, retry: false, - staleTime: 5 * 60 * 1000, + staleTime: 0, gcTime: 10 * 60 * 1000, }); } From 624b27177a4b561344b315168a585b7de4cea28b Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 11:12:10 +0900 Subject: [PATCH 03/11] =?UTF-8?q?fix:=20=EA=B3=B5=EC=A7=80=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=ED=8C=90=20=EB=8C=93=EA=B8=80=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/board/BoardCard.tsx | 4 ++-- src/components/admin/board/BoardPageContent.tsx | 12 ++++++++++-- src/types/admin/board.d.ts | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/admin/board/BoardCard.tsx b/src/components/admin/board/BoardCard.tsx index 5acc865e..d1059d58 100644 --- a/src/components/admin/board/BoardCard.tsx +++ b/src/components/admin/board/BoardCard.tsx @@ -91,11 +91,11 @@ function BoardCard({ {/* Comment toggle */}
- {commentEnabled !== null && ( + {showCommentToggle && ( <>

댓글 허용

{}; } +const FIXED_BOARD_ORDER: BoardKind[] = ['ALL', 'NOTICE']; + +function compareFixedBoards(a: Board, b: Board) { + const ai = FIXED_BOARD_ORDER.indexOf(a.kind); + const bi = FIXED_BOARD_ORDER.indexOf(b.kind); + return (ai === -1 ? FIXED_BOARD_ORDER.length : ai) - (bi === -1 ? FIXED_BOARD_ORDER.length : bi); +} + function BoardPageContent() { const clubId = useClubId(); const queryClient = useQueryClient(); @@ -196,7 +204,7 @@ function BoardPageContent() { ? boards.filter((b) => b.name.toLowerCase().includes(query)) : boards; - const fixedBoards = filteredBoards.filter((b) => !b.editable); + const fixedBoards = filteredBoards.filter((b) => !b.editable).sort(compareFixedBoards); const customBoards = filteredBoards.filter((b) => b.editable); const totalCustomCount = boards.filter((b) => b.editable).length; const reachedLimit = totalCustomCount >= MAX_CUSTOM_BOARDS; diff --git a/src/types/admin/board.d.ts b/src/types/admin/board.d.ts index d381797c..9ebc3660 100644 --- a/src/types/admin/board.d.ts +++ b/src/types/admin/board.d.ts @@ -8,7 +8,7 @@ export interface Board { kind: BoardKind; visibility: BoardVisibility; postCount: number; - /** 댓글 허용 여부. ALL/NOTICE 등 일부 게시판은 토글이 노출되지 않음 */ + /** 댓글 허용 여부. 전체(ALL) 게시판은 토글이 노출되지 않아 null 일 수 있음 */ commentEnabled: boolean | null; /** 사용자가 수정/삭제할 수 있는 게시판인지 (커스텀 게시판) */ editable: boolean; From a131e337cb84bb894f2c865bcbea1cc00b0e3ca2 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 11:21:48 +0900 Subject: [PATCH 04/11] =?UTF-8?q?fix:=20=EA=B3=B5=EC=A7=80=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=ED=8C=90=20=EB=8C=93=EA=B8=80=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=95=88=20=EB=90=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/board/BoardPageContent.tsx | 2 +- src/lib/apis/adminBoard.ts | 3 ++- src/utils/admin/boardMapper.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/admin/board/BoardPageContent.tsx b/src/components/admin/board/BoardPageContent.tsx index 3f0e9dd1..93f9717f 100644 --- a/src/components/admin/board/BoardPageContent.tsx +++ b/src/components/admin/board/BoardPageContent.tsx @@ -152,7 +152,7 @@ function BoardPageContent() { { boardId, body: { - name: target.name, + ...(target.kind === 'NOTICE' ? {} : { name: target.name }), description: target.description, commentEnabled: next, ...toApiPermission(target.visibility), diff --git a/src/lib/apis/adminBoard.ts b/src/lib/apis/adminBoard.ts index 0f4ae258..d33824a3 100644 --- a/src/lib/apis/adminBoard.ts +++ b/src/lib/apis/adminBoard.ts @@ -46,7 +46,8 @@ export interface CreateBoardBody { } export interface UpdateBoardBody { - name: string; + /** 공지 게시판의 댓글 허용 토글 요청 시에는 제외하고 전송 */ + name?: string; description: string; commentEnabled: boolean; writePermission: AdminBoardWritePermission; diff --git a/src/utils/admin/boardMapper.ts b/src/utils/admin/boardMapper.ts index fbf9b82f..7116d69f 100644 --- a/src/utils/admin/boardMapper.ts +++ b/src/utils/admin/boardMapper.ts @@ -28,7 +28,7 @@ export function toBoard(dto: AdminBoardDto): Board { kind: dto.type, visibility: mapVisibility(dto.writePermission, dto.isPrivate), postCount: dto.postCount, - commentEnabled: dto.type === 'ALL' || dto.type === 'NOTICE' ? null : dto.commentEnabled, + commentEnabled: dto.type === 'ALL' ? null : dto.commentEnabled, editable: dto.type === 'GENERAL', }; } From 5725e6cd4ad9dd5f4a9d5d476b5777deb4f0175d Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 11:29:08 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20muta?= =?UTF-8?q?tion=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/board/BoardPageContent.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/admin/board/BoardPageContent.tsx b/src/components/admin/board/BoardPageContent.tsx index 93f9717f..b80dfbdc 100644 --- a/src/components/admin/board/BoardPageContent.tsx +++ b/src/components/admin/board/BoardPageContent.tsx @@ -32,7 +32,7 @@ import { useUpdateBoardOrderMutation } from '@/hooks/queries/admin/useUpdateBoar import { adminBoardQueryKeys } from '@/hooks/queries/admin/boardQueryKeys'; import { ADMIN_BOARD_ERROR, getApiErrorCode, getApiErrorMessage } from '@/lib/apis/adminBoard'; import { useClubId } from '@/stores'; -import { toastError } from '@/stores/useToastStore'; +import { toastError, toastSuccess } from '@/stores/useToastStore'; import { toApiPermission } from '@/utils/admin/boardMapper'; import { MAX_CUSTOM_BOARDS } from '@/constants/admin/board.constants'; import type { Board, BoardKind, BoardListCache } from '@/types/admin/board'; @@ -78,7 +78,10 @@ function BoardPageContent() { const { handleDragStart, handleDragEnd } = useBoardDragReorder({ onReorder: updateBoardOrder }); const { mutate: createBoard } = useCreateBoardMutation({ - onSuccess: () => setCreateModalOpen(false), + onSuccess: () => { + setCreateModalOpen(false); + toastSuccess('게시판이 생성되었어요.'); + }, onError: (err) => { const code = getApiErrorCode(err); if (code === ADMIN_BOARD_ERROR.DUPLICATE_NAME) { @@ -90,7 +93,10 @@ function BoardPageContent() { }); const { mutate: updateBoard } = useUpdateBoardMutation({ - onSuccess: () => setEditingBoardId(null), + onSuccess: () => { + setEditingBoardId(null); + toastSuccess('게시판이 수정되었어요.'); + }, onError: (err) => { const code = getApiErrorCode(err); if (code === ADMIN_BOARD_ERROR.DUPLICATE_NAME) { @@ -102,7 +108,10 @@ function BoardPageContent() { }); const { mutate: deleteBoard } = useDeleteBoardMutation({ - onSuccess: () => setEditingBoardId(null), + onSuccess: () => { + setEditingBoardId(null); + toastSuccess('게시판이 삭제되었어요.'); + }, onError: (err) => { const code = getApiErrorCode(err); if (code === ADMIN_BOARD_ERROR.BOARD_NOT_FOUND) { @@ -114,6 +123,7 @@ function BoardPageContent() { }); const { mutate: toggleComment } = useUpdateBoardMutation({ + onSuccess: () => toastSuccess('댓글 허용 설정을 변경했어요.'), onError: (err) => toastError(getApiErrorMessage(err)), }); From a15a05cb6126c4eccbd2715b1d5d481b5ea312d2 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 11:48:14 +0900 Subject: [PATCH 06/11] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9=20=EC=A4=91=20=EC=9A=94=EC=B2=AD=EC=9D=B4=20=EB=8D=94?= =?UTF-8?q?=20=EA=B0=80=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EA=B0=80?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/board/BoardCard.tsx | 4 +++- .../admin/board/BoardPageContent.tsx | 19 +++++++++++++++++++ .../admin/board/SortableBoardCard.tsx | 3 +++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/admin/board/BoardCard.tsx b/src/components/admin/board/BoardCard.tsx index d1059d58..e58942e0 100644 --- a/src/components/admin/board/BoardCard.tsx +++ b/src/components/admin/board/BoardCard.tsx @@ -13,6 +13,7 @@ import { MegaphoneIcon } from '@/components/board'; interface BoardCardProps extends React.HTMLAttributes { board: Board; onToggleComments?: (next: boolean) => void; + commentTogglePending?: boolean; onEdit?: () => void; onDelete?: () => void; draggable?: boolean; @@ -24,6 +25,7 @@ function BoardCard({ className, board, onToggleComments, + commentTogglePending = false, onEdit, onDelete, draggable = true, @@ -97,7 +99,7 @@ function BoardCard({ diff --git a/src/components/admin/board/BoardPageContent.tsx b/src/components/admin/board/BoardPageContent.tsx index b80dfbdc..6d211bca 100644 --- a/src/components/admin/board/BoardPageContent.tsx +++ b/src/components/admin/board/BoardPageContent.tsx @@ -61,6 +61,7 @@ function BoardPageContent() { const [createNameError, setCreateNameError] = useState(null); const [editingBoardId, setEditingBoardId] = useState(null); const [editNameError, setEditNameError] = useState(null); + const [pendingToggleIds, setPendingToggleIds] = useState>(() => new Set()); const mounted = useSyncExternalStore( subscribeMounted, () => true, @@ -148,6 +149,8 @@ function BoardPageContent() { }; const handleToggleComments = (boardId: number, next: boolean) => { + if (pendingToggleIds.has(boardId)) return; + const target = boards.find((b) => b.boardId === boardId); if (!target) return; @@ -158,6 +161,12 @@ function BoardPageContent() { boards: prev.boards.map((b) => (b.boardId === boardId ? { ...b, commentEnabled: next } : b)), })); + setPendingToggleIds((prev) => { + const nextSet = new Set(prev); + nextSet.add(boardId); + return nextSet; + }); + toggleComment( { boardId, @@ -172,6 +181,13 @@ function BoardPageContent() { onError: () => { queryClient.setQueryData(cacheKey, snapshot); }, + onSettled: () => { + setPendingToggleIds((prev) => { + const nextSet = new Set(prev); + nextSet.delete(boardId); + return nextSet; + }); + }, }, ); }; @@ -247,6 +263,7 @@ function BoardPageContent() { board={board} draggable={false} onToggleComments={(next) => handleToggleComments(board.boardId, next)} + commentTogglePending={pendingToggleIds.has(board.boardId)} /> ))} @@ -266,6 +283,7 @@ function BoardPageContent() { board={board} draggable={false} onToggleComments={(next) => handleToggleComments(board.boardId, next)} + commentTogglePending={pendingToggleIds.has(board.boardId)} onEdit={() => setEditingBoardId(board.boardId)} onDelete={() => handleMoveToTrash(board)} /> @@ -288,6 +306,7 @@ function BoardPageContent() { key={board.boardId} board={board} onToggleComments={(next) => handleToggleComments(board.boardId, next)} + commentTogglePending={pendingToggleIds.has(board.boardId)} onEdit={() => setEditingBoardId(board.boardId)} onDelete={() => handleMoveToTrash(board)} /> diff --git a/src/components/admin/board/SortableBoardCard.tsx b/src/components/admin/board/SortableBoardCard.tsx index faa2ebc2..bf22dcd0 100644 --- a/src/components/admin/board/SortableBoardCard.tsx +++ b/src/components/admin/board/SortableBoardCard.tsx @@ -6,6 +6,7 @@ import { BoardCard } from './BoardCard'; interface SortableBoardCardProps { board: Board; onToggleComments: (next: boolean) => void; + commentTogglePending?: boolean; onEdit: () => void; onDelete: () => void; } @@ -13,6 +14,7 @@ interface SortableBoardCardProps { export function SortableBoardCard({ board, onToggleComments, + commentTogglePending, onEdit, onDelete, }: SortableBoardCardProps) { @@ -25,6 +27,7 @@ export function SortableBoardCard({ ref={setNodeRef} board={board} onToggleComments={onToggleComments} + commentTogglePending={commentTogglePending} onEdit={onEdit} onDelete={onDelete} dragHandleProps={{ ...attributes, ...listeners }} From 0722c872811603d0c4c86484d2ab27190e7fce9c Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 11:50:33 +0900 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/board/BoardPageContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/admin/board/BoardPageContent.tsx b/src/components/admin/board/BoardPageContent.tsx index 6d211bca..e9743a27 100644 --- a/src/components/admin/board/BoardPageContent.tsx +++ b/src/components/admin/board/BoardPageContent.tsx @@ -248,7 +248,7 @@ function BoardPageContent() { // onTrashClick={() => setTrashModalOpen(true)} onCreateClick={ reachedLimit - ? () => toastError(`추가 게시판은 최대 ${MAX_CUSTOM_BOARDS}개까지 만들 수 있어요.`) + ? () => toastError(`게시판은 최대 ${MAX_CUSTOM_BOARDS}개까지 만들 수 있어요.`) : () => setCreateModalOpen(true) } /> @@ -319,7 +319,7 @@ function BoardPageContent() {

- 추가 게시판은 최대 {MAX_CUSTOM_BOARDS}개까지 가능 합니다. + 게시판 추가는 최대 {MAX_CUSTOM_BOARDS}개까지 가능 합니다.

From 2bbf1ffd4ae2fc8f99de7ebb5c1ed71087a145cf Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 12:36:02 +0900 Subject: [PATCH 08/11] =?UTF-8?q?refactor:=20=EB=A0=88=EA=B1=B0=EC=8B=9C?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/board/BoardPageContent.tsx | 90 ++++++++++--------- src/hooks/queries/admin/index.ts | 1 + .../queries/admin/useAdminBoardsQuery.ts | 2 +- .../queries/admin/useCreateBoardMutation.ts | 1 + .../queries/admin/useDeleteBoardMutation.ts | 1 + .../admin/useToggleBoardCommentMutation.ts | 27 ++++++ .../queries/admin/useUpdateBoardMutation.ts | 1 + src/lib/apis/adminBoard.ts | 14 ++- 8 files changed, 90 insertions(+), 47 deletions(-) create mode 100644 src/hooks/queries/admin/useToggleBoardCommentMutation.ts diff --git a/src/components/admin/board/BoardPageContent.tsx b/src/components/admin/board/BoardPageContent.tsx index e9743a27..90dc9be8 100644 --- a/src/components/admin/board/BoardPageContent.tsx +++ b/src/components/admin/board/BoardPageContent.tsx @@ -27,6 +27,7 @@ import { useBoardDragReorder } from '@/hooks/admin'; import { useAdminBoardsQuery } from '@/hooks/queries/admin/useAdminBoardsQuery'; import { useCreateBoardMutation } from '@/hooks/queries/admin/useCreateBoardMutation'; import { useUpdateBoardMutation } from '@/hooks/queries/admin/useUpdateBoardMutation'; +import { useToggleBoardCommentMutation } from '@/hooks/queries/admin/useToggleBoardCommentMutation'; import { useDeleteBoardMutation } from '@/hooks/queries/admin/useDeleteBoardMutation'; import { useUpdateBoardOrderMutation } from '@/hooks/queries/admin/useUpdateBoardOrderMutation'; import { adminBoardQueryKeys } from '@/hooks/queries/admin/boardQueryKeys'; @@ -52,6 +53,17 @@ function compareFixedBoards(a: Board, b: Board) { return (ai === -1 ? FIXED_BOARD_ORDER.length : ai) - (bi === -1 ? FIXED_BOARD_ORDER.length : bi); } +function handleNameMutationError(setNameError: (msg: string) => void) { + return (err: unknown) => { + const code = getApiErrorCode(err); + if (code === ADMIN_BOARD_ERROR.DUPLICATE_NAME) { + setNameError('같은 이름의 게시판이 이미 있어요.'); + } else { + toastError(getApiErrorMessage(err)); + } + }; +} + function BoardPageContent() { const clubId = useClubId(); const queryClient = useQueryClient(); @@ -62,6 +74,20 @@ function BoardPageContent() { const [editingBoardId, setEditingBoardId] = useState(null); const [editNameError, setEditNameError] = useState(null); const [pendingToggleIds, setPendingToggleIds] = useState>(() => new Set()); + + const addPendingToggle = (boardId: number) => + setPendingToggleIds((prev) => { + const next = new Set(prev); + next.add(boardId); + return next; + }); + + const removePendingToggle = (boardId: number) => + setPendingToggleIds((prev) => { + const next = new Set(prev); + next.delete(boardId); + return next; + }); const mounted = useSyncExternalStore( subscribeMounted, () => true, @@ -83,14 +109,7 @@ function BoardPageContent() { setCreateModalOpen(false); toastSuccess('게시판이 생성되었어요.'); }, - onError: (err) => { - const code = getApiErrorCode(err); - if (code === ADMIN_BOARD_ERROR.DUPLICATE_NAME) { - setCreateNameError('같은 이름의 게시판이 이미 있어요.'); - } else { - toastError(getApiErrorMessage(err)); - } - }, + onError: handleNameMutationError(setCreateNameError), }); const { mutate: updateBoard } = useUpdateBoardMutation({ @@ -98,14 +117,7 @@ function BoardPageContent() { setEditingBoardId(null); toastSuccess('게시판이 수정되었어요.'); }, - onError: (err) => { - const code = getApiErrorCode(err); - if (code === ADMIN_BOARD_ERROR.DUPLICATE_NAME) { - setEditNameError('같은 이름의 게시판이 이미 있어요.'); - } else { - toastError(getApiErrorMessage(err)); - } - }, + onError: handleNameMutationError(setEditNameError), }); const { mutate: deleteBoard } = useDeleteBoardMutation({ @@ -123,7 +135,7 @@ function BoardPageContent() { }, }); - const { mutate: toggleComment } = useUpdateBoardMutation({ + const { mutate: toggleComment } = useToggleBoardCommentMutation({ onSuccess: () => toastSuccess('댓글 허용 설정을 변경했어요.'), onError: (err) => toastError(getApiErrorMessage(err)), }); @@ -161,17 +173,12 @@ function BoardPageContent() { boards: prev.boards.map((b) => (b.boardId === boardId ? { ...b, commentEnabled: next } : b)), })); - setPendingToggleIds((prev) => { - const nextSet = new Set(prev); - nextSet.add(boardId); - return nextSet; - }); + addPendingToggle(boardId); toggleComment( { boardId, body: { - ...(target.kind === 'NOTICE' ? {} : { name: target.name }), description: target.description, commentEnabled: next, ...toApiPermission(target.visibility), @@ -181,13 +188,7 @@ function BoardPageContent() { onError: () => { queryClient.setQueryData(cacheKey, snapshot); }, - onSettled: () => { - setPendingToggleIds((prev) => { - const nextSet = new Set(prev); - nextSet.delete(boardId); - return nextSet; - }); - }, + onSettled: () => removePendingToggle(boardId), }, ); }; @@ -238,6 +239,16 @@ function BoardPageContent() { const editingBoard = editingBoardId !== null ? (boards.find((b) => b.boardId === editingBoardId) ?? null) : null; + const getToggleProps = (board: Board) => ({ + onToggleComments: (next: boolean) => handleToggleComments(board.boardId, next), + commentTogglePending: pendingToggleIds.has(board.boardId), + }); + + const getEditableProps = (board: Board) => ({ + onEdit: () => setEditingBoardId(board.boardId), + onDelete: () => handleMoveToTrash(board), + }); + return (
( {index > 0 &&
} - handleToggleComments(board.boardId, next)} - commentTogglePending={pendingToggleIds.has(board.boardId)} - /> + ))}
@@ -282,10 +288,8 @@ function BoardPageContent() { key={board.boardId} board={board} draggable={false} - onToggleComments={(next) => handleToggleComments(board.boardId, next)} - commentTogglePending={pendingToggleIds.has(board.boardId)} - onEdit={() => setEditingBoardId(board.boardId)} - onDelete={() => handleMoveToTrash(board)} + {...getToggleProps(board)} + {...getEditableProps(board)} /> ))}
@@ -305,10 +309,8 @@ function BoardPageContent() { handleToggleComments(board.boardId, next)} - commentTogglePending={pendingToggleIds.has(board.boardId)} - onEdit={() => setEditingBoardId(board.boardId)} - onDelete={() => handleMoveToTrash(board)} + {...getToggleProps(board)} + {...getEditableProps(board)} /> ))}
diff --git a/src/hooks/queries/admin/index.ts b/src/hooks/queries/admin/index.ts index 8c1edb14..054bc9a1 100644 --- a/src/hooks/queries/admin/index.ts +++ b/src/hooks/queries/admin/index.ts @@ -8,5 +8,6 @@ export { useAdminSessionList } from './useAdminScheduleQueries'; export { useAdminBoardsQuery } from './useAdminBoardsQuery'; export { useCreateBoardMutation } from './useCreateBoardMutation'; export { useUpdateBoardMutation } from './useUpdateBoardMutation'; +export { useToggleBoardCommentMutation } from './useToggleBoardCommentMutation'; export { useDeleteBoardMutation } from './useDeleteBoardMutation'; export { useUpdateBoardOrderMutation } from './useUpdateBoardOrderMutation'; diff --git a/src/hooks/queries/admin/useAdminBoardsQuery.ts b/src/hooks/queries/admin/useAdminBoardsQuery.ts index a25a97a4..7108f5b8 100644 --- a/src/hooks/queries/admin/useAdminBoardsQuery.ts +++ b/src/hooks/queries/admin/useAdminBoardsQuery.ts @@ -31,7 +31,7 @@ export function useAdminBoardsQuery() { }, enabled: !!clubId, retry: false, - staleTime: 0, + staleTime: 5 * 60 * 1000, gcTime: 10 * 60 * 1000, }); } diff --git a/src/hooks/queries/admin/useCreateBoardMutation.ts b/src/hooks/queries/admin/useCreateBoardMutation.ts index ee820b03..b4d82688 100644 --- a/src/hooks/queries/admin/useCreateBoardMutation.ts +++ b/src/hooks/queries/admin/useCreateBoardMutation.ts @@ -11,6 +11,7 @@ export function useCreateBoardMutation(callbacks?: MutationCallbacks const queryClient = useQueryClient(); return useMutation({ + mutationKey: ['admin', 'board', 'create'], mutationFn: (body: CreateBoardBody) => { if (!clubId) throw new Error('clubId is required'); return adminBoardApi.createBoard(clubId, body); diff --git a/src/hooks/queries/admin/useDeleteBoardMutation.ts b/src/hooks/queries/admin/useDeleteBoardMutation.ts index 6d547d9a..2f276aef 100644 --- a/src/hooks/queries/admin/useDeleteBoardMutation.ts +++ b/src/hooks/queries/admin/useDeleteBoardMutation.ts @@ -11,6 +11,7 @@ export function useDeleteBoardMutation(callbacks?: MutationCallbacks const queryClient = useQueryClient(); return useMutation({ + mutationKey: ['admin', 'board', 'delete'], mutationFn: (boardId: number) => { if (!clubId) throw new Error('clubId is required'); return adminBoardApi.deleteBoard(clubId, boardId); diff --git a/src/hooks/queries/admin/useToggleBoardCommentMutation.ts b/src/hooks/queries/admin/useToggleBoardCommentMutation.ts new file mode 100644 index 00000000..e8c2e5ba --- /dev/null +++ b/src/hooks/queries/admin/useToggleBoardCommentMutation.ts @@ -0,0 +1,27 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; + +import { adminBoardApi, type UpdateBoardCommentBody } from '@/lib/apis/adminBoard'; +import { useClubId } from '@/stores'; +import type { MutationCallbacks } from '@/types/common'; +import { adminBoardQueryKeys } from './boardQueryKeys'; + +export function useToggleBoardCommentMutation(callbacks?: MutationCallbacks) { + const clubId = useClubId(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['admin', 'board', 'comment-toggle'], + mutationFn: ({ boardId, body }: { boardId: number; body: UpdateBoardCommentBody }) => { + if (!clubId) throw new Error('clubId is required'); + return adminBoardApi.updateBoardComment(clubId, boardId, body); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: adminBoardQueryKeys.list(clubId) }); + callbacks?.onSuccess?.(); + }, + onError: callbacks?.onError, + onMutate: callbacks?.onMutate, + onSettled: callbacks?.onSettled, + }); +} diff --git a/src/hooks/queries/admin/useUpdateBoardMutation.ts b/src/hooks/queries/admin/useUpdateBoardMutation.ts index 8dc68b66..8203ea05 100644 --- a/src/hooks/queries/admin/useUpdateBoardMutation.ts +++ b/src/hooks/queries/admin/useUpdateBoardMutation.ts @@ -11,6 +11,7 @@ export function useUpdateBoardMutation(callbacks?: MutationCallbacks const queryClient = useQueryClient(); return useMutation({ + mutationKey: ['admin', 'board', 'update'], mutationFn: ({ boardId, body }: { boardId: number; body: UpdateBoardBody }) => { if (!clubId) throw new Error('clubId is required'); return adminBoardApi.updateBoard(clubId, boardId, body); diff --git a/src/lib/apis/adminBoard.ts b/src/lib/apis/adminBoard.ts index d33824a3..1d37cf92 100644 --- a/src/lib/apis/adminBoard.ts +++ b/src/lib/apis/adminBoard.ts @@ -46,8 +46,15 @@ export interface CreateBoardBody { } export interface UpdateBoardBody { - /** 공지 게시판의 댓글 허용 토글 요청 시에는 제외하고 전송 */ - name?: string; + name: string; + description: string; + commentEnabled: boolean; + writePermission: AdminBoardWritePermission; + isPrivate: boolean; +} + +/** 댓글 허용 토글 전용 body. 공지 게시판도 안전하게 PATCH 가능하도록 name 미포함 */ +export interface UpdateBoardCommentBody { description: string; commentEnabled: boolean; writePermission: AdminBoardWritePermission; @@ -64,6 +71,9 @@ export const adminBoardApi = { updateBoard: (clubId: string, boardId: number, body: UpdateBoardBody) => apiClient.patch>(`/admin/clubs/${clubId}/boards/${boardId}`, body), + updateBoardComment: (clubId: string, boardId: number, body: UpdateBoardCommentBody) => + apiClient.patch>(`/admin/clubs/${clubId}/boards/${boardId}`, body), + deleteBoard: (clubId: string, boardId: number) => apiClient.delete>(`/admin/clubs/${clubId}/boards/${boardId}`), From 28be4597047e719453f2b94fdf23bc10b0204da0 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 19:29:56 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=ED=86=A0?= =?UTF-8?q?=EA=B8=80=20=ED=8B=88=EC=83=88=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/Switch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/Switch.tsx b/src/components/ui/Switch.tsx index 86a38e21..9f3838d0 100644 --- a/src/components/ui/Switch.tsx +++ b/src/components/ui/Switch.tsx @@ -14,7 +14,7 @@ function Switch({ className, ref, ...props }: SwitchProps) { ref={ref} className={cn( 'relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-[10px] disabled:cursor-not-allowed disabled:opacity-50', - 'before:absolute before:inset-x-0 before:top-1/2 before:h-[15px] before:-translate-y-1/2 before:rounded-[10px] before:transition-colors', + 'before:absolute before:inset-x-[2px] before:top-1/2 before:h-[15px] before:-translate-y-1/2 before:rounded-[10px] before:transition-colors', 'data-[state=checked]:before:bg-brand-primary data-[state=unchecked]:before:bg-button-neutral', className, )} From e66fa324dbd1241665f32a5ceb04dd2fd42f0aff Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 19:37:24 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=EC=84=9C=EB=B2=84=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=ED=8E=98=EC=B9=AD=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/admin/useBoardDragReorder.ts | 2 +- src/hooks/queries/admin/useUpdateBoardOrderMutation.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/admin/useBoardDragReorder.ts b/src/hooks/admin/useBoardDragReorder.ts index 541b57f0..3c88682d 100644 --- a/src/hooks/admin/useBoardDragReorder.ts +++ b/src/hooks/admin/useBoardDragReorder.ts @@ -70,7 +70,7 @@ function useBoardDragReorder({ onReorder, debounceMs = 500 }: UseBoardDragReorde reorderTimerRef.current = setTimeout(() => { reorderTimerRef.current = null; onReorder(reorderedIds, { - onError: () => queryClient.setQueryData(cacheKey, snapshot), + onError: () => queryClient.invalidateQueries({ queryKey: cacheKey }), }); }, debounceMs); }; diff --git a/src/hooks/queries/admin/useUpdateBoardOrderMutation.ts b/src/hooks/queries/admin/useUpdateBoardOrderMutation.ts index 5ba57e55..bebd2dec 100644 --- a/src/hooks/queries/admin/useUpdateBoardOrderMutation.ts +++ b/src/hooks/queries/admin/useUpdateBoardOrderMutation.ts @@ -12,6 +12,7 @@ export function useUpdateBoardOrderMutation(callbacks?: MutationCallbacks { if (!clubId) throw new Error('clubId is required'); return adminBoardApi.updateBoardOrder(clubId, boardIds); From 566e6c945adb48537e4d3ecd5a11696b74ee7c3c Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 20:24:40 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20=EB=A9=A4=EB=B2=84=20=EA=B8=B0?= =?UTF-8?q?=EC=88=98=EC=B5=9C=EC=8B=A0=20=EA=B8=B0=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=ED=95=98=EB=8F=84=EB=A1=9D=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/components/admin/member/CardinalPillList.tsx | 4 +++- src/components/admin/member/modal/MemberDetailModal.tsx | 9 ++++----- src/constants/term.ts | 8 ++++---- src/hooks/queries/admin/useCreateBoardMutation.ts | 3 +-- src/hooks/queries/admin/useDeleteBoardMutation.ts | 3 +-- src/hooks/queries/admin/useToggleBoardCommentMutation.ts | 3 +-- src/hooks/queries/admin/useUpdateBoardMutation.ts | 3 +-- src/hooks/queries/admin/useUpdateBoardOrderMutation.ts | 5 ++--- 8 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/components/admin/member/CardinalPillList.tsx b/src/components/admin/member/CardinalPillList.tsx index 0d0ef9f6..bd0ea215 100644 --- a/src/components/admin/member/CardinalPillList.tsx +++ b/src/components/admin/member/CardinalPillList.tsx @@ -1,7 +1,9 @@ 'use client'; import { MoreVerticalIcon } from '@/assets/icons'; -import { AddCardinalButton, AddCardinalModal, CardinalCard } from '@/components/admin'; +import { AddCardinalButton } from './AddCardinalButton'; +import { AddCardinalModal } from './modal/AddCardinalModal'; +import { CardinalCard } from './CardinalCard'; import { DropdownMenu, DropdownMenuContent, diff --git a/src/components/admin/member/modal/MemberDetailModal.tsx b/src/components/admin/member/modal/MemberDetailModal.tsx index 5f2d4792..b3dc6ab7 100644 --- a/src/components/admin/member/modal/MemberDetailModal.tsx +++ b/src/components/admin/member/modal/MemberDetailModal.tsx @@ -40,6 +40,8 @@ function MemberDetailModal({ const personalInfo = getPersonalInfo(member); const activityStats = getActivityStats(member); + const cardinals = parseCardinals(member.cardinal); + const latestCardinal = cardinals.at(-1); const footerActions = getFooterActions({ memberRole: member.memberRole, status: member.status, @@ -76,10 +78,7 @@ function MemberDetailModal({
{member.name} - {/* TODO: 응답 기수 정렬 확인 후 수정 (다중 기수일 때 첫 숫자만 노출됨) */} - - {parseInt(member.cardinal, 10) || member.cardinal || '-'}기 - + {latestCardinal ?? '-'}기
@@ -100,7 +99,7 @@ function MemberDetailModal({
활동 기수
- {parseCardinals(member.cardinal).map((c) => ( + {cardinals.map((c) => ( ) { +export function useCreateBoardMutation(callbacks?: MutationCallbacks) { const clubId = useClubId(); const queryClient = useQueryClient(); diff --git a/src/hooks/queries/admin/useDeleteBoardMutation.ts b/src/hooks/queries/admin/useDeleteBoardMutation.ts index 2f276aef..808d5281 100644 --- a/src/hooks/queries/admin/useDeleteBoardMutation.ts +++ b/src/hooks/queries/admin/useDeleteBoardMutation.ts @@ -1,12 +1,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import type { AxiosError } from 'axios'; import { adminBoardApi } from '@/lib/apis/adminBoard'; import { useClubId } from '@/stores'; import type { MutationCallbacks } from '@/types/common'; import { adminBoardQueryKeys } from './boardQueryKeys'; -export function useDeleteBoardMutation(callbacks?: MutationCallbacks) { +export function useDeleteBoardMutation(callbacks?: MutationCallbacks) { const clubId = useClubId(); const queryClient = useQueryClient(); diff --git a/src/hooks/queries/admin/useToggleBoardCommentMutation.ts b/src/hooks/queries/admin/useToggleBoardCommentMutation.ts index e8c2e5ba..a6602841 100644 --- a/src/hooks/queries/admin/useToggleBoardCommentMutation.ts +++ b/src/hooks/queries/admin/useToggleBoardCommentMutation.ts @@ -1,12 +1,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import type { AxiosError } from 'axios'; import { adminBoardApi, type UpdateBoardCommentBody } from '@/lib/apis/adminBoard'; import { useClubId } from '@/stores'; import type { MutationCallbacks } from '@/types/common'; import { adminBoardQueryKeys } from './boardQueryKeys'; -export function useToggleBoardCommentMutation(callbacks?: MutationCallbacks) { +export function useToggleBoardCommentMutation(callbacks?: MutationCallbacks) { const clubId = useClubId(); const queryClient = useQueryClient(); diff --git a/src/hooks/queries/admin/useUpdateBoardMutation.ts b/src/hooks/queries/admin/useUpdateBoardMutation.ts index 8203ea05..aef21bc9 100644 --- a/src/hooks/queries/admin/useUpdateBoardMutation.ts +++ b/src/hooks/queries/admin/useUpdateBoardMutation.ts @@ -1,12 +1,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import type { AxiosError } from 'axios'; import { adminBoardApi, type UpdateBoardBody } from '@/lib/apis/adminBoard'; import { useClubId } from '@/stores'; import type { MutationCallbacks } from '@/types/common'; import { adminBoardQueryKeys } from './boardQueryKeys'; -export function useUpdateBoardMutation(callbacks?: MutationCallbacks) { +export function useUpdateBoardMutation(callbacks?: MutationCallbacks) { const clubId = useClubId(); const queryClient = useQueryClient(); diff --git a/src/hooks/queries/admin/useUpdateBoardOrderMutation.ts b/src/hooks/queries/admin/useUpdateBoardOrderMutation.ts index bebd2dec..f1559d2e 100644 --- a/src/hooks/queries/admin/useUpdateBoardOrderMutation.ts +++ b/src/hooks/queries/admin/useUpdateBoardOrderMutation.ts @@ -1,5 +1,4 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import type { AxiosError } from 'axios'; import { adminBoardApi, getApiErrorMessage } from '@/lib/apis/adminBoard'; import { useClubId } from '@/stores'; @@ -7,7 +6,7 @@ import { toastError, toastSuccess } from '@/stores/useToastStore'; import type { MutationCallbacks } from '@/types/common'; import { adminBoardQueryKeys } from './boardQueryKeys'; -export function useUpdateBoardOrderMutation(callbacks?: MutationCallbacks) { +export function useUpdateBoardOrderMutation(callbacks?: MutationCallbacks) { const clubId = useClubId(); const queryClient = useQueryClient(); @@ -21,7 +20,7 @@ export function useUpdateBoardOrderMutation(callbacks?: MutationCallbacks { + onError: (err) => { toastError(getApiErrorMessage(err)); queryClient.invalidateQueries({ queryKey: adminBoardQueryKeys.list(clubId) }); callbacks?.onError?.(err);