From 7126b51f2779a13506fe39afa5a1339f67b35f48 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Thu, 23 Apr 2026 21:01:04 +0900 Subject: [PATCH 01/16] =?UTF-8?q?fix:=20=EB=82=A0=EC=A7=9C=20=ED=94=BC?= =?UTF-8?q?=EC=BB=A4=20z=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/CalendarPicker.tsx | 2 +- src/components/ui/TimePicker.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ui/CalendarPicker.tsx b/src/components/ui/CalendarPicker.tsx index 2a89c754..90c0a4ae 100644 --- a/src/components/ui/CalendarPicker.tsx +++ b/src/components/ui/CalendarPicker.tsx @@ -102,7 +102,7 @@ function CalendarPicker({ value, onChange, minDate, maxDate }: CalendarPickerPro {/* Month navigation */}
diff --git a/src/components/ui/TimePicker.tsx b/src/components/ui/TimePicker.tsx index 59946faf..d7c19341 100644 --- a/src/components/ui/TimePicker.tsx +++ b/src/components/ui/TimePicker.tsx @@ -94,7 +94,7 @@ function TimePicker({ value, onChange }: TimePickerProps) { ref={attachWheel} sideOffset={4} align="start" - className="bg-container-neutral z-50 flex h-60 rounded-md shadow-[0px_4px_14px_0px_rgba(0,0,0,0.25)]" + className="bg-container-neutral z-90 flex h-60 rounded-md shadow-[0px_4px_14px_0px_rgba(0,0,0,0.25)]" > {/* Hours */}
From 8ab9dc052c8350d3d5ca7ede710ce08d29c2f645 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Thu, 23 Apr 2026 21:19:29 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20get=20api=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/schedule/SchedulePageContent.tsx | 42 +++---------------- .../queries/admin/useAdminScheduleQueries.ts | 27 ++++++++++++ src/lib/apis/adminSchedule.ts | 10 +++++ src/lib/apis/index.ts | 1 + 4 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 src/hooks/queries/admin/useAdminScheduleQueries.ts create mode 100644 src/lib/apis/adminSchedule.ts diff --git a/src/components/admin/schedule/SchedulePageContent.tsx b/src/components/admin/schedule/SchedulePageContent.tsx index 12159013..17d1d992 100644 --- a/src/components/admin/schedule/SchedulePageContent.tsx +++ b/src/components/admin/schedule/SchedulePageContent.tsx @@ -14,40 +14,11 @@ import { SessionTabContent } from '@/components/admin/schedule/session/SessionTa import { CreateScheduleModal } from '@/components/admin/schedule/modal/CreateScheduleModal'; import { EditScheduleModal } from '@/components/admin/schedule/modal/EditScheduleModal'; import { useCardinalSelector } from '@/hooks'; +import { useAdminMonthlySchedules } from '@/hooks/queries/admin/useAdminScheduleQueries'; import type { Schedule } from '@/types/admin/schedule'; type ScheduleTab = 'all' | 'session'; -const MOCK_SCHEDULES: Schedule[] = [ - { - scheduleId: 1, - title: '7기 1차 정기모임', - type: 'SESSION', - startDateTime: '2026-04-12T20:00:00', - endDateTime: '2026-04-12T22:00:00', - location: '가천대 체육관', - cardinalNumber: 7, - }, - { - scheduleId: 2, - title: '중간고사 기간', - type: 'GENERAL', - startDateTime: '2026-04-14T20:00:00', - endDateTime: '2026-04-14T22:00:00', - location: '가천관 123호', - cardinalNumber: 7, - }, - { - scheduleId: 3, - title: '7기 2차 정기모임', - type: 'SESSION', - startDateTime: '2026-04-15T20:00:00', - endDateTime: '2026-04-15T22:00:00', - location: '종합경기장', - cardinalNumber: 7, - }, -]; - function SchedulePageContent() { const { cardinals, selectedCardinalId, setSelectedCardinalId, activeCardinal } = useCardinalSelector({ autoSelectLatest: true }); @@ -58,22 +29,19 @@ function SchedulePageContent() { const [createModalOpen, setCreateModalOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); - const schedules = MOCK_SCHEDULES; + const { data: schedules = [] } = useAdminMonthlySchedules(currentYear, currentMonth); // 기수 필터링 const cardinalFiltered = selectedCardinalId === null ? schedules : schedules.filter((s) => s.cardinalNumber === activeCardinal?.cardinalNumber); - // 월 필터링 - const monthFiltered = cardinalFiltered.filter((s) => { - const date = new Date(s.startDateTime); - return date.getFullYear() === currentYear && date.getMonth() + 1 === currentMonth; - }); // 탭 필터링 const tabFiltered = - activeTab === 'session' ? monthFiltered.filter((s) => s.type === 'SESSION') : monthFiltered; + activeTab === 'session' + ? cardinalFiltered.filter((s) => s.type === 'SESSION') + : cardinalFiltered; // 검색 필터링 const query = searchValue.trim().toLowerCase(); diff --git a/src/hooks/queries/admin/useAdminScheduleQueries.ts b/src/hooks/queries/admin/useAdminScheduleQueries.ts new file mode 100644 index 00000000..faadd04e --- /dev/null +++ b/src/hooks/queries/admin/useAdminScheduleQueries.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; + +import { adminScheduleApi } from '@/lib/apis/adminSchedule'; +import { useClubId } from '@/stores'; + +function toMonthRange(year: number, month: number) { + const pad = (n: number) => String(n).padStart(2, '0'); + const lastDay = new Date(year, month, 0).getDate(); + return { + start: `${year}-${pad(month)}-01T00:00:00`, + end: `${year}-${pad(month)}-${pad(lastDay)}T23:59:59`, + }; +} + +export function useAdminMonthlySchedules(year: number, month: number) { + const clubId = useClubId(); + const { start, end } = toMonthRange(year, month); + + return useQuery({ + queryKey: ['admin', 'schedules', clubId, year, month], + queryFn: async () => { + const res = await adminScheduleApi.getMonthly(clubId!, start, end); + return res.data.data; + }, + enabled: !!clubId, + }); +} diff --git a/src/lib/apis/adminSchedule.ts b/src/lib/apis/adminSchedule.ts new file mode 100644 index 00000000..6945cfbc --- /dev/null +++ b/src/lib/apis/adminSchedule.ts @@ -0,0 +1,10 @@ +import { apiClient } from '@/lib/apis/client'; +import type { ApiResponse } from '@/types/common'; +import type { Schedule } from '@/types/admin/schedule'; + +export const adminScheduleApi = { + getMonthly: (clubId: string, start: string, end: string) => + apiClient.get>(`/clubs/${clubId}/schedules/monthly`, { + params: { start, end }, + }), +}; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 66325508..a2913aa3 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -15,3 +15,4 @@ export { cardinalApi } from './cardinal'; export { inquiryApi } from './inquiry'; export { adminClubApi } from './adminClub'; export { adminAttendanceApi } from './adminAttendance'; +export { adminScheduleApi } from './adminSchedule'; From 99abf4695d729a2a0080ee4d694e9b87ed8ed364 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Thu, 23 Apr 2026 21:44:33 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=83=9D=EC=84=B1=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/schedule/ScheduleTextField.tsx | 15 ++++++++- .../admin/schedule/ScheduleTextareaField.tsx | 10 +++++- .../modal/CreateGeneralScheduleForm.tsx | 31 ++++++++++++++++--- .../admin/schedule/modal/ScheduleFormBody.tsx | 4 +++ src/constants/admin/schedule.constants.ts | 4 +++ .../queries/admin/useAdminScheduleQueries.ts | 22 ++++++++++++- src/lib/apis/adminSchedule.ts | 4 ++- src/types/admin/schedule.d.ts | 9 ++++++ src/utils/admin/scheduleFormUtils.ts | 18 +++++++++++ 9 files changed, 108 insertions(+), 9 deletions(-) diff --git a/src/components/admin/schedule/ScheduleTextField.tsx b/src/components/admin/schedule/ScheduleTextField.tsx index 56ee1f9b..dbbf409f 100644 --- a/src/components/admin/schedule/ScheduleTextField.tsx +++ b/src/components/admin/schedule/ScheduleTextField.tsx @@ -5,9 +5,16 @@ interface ScheduleTextFieldProps { value: string; onChange: (value: string) => void; placeholder?: string; + maxLength?: number; } -function ScheduleTextField({ label, value, onChange, placeholder }: ScheduleTextFieldProps) { +function ScheduleTextField({ + label, + value, + onChange, + placeholder, + maxLength, +}: ScheduleTextFieldProps) { return ( onChange(e.target.value)} placeholder={placeholder} + maxLength={maxLength} className="bg-container-neutral typo-body1 placeholder:text-text-alternative text-text-normal h-12 w-full rounded-sm px-400 py-300 focus:outline-none" /> + {maxLength !== undefined && ( + + {value.length}/{maxLength} + + )} ); } diff --git a/src/components/admin/schedule/ScheduleTextareaField.tsx b/src/components/admin/schedule/ScheduleTextareaField.tsx index 03608ae0..55b52ffc 100644 --- a/src/components/admin/schedule/ScheduleTextareaField.tsx +++ b/src/components/admin/schedule/ScheduleTextareaField.tsx @@ -5,6 +5,7 @@ interface ScheduleTextareaFieldProps { value: string; onChange: (value: string) => void; placeholder?: string; + maxLength?: number; } function ScheduleTextareaField({ @@ -12,6 +13,7 @@ function ScheduleTextareaField({ value, onChange, placeholder, + maxLength, }: ScheduleTextareaFieldProps) { return ( @@ -19,8 +21,14 @@ function ScheduleTextareaField({ value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} - className="bg-container-neutral typo-body1 placeholder:text-text-alternative text-text-normal h-[150px] w-full resize-none rounded-sm px-400 py-300 focus:outline-none" + maxLength={maxLength} + className="bg-container-neutral typo-body1 placeholder:text-text-alternative text-text-normal h-37.5 w-full resize-none rounded-sm px-400 py-300 focus:outline-none" /> + {maxLength !== undefined && ( + + {value.length}/{maxLength} + + )} ); } diff --git a/src/components/admin/schedule/modal/CreateGeneralScheduleForm.tsx b/src/components/admin/schedule/modal/CreateGeneralScheduleForm.tsx index 041641a9..c21b973c 100644 --- a/src/components/admin/schedule/modal/CreateGeneralScheduleForm.tsx +++ b/src/components/admin/schedule/modal/CreateGeneralScheduleForm.tsx @@ -4,7 +4,13 @@ import { useState } from 'react'; import { Button } from '@/components/ui'; import { ScheduleFormBody } from '@/components/admin/schedule/modal/ScheduleFormBody'; +import { useCreateSchedule } from '@/hooks/queries/admin/useAdminScheduleQueries'; import { toDateInputValue } from '@/utils/shared/date'; +import { + isScheduleContentValid, + isScheduleLocationValid, + isScheduleTitleValid, +} from '@/utils/admin/scheduleFormUtils'; import { isDateRangeValid, type ScheduleFormState } from './types'; @@ -25,16 +31,31 @@ interface CreateGeneralScheduleFormProps { function CreateGeneralScheduleForm({ cardinalNumber, onClose }: CreateGeneralScheduleFormProps) { const [form, setForm] = useState(INITIAL_FORM); + const { mutate, isPending } = useCreateSchedule(); const updateForm = (patch: Partial) => setForm((prev) => ({ ...prev, ...patch })); - const isValid = form.title.trim().length > 0 && isDateRangeValid(form) && cardinalNumber !== null; + const isValid = + isScheduleTitleValid(form.title) && + isScheduleLocationValid(form.location) && + isScheduleContentValid(form.content) && + isDateRangeValid(form) && + cardinalNumber !== null; const handleSubmit = () => { - if (!isValid) return; - // TODO: 일반 일정 API 연동 시 구현 - onClose(); + if (!isValid || cardinalNumber === null) return; + mutate( + { + title: form.title, + content: form.content, + location: form.location, + cardinal: cardinalNumber, + start: `${form.startDate}T${form.startTime}:00`, + end: `${form.endDate}T${form.endTime}:00`, + }, + { onSuccess: onClose }, + ); }; return ( @@ -53,7 +74,7 @@ function CreateGeneralScheduleForm({ cardinalNumber, onClose }: CreateGeneralSch -
diff --git a/src/components/admin/schedule/modal/ScheduleFormBody.tsx b/src/components/admin/schedule/modal/ScheduleFormBody.tsx index 7351571f..6d93170e 100644 --- a/src/components/admin/schedule/modal/ScheduleFormBody.tsx +++ b/src/components/admin/schedule/modal/ScheduleFormBody.tsx @@ -1,6 +1,7 @@ import { ScheduleTextField } from '@/components/admin/schedule/ScheduleTextField'; import { ScheduleTextareaField } from '@/components/admin/schedule/ScheduleTextareaField'; import { DateTimeInput } from '@/components/ui/DateTimeInput'; +import { SCHEDULE_FIELD_LIMITS } from '@/utils/admin/scheduleFormUtils'; import type { ScheduleFormState } from './types'; @@ -24,6 +25,7 @@ function ScheduleFormBody({ value={form.title} onChange={(v) => onFormChange({ title: v })} placeholder={titlePlaceholder} + maxLength={SCHEDULE_FIELD_LIMITS.title} />
@@ -48,6 +50,7 @@ function ScheduleFormBody({ value={form.location} onChange={(v) => onFormChange({ location: v })} placeholder="장소를 입력해주세요." + maxLength={SCHEDULE_FIELD_LIMITS.location} /> onFormChange({ content: v })} placeholder="일정에 대한 설명을 입력해주세요." + maxLength={SCHEDULE_FIELD_LIMITS.content} />
); diff --git a/src/constants/admin/schedule.constants.ts b/src/constants/admin/schedule.constants.ts index 15507bf8..26047e23 100644 --- a/src/constants/admin/schedule.constants.ts +++ b/src/constants/admin/schedule.constants.ts @@ -4,3 +4,7 @@ export const SCHEDULE_TYPE_LABEL: Record = { SESSION: '세션', GENERAL: '일반 일정', }; + +export const SCHEDULE_ERROR_MESSAGE: Record = { + 20800: '존재하지 않는 일정입니다.', +}; diff --git a/src/hooks/queries/admin/useAdminScheduleQueries.ts b/src/hooks/queries/admin/useAdminScheduleQueries.ts index faadd04e..e82f20dc 100644 --- a/src/hooks/queries/admin/useAdminScheduleQueries.ts +++ b/src/hooks/queries/admin/useAdminScheduleQueries.ts @@ -1,7 +1,11 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import { SCHEDULE_ERROR_MESSAGE } from '@/constants/admin/schedule.constants'; import { adminScheduleApi } from '@/lib/apis/adminSchedule'; import { useClubId } from '@/stores'; +import { toastError } from '@/stores/useToastStore'; +import type { CreateEventBody } from '@/types/admin/schedule'; function toMonthRange(year: number, month: number) { const pad = (n: number) => String(n).padStart(2, '0'); @@ -25,3 +29,19 @@ export function useAdminMonthlySchedules(year: number, month: number) { enabled: !!clubId, }); } + +export function useCreateSchedule() { + const clubId = useClubId(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: CreateEventBody) => adminScheduleApi.createEvent(clubId!, body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin', 'schedules'] }); + }, + onError: (error) => { + const code = isAxiosError(error) ? error.response?.data?.code : undefined; + toastError(code ? (SCHEDULE_ERROR_MESSAGE[code] ?? undefined) : undefined); + }, + }); +} diff --git a/src/lib/apis/adminSchedule.ts b/src/lib/apis/adminSchedule.ts index 6945cfbc..475408a7 100644 --- a/src/lib/apis/adminSchedule.ts +++ b/src/lib/apis/adminSchedule.ts @@ -1,10 +1,12 @@ import { apiClient } from '@/lib/apis/client'; import type { ApiResponse } from '@/types/common'; -import type { Schedule } from '@/types/admin/schedule'; +import type { CreateEventBody, Schedule } from '@/types/admin/schedule'; export const adminScheduleApi = { getMonthly: (clubId: string, start: string, end: string) => apiClient.get>(`/clubs/${clubId}/schedules/monthly`, { params: { start, end }, }), + createEvent: (clubId: string, body: CreateEventBody) => + apiClient.post>(`/admin/clubs/${clubId}/events`, body), }; diff --git a/src/types/admin/schedule.d.ts b/src/types/admin/schedule.d.ts index 36e18698..6fd68145 100644 --- a/src/types/admin/schedule.d.ts +++ b/src/types/admin/schedule.d.ts @@ -9,3 +9,12 @@ export interface Schedule { location: string; cardinalNumber: number; } + +export interface CreateEventBody { + title: string; + content: string; + location: string; + cardinal: number; + start: string; + end: string; +} diff --git a/src/utils/admin/scheduleFormUtils.ts b/src/utils/admin/scheduleFormUtils.ts index 22b096aa..f6c2cb55 100644 --- a/src/utils/admin/scheduleFormUtils.ts +++ b/src/utils/admin/scheduleFormUtils.ts @@ -3,6 +3,24 @@ import type { AdminSession, AdminSessionGroup } from '@/types/admin/session'; import type { ScheduleFormState } from '../../components/admin/schedule/modal/types'; +export const SCHEDULE_FIELD_LIMITS = { + title: 30, + location: 30, + content: 500, +} as const; + +export function isScheduleTitleValid(title: string): boolean { + return title.trim().length > 0 && title.length <= SCHEDULE_FIELD_LIMITS.title; +} + +export function isScheduleLocationValid(location: string): boolean { + return location.length <= SCHEDULE_FIELD_LIMITS.location; +} + +export function isScheduleContentValid(content: string): boolean { + return content.length <= SCHEDULE_FIELD_LIMITS.content; +} + export function isSessionGroup( target: AdminSession | AdminSessionGroup, ): target is AdminSessionGroup { From 890eeae7f9f56ad55b968098f5e49158f353df57 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Thu, 23 Apr 2026 22:08:54 +0900 Subject: [PATCH 04/16] =?UTF-8?q?fix:=20=EC=9D=BC=EB=B0=98=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=84=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=95=88=20=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=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/app/globals.css | 1 + src/assets/icons/index.ts | 1 + src/assets/icons/location.svg | 3 +++ .../admin/schedule/{ => general}/ScheduleFormField.tsx | 0 src/components/admin/schedule/general/ScheduleItem.tsx | 8 ++++---- src/components/admin/schedule/general/ScheduleList.tsx | 2 +- .../schedule/{ => general}/SchedulePageContent.tsx | 8 ++++---- src/components/admin/schedule/general/ScheduleTag.tsx | 6 ++++-- .../admin/schedule/{ => general}/ScheduleTextField.tsx | 2 +- .../schedule/{ => general}/ScheduleTextareaField.tsx | 0 .../admin/schedule/modal/CreateScheduleModal.tsx | 4 ++-- .../admin/schedule/modal/ScheduleFormBody.tsx | 4 ++-- .../admin/schedule/modal/SessionScheduleForm.tsx | 6 +++--- src/constants/admin/schedule.constants.ts | 2 +- src/types/admin/schedule.d.ts | 10 +++++----- src/utils/admin/scheduleFormUtils.ts | 8 ++++---- 16 files changed, 36 insertions(+), 29 deletions(-) create mode 100644 src/assets/icons/location.svg rename src/components/admin/schedule/{ => general}/ScheduleFormField.tsx (100%) rename src/components/admin/schedule/{ => general}/SchedulePageContent.tsx (95%) rename src/components/admin/schedule/{ => general}/ScheduleTextField.tsx (91%) rename src/components/admin/schedule/{ => general}/ScheduleTextareaField.tsx (100%) diff --git a/src/app/globals.css b/src/app/globals.css index 0975fcf2..cdfb9460 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -212,6 +212,7 @@ --sub2-size: 14px; --sub2-line-height: 18px; --sub3-size: 14px; + --sub3-weight: var(--font-weight-semibold); --sub3-line-height: 18px; --body1-size: 14px; --body1-line-height: 22px; diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index ac12b53e..5c0e1b3e 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -51,3 +51,4 @@ export { default as TrashcanIcon } from './trash_can.svg'; export { default as PersonIcon } from './person.svg'; export { default as LogoutIcon } from './logout.svg'; export { default as TimeIcon } from './time.svg'; +export { default as LocationIcon } from './location.svg'; diff --git a/src/assets/icons/location.svg b/src/assets/icons/location.svg new file mode 100644 index 00000000..9b57a26c --- /dev/null +++ b/src/assets/icons/location.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/admin/schedule/ScheduleFormField.tsx b/src/components/admin/schedule/general/ScheduleFormField.tsx similarity index 100% rename from src/components/admin/schedule/ScheduleFormField.tsx rename to src/components/admin/schedule/general/ScheduleFormField.tsx diff --git a/src/components/admin/schedule/general/ScheduleItem.tsx b/src/components/admin/schedule/general/ScheduleItem.tsx index 14f208e2..a9699c07 100644 --- a/src/components/admin/schedule/general/ScheduleItem.tsx +++ b/src/components/admin/schedule/general/ScheduleItem.tsx @@ -20,9 +20,9 @@ function ScheduleItem({ onDelete, ...props }: ScheduleItemProps) { - const day = getDayOfMonth(schedule.startDateTime); - const dayLabel = getDayLabel(schedule.startDateTime); - const dateTimeText = formatScheduleDateTime(schedule.startDateTime); + const day = getDayOfMonth(schedule.start); + const dayLabel = getDayLabel(schedule.start); + const dateTimeText = formatScheduleDateTime(schedule.start); return (
- {schedule.title} + {schedule.title}
{SCHEDULE_TYPE_LABEL[schedule.type]} {dateTimeText} diff --git a/src/components/admin/schedule/general/ScheduleList.tsx b/src/components/admin/schedule/general/ScheduleList.tsx index 9bdbf751..c44393d0 100644 --- a/src/components/admin/schedule/general/ScheduleList.tsx +++ b/src/components/admin/schedule/general/ScheduleList.tsx @@ -42,7 +42,7 @@ function ScheduleList({ ) : ( schedules.map((schedule, index) => ( onEdit?.(schedule)} diff --git a/src/components/admin/schedule/SchedulePageContent.tsx b/src/components/admin/schedule/general/SchedulePageContent.tsx similarity index 95% rename from src/components/admin/schedule/SchedulePageContent.tsx rename to src/components/admin/schedule/general/SchedulePageContent.tsx index 17d1d992..8efde92b 100644 --- a/src/components/admin/schedule/SchedulePageContent.tsx +++ b/src/components/admin/schedule/general/SchedulePageContent.tsx @@ -35,7 +35,7 @@ function SchedulePageContent() { const cardinalFiltered = selectedCardinalId === null ? schedules - : schedules.filter((s) => s.cardinalNumber === activeCardinal?.cardinalNumber); + : schedules.filter((s) => s.cardinal === activeCardinal?.cardinalNumber); // 탭 필터링 const tabFiltered = @@ -53,7 +53,7 @@ function SchedulePageContent() { // 날짜순 정렬 const sortedSchedules = [...filteredSchedules].sort( - (a, b) => new Date(a.startDateTime).getTime() - new Date(b.startDateTime).getTime(), + (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime(), ); const handlePrevMonth = () => { @@ -153,13 +153,13 @@ function SchedulePageContent() { open={createModalOpen} onOpenChange={setCreateModalOpen} cardinalNumber={activeCardinal?.cardinalNumber ?? null} - initialTab={activeTab === 'session' ? 'SESSION' : 'GENERAL'} + initialTab={activeTab === 'session' ? 'SESSION' : 'EVENT'} /> {/* Edit schedule modal */} {editTarget && ( { if (!open) setEditTarget(null); diff --git a/src/components/admin/schedule/general/ScheduleTag.tsx b/src/components/admin/schedule/general/ScheduleTag.tsx index 9a5ed7f3..39c5e5f3 100644 --- a/src/components/admin/schedule/general/ScheduleTag.tsx +++ b/src/components/admin/schedule/general/ScheduleTag.tsx @@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/cn'; import { Icon } from '@/components/ui'; import { AdminCalendarIcon } from '@/assets/icons/admin'; -import { PinIcon } from '@/assets/icons'; +import { LocationIcon } from '@/assets/icons'; const scheduleTagVariants = cva( 'typo-caption1 inline-flex h-6 items-center gap-100 rounded-sm px-200 py-100 whitespace-nowrap', @@ -31,7 +31,9 @@ function ScheduleTag({ className, variant, icon, children, ...props }: ScheduleT {icon === 'calendar' && ( )} - {icon === 'location' && } + {icon === 'location' && ( + + )} {children} ); diff --git a/src/components/admin/schedule/ScheduleTextField.tsx b/src/components/admin/schedule/general/ScheduleTextField.tsx similarity index 91% rename from src/components/admin/schedule/ScheduleTextField.tsx rename to src/components/admin/schedule/general/ScheduleTextField.tsx index dbbf409f..f758ae89 100644 --- a/src/components/admin/schedule/ScheduleTextField.tsx +++ b/src/components/admin/schedule/general/ScheduleTextField.tsx @@ -1,4 +1,4 @@ -import { ScheduleFormField } from '@/components/admin/schedule/ScheduleFormField'; +import { ScheduleFormField } from '@/components/admin/schedule/general/ScheduleFormField'; interface ScheduleTextFieldProps { label: string; diff --git a/src/components/admin/schedule/ScheduleTextareaField.tsx b/src/components/admin/schedule/general/ScheduleTextareaField.tsx similarity index 100% rename from src/components/admin/schedule/ScheduleTextareaField.tsx rename to src/components/admin/schedule/general/ScheduleTextareaField.tsx diff --git a/src/components/admin/schedule/modal/CreateScheduleModal.tsx b/src/components/admin/schedule/modal/CreateScheduleModal.tsx index ee7f760e..413f81a1 100644 --- a/src/components/admin/schedule/modal/CreateScheduleModal.tsx +++ b/src/components/admin/schedule/modal/CreateScheduleModal.tsx @@ -23,7 +23,7 @@ function CreateScheduleModal({ open, onOpenChange, cardinalNumber, - initialTab = 'GENERAL', + initialTab = 'EVENT', onCreateSession, }: CreateScheduleModalProps) { const [activeTab, setActiveTab] = useState(initialTab); @@ -52,7 +52,7 @@ function CreateScheduleModal({ className="gap-0" > - {SCHEDULE_TYPE_LABEL.GENERAL} + {SCHEDULE_TYPE_LABEL.EVENT} {SCHEDULE_TYPE_LABEL.SESSION} diff --git a/src/components/admin/schedule/modal/ScheduleFormBody.tsx b/src/components/admin/schedule/modal/ScheduleFormBody.tsx index 6d93170e..6b9def6c 100644 --- a/src/components/admin/schedule/modal/ScheduleFormBody.tsx +++ b/src/components/admin/schedule/modal/ScheduleFormBody.tsx @@ -1,5 +1,5 @@ -import { ScheduleTextField } from '@/components/admin/schedule/ScheduleTextField'; -import { ScheduleTextareaField } from '@/components/admin/schedule/ScheduleTextareaField'; +import { ScheduleTextField } from '@/components/admin/schedule/general/ScheduleTextField'; +import { ScheduleTextareaField } from '@/components/admin/schedule/general/ScheduleTextareaField'; import { DateTimeInput } from '@/components/ui/DateTimeInput'; import { SCHEDULE_FIELD_LIMITS } from '@/utils/admin/scheduleFormUtils'; diff --git a/src/components/admin/schedule/modal/SessionScheduleForm.tsx b/src/components/admin/schedule/modal/SessionScheduleForm.tsx index 9b9317e3..482b7df4 100644 --- a/src/components/admin/schedule/modal/SessionScheduleForm.tsx +++ b/src/components/admin/schedule/modal/SessionScheduleForm.tsx @@ -11,9 +11,9 @@ import { import { CalendarPicker } from '@/components/ui/CalendarPicker'; import { DateTimeInput } from '@/components/ui/DateTimeInput'; import { ArrowDownIcon } from '@/assets/icons'; -import { ScheduleFormField } from '@/components/admin/schedule/ScheduleFormField'; -import { ScheduleTextField } from '@/components/admin/schedule/ScheduleTextField'; -import { ScheduleTextareaField } from '@/components/admin/schedule/ScheduleTextareaField'; +import { ScheduleFormField } from '@/components/admin/schedule/general/ScheduleFormField'; +import { ScheduleTextField } from '@/components/admin/schedule/general/ScheduleTextField'; +import { ScheduleTextareaField } from '@/components/admin/schedule/general/ScheduleTextareaField'; import { SESSION_RECURRENCE_LABEL, SESSION_RECURRENCE_OPTIONS, diff --git a/src/constants/admin/schedule.constants.ts b/src/constants/admin/schedule.constants.ts index 26047e23..bed34326 100644 --- a/src/constants/admin/schedule.constants.ts +++ b/src/constants/admin/schedule.constants.ts @@ -2,7 +2,7 @@ import type { ScheduleType } from '@/types/admin/schedule'; export const SCHEDULE_TYPE_LABEL: Record = { SESSION: '세션', - GENERAL: '일반 일정', + EVENT: '일반 일정', }; export const SCHEDULE_ERROR_MESSAGE: Record = { diff --git a/src/types/admin/schedule.d.ts b/src/types/admin/schedule.d.ts index 6fd68145..425730fd 100644 --- a/src/types/admin/schedule.d.ts +++ b/src/types/admin/schedule.d.ts @@ -1,13 +1,13 @@ -export type ScheduleType = 'SESSION' | 'GENERAL'; +export type ScheduleType = 'SESSION' | 'EVENT'; export interface Schedule { - scheduleId: number; + id: number; title: string; type: ScheduleType; - startDateTime: string; - endDateTime: string; + start: string; + end: string; location: string; - cardinalNumber: number; + cardinal: number; } export interface CreateEventBody { diff --git a/src/utils/admin/scheduleFormUtils.ts b/src/utils/admin/scheduleFormUtils.ts index f6c2cb55..4d8b7c8a 100644 --- a/src/utils/admin/scheduleFormUtils.ts +++ b/src/utils/admin/scheduleFormUtils.ts @@ -53,10 +53,10 @@ export function toInitialSessionForm(target: AdminSession | AdminSessionGroup): export function toInitialScheduleForm(schedule: Schedule): ScheduleFormState { return { title: schedule.title, - startDate: schedule.startDateTime.slice(0, 10), - startTime: schedule.startDateTime.slice(11, 16), - endDate: schedule.endDateTime.slice(0, 10), - endTime: schedule.endDateTime.slice(11, 16), + startDate: schedule.start.slice(0, 10), + startTime: schedule.start.slice(11, 16), + endDate: schedule.end.slice(0, 10), + endTime: schedule.end.slice(11, 16), location: schedule.location, content: '', }; From ef0dab16ce0900eca5ef3e9cff98cd1524f08008 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Thu, 23 Apr 2026 22:32:01 +0900 Subject: [PATCH 05/16] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/index.ts | 7 +++++-- .../schedule/general/SchedulePageContent.tsx | 10 +++++++--- .../schedule/general/ScheduleTextareaField.tsx | 2 +- .../queries/admin/useAdminScheduleQueries.ts | 16 ++++++++++++++++ src/lib/apis/adminSchedule.ts | 2 ++ 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/components/admin/index.ts b/src/components/admin/index.ts index 90e245fa..300e2007 100644 --- a/src/components/admin/index.ts +++ b/src/components/admin/index.ts @@ -21,13 +21,16 @@ export { MemberPageContent } from './member/MemberPageContent'; export { MemberSearchBar, type MemberSearchBarProps } from './member/MemberSearchBar'; export { MemberTable } from './member/MemberTable'; export { MemberTopBar, type MemberTopBarProps } from './member/MemberTopBar'; -export { SchedulePageContent } from './schedule/SchedulePageContent'; +export { SchedulePageContent } from './schedule/general/SchedulePageContent'; export { EditScheduleModal, type EditScheduleModalProps } from './schedule/modal/EditScheduleModal'; export { CreateScheduleModal, type CreateScheduleModalProps, } from './schedule/modal/CreateScheduleModal'; -export { ScheduleFormField, type ScheduleFormFieldProps } from './schedule/ScheduleFormField'; +export { + ScheduleFormField, + type ScheduleFormFieldProps, +} from './schedule/general/ScheduleFormField'; export { BoardCard, type BoardCardProps } from './board/BoardCard'; export { BoardToolbar, type BoardToolbarProps } from './board/BoardToolbar'; export { BoardPageContent } from './board/BoardPageContent'; diff --git a/src/components/admin/schedule/general/SchedulePageContent.tsx b/src/components/admin/schedule/general/SchedulePageContent.tsx index 8efde92b..6feaa730 100644 --- a/src/components/admin/schedule/general/SchedulePageContent.tsx +++ b/src/components/admin/schedule/general/SchedulePageContent.tsx @@ -14,7 +14,10 @@ import { SessionTabContent } from '@/components/admin/schedule/session/SessionTa import { CreateScheduleModal } from '@/components/admin/schedule/modal/CreateScheduleModal'; import { EditScheduleModal } from '@/components/admin/schedule/modal/EditScheduleModal'; import { useCardinalSelector } from '@/hooks'; -import { useAdminMonthlySchedules } from '@/hooks/queries/admin/useAdminScheduleQueries'; +import { + useAdminMonthlySchedules, + useDeleteSchedule, +} from '@/hooks/queries/admin/useAdminScheduleQueries'; import type { Schedule } from '@/types/admin/schedule'; type ScheduleTab = 'all' | 'session'; @@ -30,6 +33,7 @@ function SchedulePageContent() { const [editTarget, setEditTarget] = useState(null); const { data: schedules = [] } = useAdminMonthlySchedules(currentYear, currentMonth); + const { mutate: deleteSchedule } = useDeleteSchedule(); // 기수 필터링 const cardinalFiltered = @@ -74,8 +78,8 @@ function SchedulePageContent() { } }; - const handleDelete = (_schedule: Schedule) => { - // TODO: API 연동 시 deleteSchedule(_schedule) 호출 + const handleDelete = (schedule: Schedule) => { + deleteSchedule(schedule.id); }; return ( diff --git a/src/components/admin/schedule/general/ScheduleTextareaField.tsx b/src/components/admin/schedule/general/ScheduleTextareaField.tsx index 55b52ffc..3d6c3c88 100644 --- a/src/components/admin/schedule/general/ScheduleTextareaField.tsx +++ b/src/components/admin/schedule/general/ScheduleTextareaField.tsx @@ -1,4 +1,4 @@ -import { ScheduleFormField } from '@/components/admin/schedule/ScheduleFormField'; +import { ScheduleFormField } from '@/components/admin/schedule/general/ScheduleFormField'; interface ScheduleTextareaFieldProps { label: string; diff --git a/src/hooks/queries/admin/useAdminScheduleQueries.ts b/src/hooks/queries/admin/useAdminScheduleQueries.ts index e82f20dc..73b32da4 100644 --- a/src/hooks/queries/admin/useAdminScheduleQueries.ts +++ b/src/hooks/queries/admin/useAdminScheduleQueries.ts @@ -45,3 +45,19 @@ export function useCreateSchedule() { }, }); } + +export function useDeleteSchedule() { + const clubId = useClubId(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (eventId: number) => adminScheduleApi.deleteEvent(clubId!, eventId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin', 'schedules'] }); + }, + onError: (error) => { + const code = isAxiosError(error) ? error.response?.data?.code : undefined; + toastError(code ? (SCHEDULE_ERROR_MESSAGE[code] ?? undefined) : undefined); + }, + }); +} diff --git a/src/lib/apis/adminSchedule.ts b/src/lib/apis/adminSchedule.ts index 475408a7..5ae7ac0f 100644 --- a/src/lib/apis/adminSchedule.ts +++ b/src/lib/apis/adminSchedule.ts @@ -9,4 +9,6 @@ export const adminScheduleApi = { }), createEvent: (clubId: string, body: CreateEventBody) => apiClient.post>(`/admin/clubs/${clubId}/events`, body), + deleteEvent: (clubId: string, eventId: number) => + apiClient.delete>(`/admin/clubs/${clubId}/events/${eventId}`), }; From b93a794bf39139d3e0b5c862cb697e0cae44c0e4 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Thu, 23 Apr 2026 22:51:15 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=82=AD=EC=A0=9C=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/modal/EditScheduleModal.tsx | 42 +++++++++++++++---- src/components/alert/CustomAlertDialog.tsx | 2 +- src/components/ui/DropdownMenu.tsx | 2 +- .../queries/admin/useAdminScheduleQueries.ts | 19 ++++++++- src/lib/apis/adminSchedule.ts | 4 +- src/types/admin/schedule.d.ts | 8 ++++ 6 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/components/admin/schedule/modal/EditScheduleModal.tsx b/src/components/admin/schedule/modal/EditScheduleModal.tsx index 9992d768..26cb8dd0 100644 --- a/src/components/admin/schedule/modal/EditScheduleModal.tsx +++ b/src/components/admin/schedule/modal/EditScheduleModal.tsx @@ -15,9 +15,18 @@ import { Dialog, DialogContent } from '@/components/ui/dialog'; import { AdminCloseIcon, AdminMeatballIcon } from '@/assets/icons/admin'; import type { Schedule } from '@/types/admin/schedule'; +import { useUpdateSchedule } from '@/hooks/queries/admin/useAdminScheduleQueries'; +import { + isFormChanged, + isScheduleContentValid, + isScheduleLocationValid, + isScheduleTitleValid, + toInitialScheduleForm, +} from '@/utils/admin/scheduleFormUtils'; + import { DiscardConfirmArea } from './DiscardConfirmArea'; import { ScheduleFormBody } from './ScheduleFormBody'; -import { isFormChanged, toInitialScheduleForm } from '../../../../utils/admin/scheduleFormUtils'; +import { isDateRangeValid } from './types'; import type { ScheduleFormState } from './types'; interface EditScheduleModalProps { @@ -33,8 +42,16 @@ function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditSched const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [discardSource, setDiscardSource] = useState<'close' | 'cancel' | null>(null); + const { mutate, isPending } = useUpdateSchedule(); + const hasChanges = isFormChanged(form, initialForm); + const isValid = + isScheduleTitleValid(form.title) && + isScheduleLocationValid(form.location) && + isScheduleContentValid(form.content) && + isDateRangeValid(form); + const updateForm = (patch: Partial) => { setForm((prev) => ({ ...prev, ...patch })); }; @@ -50,9 +67,20 @@ function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditSched }; const handleSubmit = () => { - if (!form.title.trim()) return; - // TODO: API 연동 시 수정 요청 - handleClose(); + if (!isValid) return; + mutate( + { + eventId: schedule.id, + body: { + title: form.title, + content: form.content, + location: form.location, + start: `${form.startDate}T${form.startTime}:00`, + end: `${form.endDate}T${form.endTime}:00`, + }, + }, + { onSuccess: handleClose }, + ); }; const handleDeleteConfirm = () => { @@ -79,7 +107,7 @@ function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditSched }} > { if (hasChanges) e.preventDefault(); @@ -128,7 +156,7 @@ function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditSched
{/* Body */} -
+

일반 일정 수정

저장 diff --git a/src/components/alert/CustomAlertDialog.tsx b/src/components/alert/CustomAlertDialog.tsx index 33122813..e6bcbd3e 100644 --- a/src/components/alert/CustomAlertDialog.tsx +++ b/src/components/alert/CustomAlertDialog.tsx @@ -92,7 +92,7 @@ function CustomAlertDialog({ diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx index 7e8c825f..35932d58 100644 --- a/src/components/ui/DropdownMenu.tsx +++ b/src/components/ui/DropdownMenu.tsx @@ -40,7 +40,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - 'bg-container-neutral z-50 flex min-w-[144px] flex-col items-center overflow-hidden rounded-md shadow-[0px_4px_14px_0px_rgba(0,0,0,0.25)]', + 'bg-container-neutral z-90 flex min-w-[144px] flex-col items-center overflow-hidden rounded-md shadow-[0px_4px_14px_0px_rgba(0,0,0,0.25)]', 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95', 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', className, diff --git a/src/hooks/queries/admin/useAdminScheduleQueries.ts b/src/hooks/queries/admin/useAdminScheduleQueries.ts index 73b32da4..1c113bed 100644 --- a/src/hooks/queries/admin/useAdminScheduleQueries.ts +++ b/src/hooks/queries/admin/useAdminScheduleQueries.ts @@ -5,7 +5,7 @@ import { SCHEDULE_ERROR_MESSAGE } from '@/constants/admin/schedule.constants'; import { adminScheduleApi } from '@/lib/apis/adminSchedule'; import { useClubId } from '@/stores'; import { toastError } from '@/stores/useToastStore'; -import type { CreateEventBody } from '@/types/admin/schedule'; +import type { CreateEventBody, UpdateEventBody } from '@/types/admin/schedule'; function toMonthRange(year: number, month: number) { const pad = (n: number) => String(n).padStart(2, '0'); @@ -46,6 +46,23 @@ export function useCreateSchedule() { }); } +export function useUpdateSchedule() { + const clubId = useClubId(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ eventId, body }: { eventId: number; body: UpdateEventBody }) => + adminScheduleApi.updateEvent(clubId!, eventId, body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin', 'schedules'] }); + }, + onError: (error) => { + const code = isAxiosError(error) ? error.response?.data?.code : undefined; + toastError(code ? (SCHEDULE_ERROR_MESSAGE[code] ?? undefined) : undefined); + }, + }); +} + export function useDeleteSchedule() { const clubId = useClubId(); const queryClient = useQueryClient(); diff --git a/src/lib/apis/adminSchedule.ts b/src/lib/apis/adminSchedule.ts index 5ae7ac0f..58f7c09d 100644 --- a/src/lib/apis/adminSchedule.ts +++ b/src/lib/apis/adminSchedule.ts @@ -1,6 +1,6 @@ import { apiClient } from '@/lib/apis/client'; import type { ApiResponse } from '@/types/common'; -import type { CreateEventBody, Schedule } from '@/types/admin/schedule'; +import type { CreateEventBody, UpdateEventBody, Schedule } from '@/types/admin/schedule'; export const adminScheduleApi = { getMonthly: (clubId: string, start: string, end: string) => @@ -9,6 +9,8 @@ export const adminScheduleApi = { }), createEvent: (clubId: string, body: CreateEventBody) => apiClient.post>(`/admin/clubs/${clubId}/events`, body), + updateEvent: (clubId: string, eventId: number, body: UpdateEventBody) => + apiClient.patch>(`/admin/clubs/${clubId}/events/${eventId}`, body), deleteEvent: (clubId: string, eventId: number) => apiClient.delete>(`/admin/clubs/${clubId}/events/${eventId}`), }; diff --git a/src/types/admin/schedule.d.ts b/src/types/admin/schedule.d.ts index 425730fd..bffeda5d 100644 --- a/src/types/admin/schedule.d.ts +++ b/src/types/admin/schedule.d.ts @@ -18,3 +18,11 @@ export interface CreateEventBody { start: string; end: string; } + +export interface UpdateEventBody { + title: string; + content: string; + location: string; + start: string; + end: string; +} From fba04315e3549ed83422b73c6b8c58c0f3d1379b Mon Sep 17 00:00:00 2001 From: JIN921 Date: Thu, 23 Apr 2026 22:59:43 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20=EC=9D=BC=EC=8B=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/schedule/modal/ScheduleFormBody.tsx | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/components/admin/schedule/modal/ScheduleFormBody.tsx b/src/components/admin/schedule/modal/ScheduleFormBody.tsx index 6b9def6c..af6d43d2 100644 --- a/src/components/admin/schedule/modal/ScheduleFormBody.tsx +++ b/src/components/admin/schedule/modal/ScheduleFormBody.tsx @@ -3,6 +3,7 @@ import { ScheduleTextareaField } from '@/components/admin/schedule/general/Sched import { DateTimeInput } from '@/components/ui/DateTimeInput'; import { SCHEDULE_FIELD_LIMITS } from '@/utils/admin/scheduleFormUtils'; +import { isDateRangeValid } from './types'; import type { ScheduleFormState } from './types'; interface ScheduleFormBodyProps { @@ -28,21 +29,28 @@ function ScheduleFormBody({ maxLength={SCHEDULE_FIELD_LIMITS.title} /> -
- onFormChange({ startDate: v })} - onTimeChange={(v) => onFormChange({ startTime: v })} - /> - onFormChange({ endDate: v })} - onTimeChange={(v) => onFormChange({ endTime: v })} - /> +
+
+ onFormChange({ startDate: v })} + onTimeChange={(v) => onFormChange({ startTime: v })} + /> + onFormChange({ endDate: v })} + onTimeChange={(v) => onFormChange({ endTime: v })} + /> +
+ {!isDateRangeValid(form) && ( + + 종료 일시는 시작 일시보다 이후여야 합니다. + + )}
Date: Thu, 23 Apr 2026 23:40:34 +0900 Subject: [PATCH 08/16] =?UTF-8?q?fix:=20=EC=9D=BC=EC=A0=95/=EC=84=B8?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95=20=ED=83=AD=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/CardinalDropdown.tsx | 2 +- src/components/admin/layout/Header.tsx | 89 +++++++++---------- src/components/admin/layout/NavItem.tsx | 6 +- .../admin/schedule/general/MonthNavigator.tsx | 2 +- .../schedule/general/SchedulePageContent.tsx | 40 +++++++-- .../schedule/modal/CreateScheduleModal.tsx | 26 ++---- 6 files changed, 92 insertions(+), 73 deletions(-) diff --git a/src/components/admin/CardinalDropdown.tsx b/src/components/admin/CardinalDropdown.tsx index 51318dc7..db34bcec 100644 --- a/src/components/admin/CardinalDropdown.tsx +++ b/src/components/admin/CardinalDropdown.tsx @@ -24,7 +24,7 @@ function CardinalDropdown({ cardinals, activeCardinal, onSelect }: CardinalDropd type="button" className="border-line flex cursor-pointer items-center gap-700 rounded-sm border py-300 pr-300 pl-400" > - + {activeCardinal ? `${activeCardinal.cardinalNumber}기` : '기수'} diff --git a/src/components/admin/layout/Header.tsx b/src/components/admin/layout/Header.tsx index f2caed0b..31cfd31c 100644 --- a/src/components/admin/layout/Header.tsx +++ b/src/components/admin/layout/Header.tsx @@ -1,51 +1,47 @@ -'use client'; - -import { usePathname } from 'next/navigation'; - -const PAGE_METADATA: Record = { - '/admin/member': { - title: '멤버 관리', - description: - '가입 승인 등 멤버를 관리하는 페이지입니다. 정기모임을 모두 입력하신 후에 가입 승인을 해주시길 바랍니다.', - }, - '/admin/schedule': { - title: '일정 관리', - description: - '일반 일정과 세션 일정을 추가하고 수정하는 페이지입니다. 동아리 활동과 정기 모임 일정을 관리할 수 있습니다.', - }, - '/admin/attendance': { - title: '출석 관리', - description: '기수를 선택하고, 해당 모임에 대한 출석을 수정하는 페이지입니다.', - }, - '/admin/board': { - title: '게시판 관리', - description: - '게시판을 관리하는 페이지입니다. 유저 서비스에서 사용할 게시판을 설정할 수 있습니다.', - }, - '/admin/club-info': { - title: '동아리 관리', - description: '부원에게 공유할 동아리의 프로필과 그 외의 정보를 수정하는 페이지입니다.', - }, - - // '/admin/penalty': { - // title: '페널티 관리', - // description: '기수를 선택하고, 해당 멤버에 대한 페널티를 수정하는 페이지입니다.', - // }, - // '/admin/dues': { - // title: '회비 관리', - // description: - // '기수 시작시 이월된 회비와 해당 기수 회비를 종합해 회비를 등록해주시기 바랍니다. 회비 등록은 기수당 한 번만 가능합니다.', - // }, -}; +// const PAGE_METADATA: Record = { +// '/admin/member': { +// title: '멤버 관리', +// description: +// '가입 승인 등 멤버를 관리하는 페이지입니다. 정기모임을 모두 입력하신 후에 가입 승인을 해주시길 바랍니다.', +// }, +// '/admin/schedule': { +// title: '일정 관리', +// description: +// '일반 일정과 세션 일정을 추가하고 수정하는 페이지입니다. 동아리 활동과 정기 모임 일정을 관리할 수 있습니다.', +// }, +// '/admin/attendance': { +// title: '출석 관리', +// description: '기수를 선택하고, 해당 모임에 대한 출석을 수정하는 페이지입니다.', +// }, +// '/admin/board': { +// title: '게시판 관리', +// description: +// '게시판을 관리하는 페이지입니다. 유저 서비스에서 사용할 게시판을 설정할 수 있습니다.', +// }, +// '/admin/club-info': { +// title: '동아리 관리', +// description: '부원에게 공유할 동아리의 프로필과 그 외의 정보를 수정하는 페이지입니다.', +// }, +// // '/admin/penalty': { +// // title: '페널티 관리', +// // description: '기수를 선택하고, 해당 멤버에 대한 페널티를 수정하는 페이지입니다.', +// // }, +// // '/admin/dues': { +// // title: '회비 관리', +// // description: +// // '기수 시작시 이월된 회비와 해당 기수 회비를 종합해 회비를 등록해주시기 바랍니다. 회비 등록은 기수당 한 번만 가능합니다.', +// // }, +// }; +import { logoutAction } from '@/lib/actions/auth'; export function Header() { - const pathname = usePathname(); + // const pathname = usePathname(); - const metadata = Object.entries(PAGE_METADATA).find(([path]) => pathname.startsWith(path))?.[1]; + // const metadata = Object.entries(PAGE_METADATA).find(([path]) => pathname.startsWith(path))?.[1]; return ( -
- {metadata && ( +
+ {/* {metadata && ( <>
{metadata.title} @@ -53,9 +49,12 @@ export function Header() { {metadata.description} - )} + )} */} -
diff --git a/src/components/admin/layout/NavItem.tsx b/src/components/admin/layout/NavItem.tsx index ee9839aa..2da052b7 100644 --- a/src/components/admin/layout/NavItem.tsx +++ b/src/components/admin/layout/NavItem.tsx @@ -51,21 +51,21 @@ function NavItem({ onClick={() => window.open(path, '_blank', 'noopener,noreferrer')} > {iconEl} - {!collapsed && {label}} + {!collapsed && {label}} ); } else if (external) { el = ( {iconEl} - {!collapsed && {label}} + {!collapsed && {label}} ); } else { el = ( {iconEl} - {!collapsed && {label}} + {!collapsed && {label}} ); } diff --git a/src/components/admin/schedule/general/MonthNavigator.tsx b/src/components/admin/schedule/general/MonthNavigator.tsx index 1f54ab73..b5d949e9 100644 --- a/src/components/admin/schedule/general/MonthNavigator.tsx +++ b/src/components/admin/schedule/general/MonthNavigator.tsx @@ -23,7 +23,7 @@ function MonthNavigator({ className, year, month, onPrev, onNext, ...props }: Mo > 이전 달 - {formatYearMonth(year, month)} + {formatYearMonth(year, month)}
- @@ -142,13 +149,13 @@ function SchedulePageContent() { schedules={sortedSchedules} onEdit={setEditTarget} onDelete={handleDelete} - onCreateClick={() => setCreateModalOpen(true)} + onCreateClick={() => openCreateModal('EVENT')} /> - setCreateModalOpen(true)} /> + openCreateModal('SESSION')} /> @@ -157,11 +164,12 @@ function SchedulePageContent() { open={createModalOpen} onOpenChange={setCreateModalOpen} cardinalNumber={activeCardinal?.cardinalNumber ?? null} - initialTab={activeTab === 'session' ? 'SESSION' : 'EVENT'} + activeTab={createModalTab} + onActiveTabChange={setCreateModalTab} /> {/* Edit schedule modal */} - {editTarget && ( + {editTarget?.type === 'EVENT' && ( )} + + {/* Edit session modal */} + {editTarget?.type === 'SESSION' && ( + { + if (!open) setEditTarget(null); + }} + target={{ + id: editTarget.id, + cardinal: editTarget.cardinal, + title: editTarget.title, + start: editTarget.start, + end: editTarget.end, + status: 'SCHEDULED', + }} + onDelete={() => handleDelete(editTarget)} + /> + )}
); } diff --git a/src/components/admin/schedule/modal/CreateScheduleModal.tsx b/src/components/admin/schedule/modal/CreateScheduleModal.tsx index 413f81a1..8842bf26 100644 --- a/src/components/admin/schedule/modal/CreateScheduleModal.tsx +++ b/src/components/admin/schedule/modal/CreateScheduleModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; + import { Icon, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'; import { Dialog, DialogContent } from '@/components/ui/dialog'; @@ -15,7 +15,8 @@ interface CreateScheduleModalProps { open: boolean; onOpenChange: (open: boolean) => void; cardinalNumber: number | null; - initialTab?: ScheduleType; + activeTab: ScheduleType; + onActiveTabChange: (tab: ScheduleType) => void; onCreateSession?: (body: CreateSessionBody) => void; } @@ -23,32 +24,23 @@ function CreateScheduleModal({ open, onOpenChange, cardinalNumber, - initialTab = 'EVENT', + activeTab, + onActiveTabChange, onCreateSession, }: CreateScheduleModalProps) { - const [activeTab, setActiveTab] = useState(initialTab); - - const handleClose = () => { - onOpenChange(false); - setActiveTab(initialTab); - }; - - const handleDialogOpenChange = (nextOpen: boolean) => { - onOpenChange(nextOpen); - if (!nextOpen) setActiveTab(initialTab); - }; + const handleClose = () => onOpenChange(false); return ( - + {/* Header with tabs */}
setActiveTab(v as ScheduleType)} + onValueChange={(v) => onActiveTabChange(v as ScheduleType)} className="gap-0" > From 2eac303bc0f317259f8314e1ca9af07931288864 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Fri, 24 Apr 2026 00:01:37 +0900 Subject: [PATCH 09/16] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=97=90=EB=9F=AC=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/layout/Header.tsx | 9 +++++++-- .../admin/schedule/general/ScheduleFormField.tsx | 2 +- src/components/layout/header/MobileNavSheet.tsx | 12 +++++++----- src/components/mypage/MyPageDropdownMenu.tsx | 7 +++---- src/components/ui/DateTimeInput.tsx | 2 +- src/hooks/index.ts | 1 + src/hooks/useLogout.ts | 11 +++++++++++ 7 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 src/hooks/useLogout.ts diff --git a/src/components/admin/layout/Header.tsx b/src/components/admin/layout/Header.tsx index 31cfd31c..418b6c6c 100644 --- a/src/components/admin/layout/Header.tsx +++ b/src/components/admin/layout/Header.tsx @@ -33,8 +33,12 @@ // // '기수 시작시 이월된 회비와 해당 기수 회비를 종합해 회비를 등록해주시기 바랍니다. 회비 등록은 기수당 한 번만 가능합니다.', // // }, // }; -import { logoutAction } from '@/lib/actions/auth'; +'use client'; + +import { useLogout } from '@/hooks'; + export function Header() { + const handleLogout = useLogout(); // const pathname = usePathname(); // const metadata = Object.entries(PAGE_METADATA).find(([path]) => pathname.startsWith(path))?.[1]; @@ -52,7 +56,8 @@ export function Header() { )} */} - +
); diff --git a/src/components/mypage/MyPageDropdownMenu.tsx b/src/components/mypage/MyPageDropdownMenu.tsx index d5689859..5017a49f 100644 --- a/src/components/mypage/MyPageDropdownMenu.tsx +++ b/src/components/mypage/MyPageDropdownMenu.tsx @@ -15,12 +15,13 @@ import { Icon, } from '@/components/ui'; import { AdminMeatballIcon } from '@/assets/icons/admin'; -import { logoutAction } from '@/lib/actions/auth'; +import { useLogout } from '@/hooks'; function MyPageDropdownMenu() { const { clubId } = useParams<{ clubId: string }>(); // const [withdrawOpen, setWithdrawOpen] = useState(false); const [logoutOpen, setLogoutOpen] = useState(false); + const handleLogout = useLogout(); return ( <> @@ -65,9 +66,7 @@ function MyPageDropdownMenu() { title={'로그아웃'} description="로그아웃 하시겠습니까?" > -
- 로그아웃 -
+ 로그아웃 취소 diff --git a/src/components/ui/DateTimeInput.tsx b/src/components/ui/DateTimeInput.tsx index ad38d0bf..a05f7c7a 100644 --- a/src/components/ui/DateTimeInput.tsx +++ b/src/components/ui/DateTimeInput.tsx @@ -24,7 +24,7 @@ function DateTimeInput({ return (
- {label} + {label}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts index c88c5b21..86807583 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,4 +1,5 @@ // hooks index file +export { useLogout } from './useLogout'; export { useAutoScrollIntoView } from './useAutoScrollIntoView'; export { useClickOutside } from './useClickOutside'; export { useDragScroll } from './useDragScroll'; diff --git a/src/hooks/useLogout.ts b/src/hooks/useLogout.ts new file mode 100644 index 00000000..447fd2f9 --- /dev/null +++ b/src/hooks/useLogout.ts @@ -0,0 +1,11 @@ +import { logoutAction } from '@/lib/actions/auth'; +import { useClubActions } from '@/stores/useClubStore'; + +export function useLogout() { + const { reset } = useClubActions(); + + return async () => { + reset(); + await logoutAction(); + }; +} From 86160c4375170cf71491ef15aef046bdeb8b8391 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Fri, 24 Apr 2026 09:59:19 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/index.ts | 1 + .../schedule/general/SchedulePageContent.tsx | 7 ++-- .../modal/CreateGeneralScheduleForm.tsx | 2 +- .../modal/CreateSessionScheduleForm.tsx | 6 ++- .../schedule/modal/EditScheduleModal.tsx | 21 +++-------- .../admin/schedule/modal/EditSessionModal.tsx | 37 ++++++++----------- .../admin/schedule/modal/ScheduleFormBody.tsx | 2 +- .../schedule/modal/SessionScheduleForm.tsx | 18 ++++----- 8 files changed, 38 insertions(+), 56 deletions(-) diff --git a/src/components/admin/index.ts b/src/components/admin/index.ts index 300e2007..70f3b87d 100644 --- a/src/components/admin/index.ts +++ b/src/components/admin/index.ts @@ -1,5 +1,6 @@ // admin components index file export { CardinalDropdown, type CardinalDropdownProps } from './CardinalDropdown'; +export { ModalIconButton, type ModalIconButtonProps } from './modal/ModalIconButton'; export { AttendanceCard, type AttendanceCardProps } from './attendance/AttendanceCard'; export { AttendancePageContent } from './attendance/AttendancePageContent'; export { AdminInfoCard, type AdminInfoCardProps } from './club-info/AdminInfoCard'; diff --git a/src/components/admin/schedule/general/SchedulePageContent.tsx b/src/components/admin/schedule/general/SchedulePageContent.tsx index 94264362..9497434f 100644 --- a/src/components/admin/schedule/general/SchedulePageContent.tsx +++ b/src/components/admin/schedule/general/SchedulePageContent.tsx @@ -3,8 +3,7 @@ import { useState } from 'react'; import Image from 'next/image'; -import { Button, Card, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'; -import { Icon } from '@/components/ui'; +import { Button, Card, Icon, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'; import { AdminCalendarEditIcon } from '@/assets/icons/admin'; import { SearchIcon } from '@/assets/icons'; import { CardinalDropdown } from '@/components/admin'; @@ -109,7 +108,7 @@ function SchedulePageContent() { - + {/* Month navigator */} -
+
검색 -
+

일정 생성

0 && + isScheduleTitleValid(form.title) && selectedCardinal != null && isDateRangeValid(form) && isRecurrenceEndValid; @@ -102,7 +104,7 @@ function CreateSessionScheduleForm({ onCreateSession, onClose }: CreateSessionSc return ( <> -
+

세션 생성

- + setDeleteConfirmOpen(true)}> @@ -143,14 +137,11 @@ function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditSched onConfirm={handleDiscardConfirm} placement="below-right" > - + />
diff --git a/src/components/admin/schedule/modal/EditSessionModal.tsx b/src/components/admin/schedule/modal/EditSessionModal.tsx index d8250e06..12fa0ad2 100644 --- a/src/components/admin/schedule/modal/EditSessionModal.tsx +++ b/src/components/admin/schedule/modal/EditSessionModal.tsx @@ -8,20 +8,21 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - Icon, } from '@/components/ui'; import { CustomAlertDialog } from '@/components/alert'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { AdminCloseIcon, AdminMeatballIcon } from '@/assets/icons/admin'; +import { ModalIconButton } from '@/components/admin'; import type { AdminSession, AdminSessionGroup } from '@/types/admin/session'; - -import { DiscardConfirmArea } from './DiscardConfirmArea'; -import { ScheduleFormBody } from './ScheduleFormBody'; import { isFormChanged, + isScheduleTitleValid, isSessionGroup, toInitialSessionForm, -} from '../../../../utils/admin/scheduleFormUtils'; +} from '@/utils/admin/scheduleFormUtils'; + +import { DiscardConfirmArea } from './DiscardConfirmArea'; +import { ScheduleFormBody } from './ScheduleFormBody'; import type { ScheduleFormState, SessionDeleteType, SessionSaveType } from './types'; interface EditSessionModalProps { @@ -57,7 +58,7 @@ function EditSessionModal({ open, onOpenChange, target, onDelete, onSave }: Edit }; const handleSubmit = () => { - if (!form.title.trim()) return; + if (!isScheduleTitleValid(form.title)) return; if (isRecurring) { setSaveConfirmOpen(true); } else { @@ -96,7 +97,7 @@ function EditSessionModal({ open, onOpenChange, target, onDelete, onSave }: Edit }} > { if (hasChanges) e.preventDefault(); @@ -112,12 +113,7 @@ function EditSessionModal({ open, onOpenChange, target, onDelete, onSave }: Edit
- + setDeleteConfirmOpen(true)}> @@ -132,20 +128,17 @@ function EditSessionModal({ open, onOpenChange, target, onDelete, onSave }: Edit onConfirm={handleDiscardConfirm} placement="below-right" > - + />
{/* Body */} -
+

세션 수정

저장 diff --git a/src/components/admin/schedule/modal/ScheduleFormBody.tsx b/src/components/admin/schedule/modal/ScheduleFormBody.tsx index af6d43d2..861d9ea0 100644 --- a/src/components/admin/schedule/modal/ScheduleFormBody.tsx +++ b/src/components/admin/schedule/modal/ScheduleFormBody.tsx @@ -1,6 +1,6 @@ +import { DateTimeInput } from '@/components/ui'; import { ScheduleTextField } from '@/components/admin/schedule/general/ScheduleTextField'; import { ScheduleTextareaField } from '@/components/admin/schedule/general/ScheduleTextareaField'; -import { DateTimeInput } from '@/components/ui/DateTimeInput'; import { SCHEDULE_FIELD_LIMITS } from '@/utils/admin/scheduleFormUtils'; import { isDateRangeValid } from './types'; diff --git a/src/components/admin/schedule/modal/SessionScheduleForm.tsx b/src/components/admin/schedule/modal/SessionScheduleForm.tsx index 482b7df4..fe5b04e5 100644 --- a/src/components/admin/schedule/modal/SessionScheduleForm.tsx +++ b/src/components/admin/schedule/modal/SessionScheduleForm.tsx @@ -3,13 +3,13 @@ import Image from 'next/image'; import { + CalendarPicker, + DateTimeInput, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui'; -import { CalendarPicker } from '@/components/ui/CalendarPicker'; -import { DateTimeInput } from '@/components/ui/DateTimeInput'; import { ArrowDownIcon } from '@/assets/icons'; import { ScheduleFormField } from '@/components/admin/schedule/general/ScheduleFormField'; import { ScheduleTextField } from '@/components/admin/schedule/general/ScheduleTextField'; @@ -19,15 +19,11 @@ import { SESSION_RECURRENCE_OPTIONS, } from '@/constants/admin/session.constants'; import { addYearsToDateInput } from '@/utils/shared/date'; +import type { Cardinal } from '@/types/admin/cardinal'; import type { ScheduleFormState, SessionFormState } from './types'; import SessionInfobanner from '../session/SessionInfoBanner'; -interface Cardinal { - id: number; - cardinalNumber: number; -} - interface SessionScheduleFormProps { form: ScheduleFormState; onFormChange: (patch: Partial) => void; @@ -71,7 +67,7 @@ function SessionScheduleForm({ - + {cardinals.length === 0 ? ( 기수 없음 ) : ( @@ -120,7 +116,7 @@ function SessionScheduleForm({ - + {SESSION_RECURRENCE_OPTIONS.map((type) => ( Date: Fri, 24 Apr 2026 10:01:40 +0900 Subject: [PATCH 11/16] =?UTF-8?q?style:=20Prettier=20=ED=8F=AC=EB=A7=B7=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/components/admin/schedule/modal/CreateScheduleModal.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/admin/schedule/modal/CreateScheduleModal.tsx b/src/components/admin/schedule/modal/CreateScheduleModal.tsx index 8842bf26..7ce0c2e4 100644 --- a/src/components/admin/schedule/modal/CreateScheduleModal.tsx +++ b/src/components/admin/schedule/modal/CreateScheduleModal.tsx @@ -1,7 +1,5 @@ 'use client'; - - import { Icon, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { AdminCloseIcon } from '@/assets/icons/admin'; From fa3675b52418f71c0d3b0994cd916eb2adfa5fba Mon Sep 17 00:00:00 2001 From: JIN921 Date: Sun, 26 Apr 2026 01:50:50 +0900 Subject: [PATCH 12/16] =?UTF-8?q?fix:=20sub3=20weight=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/globals.css b/src/app/globals.css index cdfb9460..0e761cfa 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -128,6 +128,7 @@ --sub2-weight: var(--font-weight-black); --sub3-size: 16px; --sub3-line-height: 20px; + --sub3-weight: var(--font-weight-semibold); --body1-size: 16px; --body1-line-height: 24px; --body1-weight: 470; @@ -212,7 +213,7 @@ --sub2-size: 14px; --sub2-line-height: 18px; --sub3-size: 14px; - --sub3-weight: var(--font-weight-semibold); + --font-weight: var(--sub3-weight); --sub3-line-height: 18px; --body1-size: 14px; --body1-line-height: 22px; From f800c0d4e4f4776edd05a07d50d32264fd2559e4 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Sun, 26 Apr 2026 02:06:16 +0900 Subject: [PATCH 13/16] =?UTF-8?q?fix:=20=EC=88=98=EC=A0=95=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=8B=AB=ED=9E=88=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 --- .../schedule/general/SchedulePageContent.tsx | 29 +++++------------- src/hooks/index.ts | 1 + .../queries/admin/useAdminScheduleQueries.ts | 8 +++-- src/hooks/useMonthNavigator.ts | 30 +++++++++++++++++++ 4 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 src/hooks/useMonthNavigator.ts diff --git a/src/components/admin/schedule/general/SchedulePageContent.tsx b/src/components/admin/schedule/general/SchedulePageContent.tsx index 9497434f..1f664839 100644 --- a/src/components/admin/schedule/general/SchedulePageContent.tsx +++ b/src/components/admin/schedule/general/SchedulePageContent.tsx @@ -13,7 +13,7 @@ import { SessionTabContent } from '@/components/admin/schedule/session/SessionTa import { CreateScheduleModal } from '@/components/admin/schedule/modal/CreateScheduleModal'; import { EditScheduleModal } from '@/components/admin/schedule/modal/EditScheduleModal'; import { EditSessionModal } from '@/components/admin/schedule/modal/EditSessionModal'; -import { useCardinalSelector } from '@/hooks'; +import { useCardinalSelector, useMonthNavigator } from '@/hooks'; import { useAdminMonthlySchedules, useDeleteSchedule, @@ -25,8 +25,8 @@ type ScheduleTab = 'all' | 'session'; function SchedulePageContent() { const { cardinals, selectedCardinalId, setSelectedCardinalId, activeCardinal } = useCardinalSelector({ autoSelectLatest: true }); - const [currentYear, setCurrentYear] = useState(() => new Date().getFullYear()); - const [currentMonth, setCurrentMonth] = useState(() => new Date().getMonth() + 1); + const { year: currentYear, month: currentMonth, prev: handlePrevMonth, next: handleNextMonth } = + useMonthNavigator(); const [searchValue, setSearchValue] = useState(''); const [activeTab, setActiveTab] = useState('all'); const [createModalOpen, setCreateModalOpen] = useState(false); @@ -66,26 +66,11 @@ function SchedulePageContent() { (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime(), ); - const handlePrevMonth = () => { - if (currentMonth === 1) { - setCurrentYear((y) => y - 1); - setCurrentMonth(12); - } else { - setCurrentMonth((m) => m - 1); - } - }; - - const handleNextMonth = () => { - if (currentMonth === 12) { - setCurrentYear((y) => y + 1); - setCurrentMonth(1); - } else { - setCurrentMonth((m) => m + 1); - } - }; - const handleDelete = (schedule: Schedule) => { - deleteSchedule(schedule.id); + deleteSchedule(schedule.id, { + onSuccess: () => setEditTarget(null), + // onError 처리도 토스트 유틸과 연결하는 것을 권장합니다. + }); }; return ( diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 86807583..f1c71f2e 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -21,3 +21,4 @@ export { useCardinalSelector } from './useCardinalSelector'; export { useImageDrop } from './useImageDrop'; export { useProgressAnimation } from './useProgressAnimation'; export { useCodeHighlight } from './useCodeHighlight'; +export { useMonthNavigator } from './useMonthNavigator'; diff --git a/src/hooks/queries/admin/useAdminScheduleQueries.ts b/src/hooks/queries/admin/useAdminScheduleQueries.ts index 1c113bed..5c2a2022 100644 --- a/src/hooks/queries/admin/useAdminScheduleQueries.ts +++ b/src/hooks/queries/admin/useAdminScheduleQueries.ts @@ -6,6 +6,7 @@ import { adminScheduleApi } from '@/lib/apis/adminSchedule'; import { useClubId } from '@/stores'; import { toastError } from '@/stores/useToastStore'; import type { CreateEventBody, UpdateEventBody } from '@/types/admin/schedule'; +import { MutationCallbacks } from '@/types'; function toMonthRange(year: number, month: number) { const pad = (n: number) => String(n).padStart(2, '0'); @@ -63,14 +64,17 @@ export function useUpdateSchedule() { }); } -export function useDeleteSchedule() { +export function useDeleteSchedule(callback?: MutationCallbacks) { const clubId = useClubId(); const queryClient = useQueryClient(); return useMutation({ mutationFn: (eventId: number) => adminScheduleApi.deleteEvent(clubId!, eventId), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['admin', 'schedules'] }); + if (callback?.onSuccess) { + callback.onSuccess(); + queryClient.invalidateQueries({ queryKey: ['admin', 'schedules'] }); + } }, onError: (error) => { const code = isAxiosError(error) ? error.response?.data?.code : undefined; diff --git a/src/hooks/useMonthNavigator.ts b/src/hooks/useMonthNavigator.ts new file mode 100644 index 00000000..82ca73dc --- /dev/null +++ b/src/hooks/useMonthNavigator.ts @@ -0,0 +1,30 @@ +'use client'; + +import { useState } from 'react'; + +function useMonthNavigator() { + const [year, setYear] = useState(() => new Date().getFullYear()); + const [month, setMonth] = useState(() => new Date().getMonth() + 1); + + const prev = () => { + if (month === 1) { + setYear((y) => y - 1); + setMonth(12); + } else { + setMonth((m) => m - 1); + } + }; + + const next = () => { + if (month === 12) { + setYear((y) => y + 1); + setMonth(1); + } else { + setMonth((m) => m + 1); + } + }; + + return { year, month, prev, next }; +} + +export { useMonthNavigator }; From cf0b750d4abf838d29a9afe6735edd4f6b0a271f Mon Sep 17 00:00:00 2001 From: JIN921 Date: Sun, 26 Apr 2026 03:00:15 +0900 Subject: [PATCH 14/16] =?UTF-8?q?fix:=20=EC=9D=BC=EB=B0=98=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=84=A4=EB=AA=85=20api=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/modal/EditScheduleModal.tsx | 259 +++++++++++------- .../queries/admin/useAdminScheduleQueries.ts | 17 +- src/lib/apis/adminSchedule.ts | 9 +- src/types/admin/schedule.d.ts | 7 + src/utils/admin/scheduleFormUtils.ts | 18 +- 5 files changed, 204 insertions(+), 106 deletions(-) diff --git a/src/components/admin/schedule/modal/EditScheduleModal.tsx b/src/components/admin/schedule/modal/EditScheduleModal.tsx index 1bb66842..3016952e 100644 --- a/src/components/admin/schedule/modal/EditScheduleModal.tsx +++ b/src/components/admin/schedule/modal/EditScheduleModal.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState } from 'react'; +import { Suspense, useEffect, useRef, useState } from 'react'; +import type { RefObject } from 'react'; import { Button, @@ -14,7 +15,10 @@ import { Dialog, DialogContent } from '@/components/ui/dialog'; import { AdminCloseIcon, AdminMeatballIcon } from '@/assets/icons/admin'; import { ModalIconButton } from '@/components/admin'; import type { Schedule } from '@/types/admin/schedule'; -import { useUpdateSchedule } from '@/hooks/queries/admin/useAdminScheduleQueries'; +import { + useAdminScheduleDetail, + useUpdateSchedule, +} from '@/hooks/queries/admin/useAdminScheduleQueries'; import { isFormChanged, isScheduleContentValid, @@ -36,7 +40,80 @@ interface EditScheduleModalProps { } function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditScheduleModalProps) { - const initialForm = toInitialScheduleForm(schedule); + const hasChangesRef = useRef(false); + const requestCloseRef = useRef<(() => void) | null>(null); + + const handleClose = () => onOpenChange(false); + + return ( + { + if (nextOpen) return; + if (requestCloseRef.current) requestCloseRef.current(); + else handleClose(); + }} + > + { + if (hasChangesRef.current) e.preventDefault(); + }} + > + }> + + + + + ); +} + +function EditScheduleModalLoading({ onClose }: { onClose: () => void }) { + return ( + <> +
+
+ + 일반 일정 + +
+ +
+
+

일반 일정 수정

+
+

불러오는 중...

+
+
+ + ); +} + +interface EditScheduleModalContentProps { + scheduleId: number; + onClose: () => void; + onDelete?: (schedule: Schedule) => void; + hasChangesRef: RefObject; + requestCloseRef: RefObject<(() => void) | null>; +} + +function EditScheduleModalContent({ + scheduleId, + onClose, + onDelete, + hasChangesRef, + requestCloseRef, +}: EditScheduleModalContentProps) { + const { data: detail } = useAdminScheduleDetail(scheduleId); + + const [initialForm] = useState(() => toInitialScheduleForm(detail)); const [form, setForm] = useState(initialForm); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [discardSource, setDiscardSource] = useState<'close' | 'cancel' | null>(null); @@ -55,21 +132,28 @@ function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditSched setForm((prev) => ({ ...prev, ...patch })); }; - const handleClose = () => onOpenChange(false); - const handleTryClose = (source: 'close' | 'cancel') => { - if (hasChanges) { - setDiscardSource(source); - } else { - handleClose(); - } + if (hasChanges) setDiscardSource(source); + else onClose(); }; + useEffect(() => { + hasChangesRef.current = hasChanges; + requestCloseRef.current = () => { + if (hasChanges) setDiscardSource('close'); + else onClose(); + }; + return () => { + hasChangesRef.current = false; + requestCloseRef.current = null; + }; + }, [hasChanges, hasChangesRef, onClose, requestCloseRef]); + const handleSubmit = () => { if (!isValid) return; mutate( { - eventId: schedule.id, + eventId: scheduleId, body: { title: form.title, content: form.content, @@ -78,19 +162,19 @@ function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditSched end: `${form.endDate}T${form.endTime}:00`, }, }, - { onSuccess: handleClose }, + { onSuccess: onClose }, ); }; const handleDeleteConfirm = () => { setDeleteConfirmOpen(false); - handleClose(); - onDelete?.(schedule); + onClose(); + onDelete?.(detail); }; const handleDiscardConfirm = () => { setDiscardSource(null); - handleClose(); + onClose(); }; const closeDiscardAlert = (source: 'close' | 'cancel') => (next: boolean) => { @@ -99,87 +183,72 @@ function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditSched return ( <> - { - if (!nextOpen) handleTryClose('close'); - }} - > - { - if (hasChanges) e.preventDefault(); - }} - > - {/* Header */} -
-
- - 일반 일정 - -
-
- - - - - - setDeleteConfirmOpen(true)}> - 일반 일정 삭제 - - - - - - handleTryClose('close')} - /> - -
-
- - {/* Body */} -
-

일반 일정 수정

- +
+ + 일반 일정 + +
+
+ + + + + + setDeleteConfirmOpen(true)}> + 일반 일정 삭제 + + + + + + handleTryClose('close')} /> -
- - {/* Footer */} -
- - - - -
- -
+ +
+
+ + {/* Body */} +
+

일반 일정 수정

+ +
+ + {/* Footer */} +
+ + + + +
{/* 삭제 확인 */} adminScheduleApi.getEventDetail(clubId!, eventId).then((res) => res.data.data), + staleTime: 5 * 60 * 1000, + }); +} + export function useCreateSchedule() { const clubId = useClubId(); const queryClient = useQueryClient(); diff --git a/src/lib/apis/adminSchedule.ts b/src/lib/apis/adminSchedule.ts index 58f7c09d..7f41fd01 100644 --- a/src/lib/apis/adminSchedule.ts +++ b/src/lib/apis/adminSchedule.ts @@ -1,12 +1,19 @@ import { apiClient } from '@/lib/apis/client'; import type { ApiResponse } from '@/types/common'; -import type { CreateEventBody, UpdateEventBody, Schedule } from '@/types/admin/schedule'; +import type { + CreateEventBody, + Schedule, + ScheduleDetail, + UpdateEventBody, +} from '@/types/admin/schedule'; export const adminScheduleApi = { getMonthly: (clubId: string, start: string, end: string) => apiClient.get>(`/clubs/${clubId}/schedules/monthly`, { params: { start, end }, }), + getEventDetail: (clubId: string, eventId: number) => + apiClient.get>(`/clubs/${clubId}/events/${eventId}`), createEvent: (clubId: string, body: CreateEventBody) => apiClient.post>(`/admin/clubs/${clubId}/events`, body), updateEvent: (clubId: string, eventId: number, body: UpdateEventBody) => diff --git a/src/types/admin/schedule.d.ts b/src/types/admin/schedule.d.ts index bffeda5d..43f627cd 100644 --- a/src/types/admin/schedule.d.ts +++ b/src/types/admin/schedule.d.ts @@ -10,6 +10,13 @@ export interface Schedule { cardinal: number; } +export interface ScheduleDetail extends Schedule { + content: string; + name: string; + createdAt: string; + modifiedAt: string; +} + export interface CreateEventBody { title: string; content: string; diff --git a/src/utils/admin/scheduleFormUtils.ts b/src/utils/admin/scheduleFormUtils.ts index 4d8b7c8a..7cfd635c 100644 --- a/src/utils/admin/scheduleFormUtils.ts +++ b/src/utils/admin/scheduleFormUtils.ts @@ -1,4 +1,4 @@ -import type { Schedule } from '@/types/admin/schedule'; +import type { ScheduleDetail } from '@/types/admin/schedule'; import type { AdminSession, AdminSessionGroup } from '@/types/admin/session'; import type { ScheduleFormState } from '../../components/admin/schedule/modal/types'; @@ -50,15 +50,15 @@ export function toInitialSessionForm(target: AdminSession | AdminSessionGroup): }; } -export function toInitialScheduleForm(schedule: Schedule): ScheduleFormState { +export function toInitialScheduleForm(detail: ScheduleDetail): ScheduleFormState { return { - title: schedule.title, - startDate: schedule.start.slice(0, 10), - startTime: schedule.start.slice(11, 16), - endDate: schedule.end.slice(0, 10), - endTime: schedule.end.slice(11, 16), - location: schedule.location, - content: '', + title: detail.title, + startDate: detail.start.slice(0, 10), + startTime: detail.start.slice(11, 16), + endDate: detail.end.slice(0, 10), + endTime: detail.end.slice(11, 16), + location: detail.location, + content: detail.content, }; } From f8d8b604881a239ff968a8a8d367bf96b7f6d1da Mon Sep 17 00:00:00 2001 From: JIN921 Date: Sun, 26 Apr 2026 03:18:43 +0900 Subject: [PATCH 15/16] =?UTF-8?q?style:=20Prettier=20=ED=8F=AC=EB=A7=B7=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/schedule/modal/EditScheduleModal.tsx | 7 +------ src/hooks/queries/admin/useAdminScheduleQueries.ts | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/admin/schedule/modal/EditScheduleModal.tsx b/src/components/admin/schedule/modal/EditScheduleModal.tsx index 3016952e..67a8382d 100644 --- a/src/components/admin/schedule/modal/EditScheduleModal.tsx +++ b/src/components/admin/schedule/modal/EditScheduleModal.tsx @@ -240,12 +240,7 @@ function EditScheduleModalContent({ 취소 -
diff --git a/src/hooks/queries/admin/useAdminScheduleQueries.ts b/src/hooks/queries/admin/useAdminScheduleQueries.ts index 2d375fc1..1304335f 100644 --- a/src/hooks/queries/admin/useAdminScheduleQueries.ts +++ b/src/hooks/queries/admin/useAdminScheduleQueries.ts @@ -1,9 +1,4 @@ -import { - useMutation, - useQuery, - useQueryClient, - useSuspenseQuery, -} from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; import { SCHEDULE_ERROR_MESSAGE } from '@/constants/admin/schedule.constants'; From 6b2e877681983d443a609abda7d42d8fc2ace672 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Sun, 26 Apr 2026 13:25:48 +0900 Subject: [PATCH 16/16] =?UTF-8?q?style:=20Prettier=20=ED=8F=AC=EB=A7=B7=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/schedule/general/SchedulePageContent.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/admin/schedule/general/SchedulePageContent.tsx b/src/components/admin/schedule/general/SchedulePageContent.tsx index 1f664839..6f7a650d 100644 --- a/src/components/admin/schedule/general/SchedulePageContent.tsx +++ b/src/components/admin/schedule/general/SchedulePageContent.tsx @@ -25,8 +25,12 @@ type ScheduleTab = 'all' | 'session'; function SchedulePageContent() { const { cardinals, selectedCardinalId, setSelectedCardinalId, activeCardinal } = useCardinalSelector({ autoSelectLatest: true }); - const { year: currentYear, month: currentMonth, prev: handlePrevMonth, next: handleNextMonth } = - useMonthNavigator(); + const { + year: currentYear, + month: currentMonth, + prev: handlePrevMonth, + next: handleNextMonth, + } = useMonthNavigator(); const [searchValue, setSearchValue] = useState(''); const [activeTab, setActiveTab] = useState('all'); const [createModalOpen, setCreateModalOpen] = useState(false);