diff --git a/apps/cs/app/(with-base-layout)/schedule/register/[schdlSn]/page.tsx b/apps/cs/app/(with-base-layout)/schedule/register/[schdlSn]/page.tsx new file mode 100644 index 000000000..90c4eb133 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/schedule/register/[schdlSn]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/schedule"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ schdlSn: string }>; +}) { + const { schdlSn } = await params; + return ; +} diff --git a/apps/cs/src/pages/schedule/api/schedule-service.ts b/apps/cs/src/pages/schedule/api/schedule-service.ts index e69878daf..b333b43f4 100644 --- a/apps/cs/src/pages/schedule/api/schedule-service.ts +++ b/apps/cs/src/pages/schedule/api/schedule-service.ts @@ -5,6 +5,7 @@ import type { ScheduleFormData, ScheduleParams, ScheduleService, + ScheduleUpdateFormData, } from "../model/schedule-interface"; class ScheduleApiService implements ScheduleService { @@ -16,9 +17,23 @@ class ScheduleApiService implements ScheduleService { return response.data; } + public async getScheduleInfo(schdlSn: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.SCHEDULES}/${schdlSn}`, + ); + return response.data; + } + public async createSchedule(data: ScheduleFormData): Promise { await client.post(ENDPOINT.CMS_SERVICE.SCHEDULES, data); } + + public async updateSchedule( + schdlSn: number, + data: ScheduleUpdateFormData, + ): Promise { + await client.put(`${ENDPOINT.CMS_SERVICE.SCHEDULES}/${schdlSn}`, data); + } } export const ScheduleApi = new ScheduleApiService(); diff --git a/apps/cs/src/pages/schedule/index.ts b/apps/cs/src/pages/schedule/index.ts index 7495f12e3..07ad3f59a 100644 --- a/apps/cs/src/pages/schedule/index.ts +++ b/apps/cs/src/pages/schedule/index.ts @@ -1,2 +1,3 @@ export { SchedulePage } from "./ui/schedule-page"; export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; diff --git a/apps/cs/src/pages/schedule/lib/utils.ts b/apps/cs/src/pages/schedule/lib/utils.ts new file mode 100644 index 000000000..22aadacc4 --- /dev/null +++ b/apps/cs/src/pages/schedule/lib/utils.ts @@ -0,0 +1,67 @@ +import dayjs from "dayjs"; +import type { + RepeatDto, + ScheduleFormData, + ScheduleUpdateFormData, +} from "../model/schedule-interface"; + +const DAY_BY_BITMASK = { + 0: 1, // 일요일 + 1: 2, // 월요일 + 2: 4, // 화요일 + 3: 8, // 수요일 + 4: 16, // 목요일 + 5: 32, // 금요일 + 6: 64, // 토요일 +} as const; + +export const generateRepeatDto = ( + data: ScheduleFormData | ScheduleUpdateFormData, +) => { + const reptDto: RepeatDto = { ...data.reptDto }; + + switch (data.reptDto?.reptCycleCd) { + case "DAILY": + break; + case "WEEKLY": + const beginDay = dayjs(data.schdlBgngDt).day(); + const endDay = dayjs(data.schdlEndDt).day(); + + reptDto.reptDowVl = + beginDay === endDay + ? DAY_BY_BITMASK[beginDay] + : calculateDaysBitmask(beginDay, endDay); + break; + case "MONTHLY": + reptDto.reptDayVl = dayjs(data.schdlBgngDt).date(); + break; + case "QUARTERLY": + const month = dayjs(data.schdlBgngDt).month(); + reptDto.reptQuVl = Math.floor(month / 3) + 1; + reptDto.reptDayVl = dayjs(data.schdlBgngDt).date(); + break; + case "YEARLY": + reptDto.reptMmVl = dayjs(data.schdlBgngDt).month() + 1; + reptDto.reptDayVl = dayjs(data.schdlBgngDt).date(); + break; + default: + } + reptDto.reptCycleCd = data.reptDto?.reptCycleCd; + + return reptDto; +}; + +const calculateDaysBitmask = (startDay: number, endDay: number) => { + const length = ((endDay - startDay + 7) % 7) + 1; + return Array.from({ length }, (_, i) => { + const day = ((startDay + i) % 7) as keyof typeof DAY_BY_BITMASK; + return DAY_BY_BITMASK[day]; + }).reduce((acc, val) => acc + val, 0); +}; + +export const calculateEventDuration = (startDate: string, endDate: string) => { + const start = dayjs(startDate); + const end = dayjs(endDate); + const minuteDiff = end.diff(start, "minute"); + return minuteDiff; +}; diff --git a/apps/cs/src/pages/schedule/model/schedule-interface.ts b/apps/cs/src/pages/schedule/model/schedule-interface.ts index 8064d9f87..98b6a703a 100644 --- a/apps/cs/src/pages/schedule/model/schedule-interface.ts +++ b/apps/cs/src/pages/schedule/model/schedule-interface.ts @@ -1,5 +1,16 @@ import { z } from "zod"; +export const REPEAT_CYCLE_CODES = { + DAILY: "DAILY", + WEEKLY: "WEEKLY", + MONTHLY: "MONTHLY", + QUARTERLY: "QUARTERLY", + YEARLY: "YEARLY", +} as const; + +export type RepeatCycleCode = + (typeof REPEAT_CYCLE_CODES)[keyof typeof REPEAT_CYCLE_CODES]; + export interface ScheduleParams { siteId?: string; schdlTtl?: string; @@ -25,7 +36,7 @@ export interface Schedule { const scheduleRepeatSchema = z.object({ schdlSn: z.number().nullish(), - reptCycleCd: z.string().nullish(), + reptCycleCd: z.nativeEnum(REPEAT_CYCLE_CODES).nullish(), reptDayVl: z.number().nullish(), reptDowVl: z.number().nullish(), reptMmVl: z.number().nullish(), @@ -47,9 +58,17 @@ export const scheduleFormSchema = z.object({ reptDto: scheduleRepeatSchema.nullish(), }); +export const scheduleUpdateFormSchema = scheduleFormSchema.omit({ + siteId: true, +}); + export type ScheduleFormData = z.infer; +export type ScheduleUpdateFormData = z.infer; +export type RepeatDto = z.infer; export interface ScheduleService { getSchedules(params?: ScheduleParams): Promise; + getScheduleInfo(schdlSn: number): Promise; createSchedule(data: ScheduleFormData): Promise; + updateSchedule(schdlSn: number, data: ScheduleUpdateFormData): Promise; } diff --git a/apps/cs/src/pages/schedule/ui/create-page.tsx b/apps/cs/src/pages/schedule/ui/create-page.tsx index 579689354..f8702424f 100644 --- a/apps/cs/src/pages/schedule/ui/create-page.tsx +++ b/apps/cs/src/pages/schedule/ui/create-page.tsx @@ -11,6 +11,7 @@ import { scheduleFormSchema, type ScheduleFormData, } from "../model/schedule-interface"; +import { generateRepeatDto } from "../lib/utils"; import { ScheduleApi } from "../api/schedule-service"; import { ScheduleForm } from "./schedule-form"; @@ -46,11 +47,9 @@ function CreatePage() { }); const handleSave = async (data: ScheduleFormData) => { - // const reptDtoValid = Object.values(data.reptDto ?? {}).every( - // (value) => value === null, - // ); - // if (reptDtoValid) data.reptDto = null; - await createSchedule(data); + const reptDto = generateRepeatDto(data); + const formData = { ...data, reptDto }; + createSchedule(formData); }; return ( diff --git a/apps/cs/src/pages/schedule/ui/modify-page.tsx b/apps/cs/src/pages/schedule/ui/modify-page.tsx new file mode 100644 index 000000000..6d020d85c --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/modify-page.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useAlertStore } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; +import { + scheduleUpdateFormSchema, + type ScheduleUpdateFormData, +} from "../model/schedule-interface"; +import { ScheduleApi } from "../api/schedule-service"; +import { ScheduleForm } from "./schedule-form"; +import { generateRepeatDto } from "../lib/utils"; + +function ModifyPage({ schdlSn }: { schdlSn: string }) { + const router = useRouter(); + const queryClient = useQueryClient(); + const { setMessage: alert } = useAlertStore((state) => state); + + const { data: scheduleInfo } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SCHEDULES, schdlSn], + queryFn: () => ScheduleApi.getScheduleInfo(parseInt(schdlSn)), + }); + + const { mutateAsync: updateSchedule } = useMutation({ + mutationFn: (data: ScheduleUpdateFormData) => + ScheduleApi.updateSchedule(parseInt(schdlSn), data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.SCHEDULES], + }); + alert("일정이 수정되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(scheduleUpdateFormSchema), + values: { + ...scheduleInfo, + schdlTtl: scheduleInfo?.schdlTtl ?? "", + schdlSeCd: scheduleInfo?.schdlSeCd ?? "", + schdlBgngDt: scheduleInfo?.schdlBgngDt ?? "", + schdlEndDt: scheduleInfo?.schdlEndDt ?? "", + }, + }); + + const handleSave = async (data: ScheduleUpdateFormData) => { + const reptDto = generateRepeatDto(data); + const formData = { ...data, reptDto }; + updateSchedule(formData); + }; + + return ( +
+
+

스케줄 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/schedule/ui/schedule-form.tsx b/apps/cs/src/pages/schedule/ui/schedule-form.tsx index bf34b02d1..b8106627f 100644 --- a/apps/cs/src/pages/schedule/ui/schedule-form.tsx +++ b/apps/cs/src/pages/schedule/ui/schedule-form.tsx @@ -1,6 +1,6 @@ import dayjs from "dayjs"; import { useState } from "react"; -import { type UseFormReturn } from "react-hook-form"; +import { type Path, type PathValue, type UseFormReturn } from "react-hook-form"; import { Checkbox, DatePicker, @@ -19,34 +19,37 @@ import { GeneralFileUploader } from "@/widgets/post/ui/general-file-uploader"; import { FormWrapper } from "@/widgets/form-wrapper"; import { CodeSelect } from "@/entities/code"; import { FormItem, FormControl, FormField } from "@/shared/ui/form"; -import type { ScheduleFormData } from "../model/schedule-interface"; +import { + type ScheduleFormData, + type ScheduleUpdateFormData, +} from "../model/schedule-interface"; import { DateTimePicker } from "./date-time-picker"; -type ScheduleFormProps = { - form: UseFormReturn; +type ScheduleFormProps = { + form: UseFormReturn; scheduleId?: string; - handleSave: (data: ScheduleFormData) => Promise; + handleSave: (data: T) => Promise; handleDelete?: () => Promise; }; -function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { +function ScheduleForm({ + form, + scheduleId, + ...props +}: ScheduleFormProps) { const isModifyMode = !!scheduleId; - const [beginDate, setBeginDate] = useState( - dayjs(form?.getValues("schdlBgngDt"))?.toDate(), - ); - const [endDate, setEndDate] = useState( - dayjs(form?.getValues("schdlEndDt"))?.toDate(), - ); const [isAllDay, setIsAllDay] = useState(false); const [deleteAttachmentIds, setDeleteAttachmentIds] = useState([]); - const handleDateTimeChange = - (name: "schdlBgngDt" | "schdlEndDt") => (date?: Date) => { - if (!date) return; - form.setValue(name, dayjs(date).format("YYYY-MM-DD HH:mm:ss")); - setIsAllDay(false); - }; + const handleDateTimeChange = (name: Path) => (date?: Date) => { + if (!date) return; + form.setValue( + name, + dayjs(date).format("YYYY-MM-DD HH:mm:ss") as PathValue>, + ); + setIsAllDay(false); + }; return ( @@ -62,13 +65,13 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { } render={({ field }) => ( @@ -85,15 +88,18 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
} render={({ field }) => ( { - setBeginDate(date); - handleDateTimeChange("schdlBgngDt")(date); - }} + onDateChange={handleDateTimeChange( + "schdlBgngDt" as Path, + )} {...field} /> )} @@ -101,15 +107,18 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { ~ } render={({ field }) => ( { - setEndDate(date); - handleDateTimeChange("schdlEndDt")(date); - }} + onDateChange={handleDateTimeChange( + "schdlEndDt" as Path, + )} {...field} /> )} @@ -122,19 +131,26 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { onCheckedChange={(checked) => { setIsAllDay(checked === true); if (checked === true) { - const baseDate = form.getValues("schdlBgngDt"); - if (baseDate) { - const newStartDate = dayjs(baseDate).format( - "YYYY-MM-DD 00:00:00", - ); - const newEndDate = dayjs(baseDate).format( - "YYYY-MM-DD 23:59:59", - ); - setBeginDate(dayjs(newStartDate).toDate()); - setEndDate(dayjs(newEndDate).toDate()); - form.setValue("schdlBgngDt", newStartDate); - form.setValue("schdlEndDt", newEndDate); - } + const schdlBgngDt = form.getValues( + "schdlBgngDt" as Path, + ); + const schdlEndDt = form.getValues( + "schdlEndDt" as Path, + ); + const allSchdlBgngDt = dayjs( + schdlBgngDt as string, + ).format("YYYY-MM-DD 00:00:00"); + const allSchdlEndDt = dayjs( + schdlEndDt as string, + ).format("YYYY-MM-DD 23:59:59"); + form.setValue( + "schdlBgngDt" as Path, + allSchdlBgngDt as PathValue>, + ); + form.setValue( + "schdlEndDt" as Path, + allSchdlEndDt as PathValue>, + ); } }} /> @@ -150,7 +166,7 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
} render={({ field }) => ( @@ -170,19 +186,17 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { /> } render={({ field }) => ( - value && field.onChange(value) - } + value={(field.value as string) ?? ""} + onValueChange={field.onChange} placeholder="반복 주기 선택" - disabled={form.watch("reptYn") === "N"} + disabled={form.watch("reptYn" as Path) === "N"} /> @@ -190,7 +204,7 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { /> } render={({ field }) => ( @@ -198,15 +212,22 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { mode="single" selected={ field.value - ? dayjs(field.value).toDate() + ? dayjs(field.value as string).toDate() : undefined } - onSelect={(value) => - value && field.onChange(value) - } + onSelect={(value) => { + if (!value) return; + field.onChange(value); + form.setValue( + "reptDto.reptEndDt" as Path, + dayjs(value).format( + "YYYY-MM-DD 23:59:59", + ) as PathValue>, + ); + }} className="w-auto px-4" placeholder="반복 종료일 선택" - disabled={form.watch("reptYn") === "N"} + disabled={form.watch("reptYn" as Path) === "N"} /> @@ -221,14 +242,14 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { } render={({ field }) => ( @@ -242,13 +263,13 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { } render={({ field }) => (