From daabd121e8fa6755f5fee6f369eb63d176686004 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 22:58:35 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EB=8B=A8=EC=9D=BC=20=EC=84=B8?= =?UTF-8?q?=EC=85=98=20ui=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/schedule/session/SessionGroupRow.tsx | 14 +++++++++----- .../admin/schedule/session/SessionTabContent.tsx | 9 +++++++++ .../admin/schedule/session/SessionTable.tsx | 2 +- src/types/admin/session.d.ts | 14 ++++++++------ 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/components/admin/schedule/session/SessionGroupRow.tsx b/src/components/admin/schedule/session/SessionGroupRow.tsx index 4a9e81bd..6010236c 100644 --- a/src/components/admin/schedule/session/SessionGroupRow.tsx +++ b/src/components/admin/schedule/session/SessionGroupRow.tsx @@ -38,12 +38,16 @@ function SessionGroupRow({ onManageAttendance, onMore, }: SessionGroupRowProps) { - // 반복 세션 그룹일 때만 토글과 하위 테이블을 노출 - const isRecurring = group.recurrenceType !== 'NONE'; + // 반복 세션 그룹일 때만 토글과 하위 테이블을 노출 (서버는 단발 세션의 recurrenceType을 null로 보냄) + const isRecurring = group.recurrenceType != null && group.recurrenceType !== 'NONE'; const [expanded, setExpanded] = useState(true); - // 그룹 시작/종료일과 오늘을 비교해 SCHEDULED / OPEN / COMPLETED 도출 - const derivedGroupStatus = deriveSessionStatus(group.status, group.startDate, group.endDate); + // 단발 세션은 endDate가 null이므로 startDate로 폴백 (단일 날짜 = 시작일과 동일) + const derivedGroupStatus = deriveSessionStatus( + group.status, + group.startDate, + group.endDate ?? group.startDate, + ); return (
@@ -89,7 +93,7 @@ function SessionGroupRow({ > {isRecurring - ? formatSessionDateRange(group.startDate, group.endDate) + ? formatSessionDateRange(group.startDate, group.endDate ?? group.startDate) : formatSessionDate(group.startDate)}
diff --git a/src/components/admin/schedule/session/SessionTabContent.tsx b/src/components/admin/schedule/session/SessionTabContent.tsx index 1662c58c..c72e0340 100644 --- a/src/components/admin/schedule/session/SessionTabContent.tsx +++ b/src/components/admin/schedule/session/SessionTabContent.tsx @@ -46,6 +46,15 @@ function SessionTabContent({ toastError('수정·삭제 가능한 세션이 없습니다.'); return; } + // 서버는 단발 세션도 그룹 wrapper로 감싸서 보내지만(groupId·recurrenceType 모두 null), + // 의미상 그룹이 아니므로 자식 세션을 직접 수정 대상으로 전달해 단일 세션 흐름을 타도록 한다. + const isSingleSessionWrapper = + isSessionGroup(target) && + (target.recurrenceType == null || target.recurrenceType === 'NONE'); + if (isSingleSessionWrapper) { + setEditTarget({ target: target.sessions[0]! }); + return; + } setEditTarget({ target, parentGroup }); }; diff --git a/src/components/admin/schedule/session/SessionTable.tsx b/src/components/admin/schedule/session/SessionTable.tsx index e8804b08..fa30a12e 100644 --- a/src/components/admin/schedule/session/SessionTable.tsx +++ b/src/components/admin/schedule/session/SessionTable.tsx @@ -69,7 +69,7 @@ function SessionTable({ ) : ( groups.map((group, index) => ( 0} onManageAttendance={onManageAttendance} diff --git a/src/types/admin/session.d.ts b/src/types/admin/session.d.ts index 31d30bdb..63d4f2e9 100644 --- a/src/types/admin/session.d.ts +++ b/src/types/admin/session.d.ts @@ -14,15 +14,17 @@ export interface AdminSession { } export interface AdminSessionGroup { - groupId: number; + /** 단발(반복 없음) 세션은 그룹이 아니므로 null */ + groupId: number | null; title: string; - recurrenceType: SessionRecurrenceType; - /** "매주 목요일 오후 7:00 ~ 오후 9:00" 처럼 서버에서 렌더링된 문구 */ - recurrenceDescription: string; + /** 단발 세션은 null */ + recurrenceType: SessionRecurrenceType | null; + /** "매주 목요일 오후 7:00 ~ 오후 9:00" 처럼 서버에서 렌더링된 문구. 단발 세션은 null */ + recurrenceDescription: string | null; /** YYYY-MM-DD */ startDate: string; - /** YYYY-MM-DD */ - endDate: string; + /** YYYY-MM-DD. 단발 세션은 반복 종료일이 없으므로 null */ + endDate: string | null; completedCount: number; totalCount: number; status: SessionStatus; From 0feafaf9f6c1ad3486f55294a8d62557ea1460a3 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 23:40:50 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EC=84=B8=EC=85=98=20=EA=B0=95?= =?UTF-8?q?=EC=A0=9C=20=EC=82=AD=EC=A0=9C=20=EB=AA=A8=EB=8B=AC=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/components/admin/layout/LNB.tsx | 38 ++++++------------- src/components/admin/layout/LNBClubInfo.tsx | 2 +- src/constants/admin/schedule.constants.ts | 5 +++ src/hooks/admin/useSessionMutations.tsx | 15 +++++--- .../queries/admin/useAdminScheduleQueries.ts | 7 ++-- 5 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/components/admin/layout/LNB.tsx b/src/components/admin/layout/LNB.tsx index be0aa795..45c1ee97 100644 --- a/src/components/admin/layout/LNB.tsx +++ b/src/components/admin/layout/LNB.tsx @@ -2,12 +2,7 @@ import { useState } from 'react'; import { useParams, usePathname, useRouter } from 'next/navigation'; -import { - AdminForumIcon, - AdminCalendarIcon, - AdminSettingIcon, - AdminFileoutIcon, -} from '@/assets/icons/admin'; +import { AdminForumIcon, AdminCalendarIcon, AdminSettingIcon } from '@/assets/icons/admin'; import { CheckRoundIcon, ExitIcon, PeopleIcon } from '@/assets/icons'; import { @@ -59,16 +54,16 @@ function LNB() { }, ]; - const moveNavItems = [ - { - id: 'manual', - icon: AdminFileoutIcon, - label: '운영진 매뉴얼', - path: 'https://weeth-develop-2.s3.ap-northeast-2.amazonaws.com/Weeth_%E1%84%80%E1%85%AA%E1%86%AB%E1%84%85%E1%85%B5%E1%84%8C%E1%85%A1_%E1%84%86%E1%85%A6%E1%84%82%E1%85%B2%E1%84%8B%E1%85%A5%E1%86%AF_v3.pdf', - external: true, - openInWindow: true, - }, - ]; + // const moveNavItems = [ + // { + // id: 'manual', + // icon: AdminFileoutIcon, + // label: '운영진 매뉴얼', + // path: 'https://weeth-develop-2.s3.ap-northeast-2.amazonaws.com/Weeth_%E1%84%80%E1%85%AA%E1%86%AB%E1%84%85%E1%85%B5%E1%84%8C%E1%85%A1_%E1%84%86%E1%85%A6%E1%84%82%E1%85%B2%E1%84%8B%E1%85%A5%E1%86%AF_v3.pdf', + // external: true, + // openInWindow: true, + // }, + // ]; return ( @@ -119,17 +114,6 @@ function LNB() { collapsed={collapsed} onClick={() => setServiceDialogOpen(true)} /> - {moveNavItems.map(({ id, icon, label, path, external, openInWindow }) => ( - - ))} diff --git a/src/constants/admin/schedule.constants.ts b/src/constants/admin/schedule.constants.ts index 1a92ce35..62e6c05e 100644 --- a/src/constants/admin/schedule.constants.ts +++ b/src/constants/admin/schedule.constants.ts @@ -15,3 +15,8 @@ export const SCHEDULE_ERROR_MESSAGE: Record = { * 백엔드가 반환하는 에러 코드. 토스트 대신 호출자 UI(force-confirm 다이얼로그)에서 처리. */ export const SESSION_UPDATE_FORCE_REQUIRED_CODE = 20305; +export const SESSION_DELETE_FORCE_REQUIRED_CODE = 20306; +export const SESSION_FORCE_REQUIRED_CODES = [ + SESSION_UPDATE_FORCE_REQUIRED_CODE, + SESSION_DELETE_FORCE_REQUIRED_CODE, +] as const; diff --git a/src/hooks/admin/useSessionMutations.tsx b/src/hooks/admin/useSessionMutations.tsx index fc60842e..eb3ad965 100644 --- a/src/hooks/admin/useSessionMutations.tsx +++ b/src/hooks/admin/useSessionMutations.tsx @@ -18,6 +18,7 @@ import type { /** CLOSED 세션 포함 → force=true 재요청 동의 다이얼로그용 페이로드 */ interface ForceConfirm { + title: string; description: string; actionLabel: string; retry: () => void; @@ -61,6 +62,7 @@ function useSessionMutations() { onError: (error) => { if (!force && isSessionForceRequiredError(error)) { setForceConfirm({ + title: '종료된 세션이 포함되어 있어요', description: scope === 'THIS_AND_FUTURE' ? '이후 일정 중 이미 종료된 세션도 함께 수정할까요?' @@ -87,11 +89,12 @@ function useSessionMutations() { onError: (error) => { if (!force && isSessionForceRequiredError(error)) { setForceConfirm({ + title: '출석 데이터가 있어요', description: scope === 'THIS_AND_FUTURE' - ? '이후 일정 중 이미 종료된 세션도 함께 삭제할까요?' - : '이미 종료된 세션이에요. 그래도 삭제할까요?', - actionLabel: '모두 삭제', + ? '이후 일정 중 출석 데이터가 있는 세션이 있어요.\n삭제하면 출석 데이터도 함께 사라져요.' + : '출석 데이터가 있는 세션이에요. 그래도 삭제할까요?\n삭제하면 출석 데이터도 함께 사라져요.', + actionLabel: scope === 'THIS_AND_FUTURE' ? '모두 삭제' : '삭제', retry: () => submitDeleteSession(sessionId, scope, true, options), }); } @@ -108,7 +111,9 @@ function useSessionMutations() { onError: (error) => { if (!force && isSessionForceRequiredError(error)) { setForceConfirm({ - description: '종료된 세션도 포함되어 있어요. 그래도 그룹 전체를 삭제할까요?', + title: '출석 데이터가 있어요', + description: + '출석 데이터가 있는 세션이 포함되어 있어요.\n삭제하면 출석 데이터도 함께 사라져요.', actionLabel: '모두 삭제', retry: () => submitDeleteGroup(groupId, true, options), }); @@ -124,7 +129,7 @@ function useSessionMutations() { onOpenChange={(open) => { if (!open) setForceConfirm(null); }} - title="종료된 세션이 포함되어 있어요" + title={forceConfirm?.title ?? ''} description={forceConfirm?.description ?? ''} actionLabel={forceConfirm?.actionLabel ?? '확인'} cancelLabel="취소" diff --git a/src/hooks/queries/admin/useAdminScheduleQueries.ts b/src/hooks/queries/admin/useAdminScheduleQueries.ts index 2f25fdcb..75a85656 100644 --- a/src/hooks/queries/admin/useAdminScheduleQueries.ts +++ b/src/hooks/queries/admin/useAdminScheduleQueries.ts @@ -3,7 +3,7 @@ import { isAxiosError } from 'axios'; import { SCHEDULE_ERROR_MESSAGE, - SESSION_UPDATE_FORCE_REQUIRED_CODE, + SESSION_FORCE_REQUIRED_CODES, } from '@/constants/admin/schedule.constants'; import { adminScheduleApi } from '@/lib/apis/adminSchedule'; import { useClubId } from '@/stores'; @@ -16,10 +16,11 @@ import type { } from '@/types/admin/session'; import { MutationCallbacks } from '@/types'; -/** 세션 update/delete 응답이 "CLOSED 포함, force 필요" 에러인지 판별 */ +/** 세션 update(20305)/delete(20306) 응답이 "CLOSED 포함, force 필요" 에러인지 판별 */ function isSessionForceRequiredError(error: unknown): boolean { if (!isAxiosError(error)) return false; - return error.response?.data?.code === SESSION_UPDATE_FORCE_REQUIRED_CODE; + const code = error.response?.data?.code; + return SESSION_FORCE_REQUIRED_CODES.includes(code); } export { isSessionForceRequiredError }; From d7f440c1949dae03438d67ac6f5021d2eef855a0 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 29 Apr 2026 23:50:34 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=ED=8F=AC=EB=A9=A7=ED=8C=85=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/schedule/session/SessionTabContent.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/admin/schedule/session/SessionTabContent.tsx b/src/components/admin/schedule/session/SessionTabContent.tsx index c72e0340..825ed7d1 100644 --- a/src/components/admin/schedule/session/SessionTabContent.tsx +++ b/src/components/admin/schedule/session/SessionTabContent.tsx @@ -49,8 +49,7 @@ function SessionTabContent({ // 서버는 단발 세션도 그룹 wrapper로 감싸서 보내지만(groupId·recurrenceType 모두 null), // 의미상 그룹이 아니므로 자식 세션을 직접 수정 대상으로 전달해 단일 세션 흐름을 타도록 한다. const isSingleSessionWrapper = - isSessionGroup(target) && - (target.recurrenceType == null || target.recurrenceType === 'NONE'); + isSessionGroup(target) && (target.recurrenceType == null || target.recurrenceType === 'NONE'); if (isSingleSessionWrapper) { setEditTarget({ target: target.sessions[0]! }); return;