From a9887eaac6f2ec6b882f62607cde8ddc9eac05e9 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 27 May 2025 13:23:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(with-base-layout)/schedule/page.tsx | 2 + .../schedule/register/page.tsx | 2 + apps/cs/package.json | 5 + .../pages/schedule/api/schedule-service.ts | 24 ++ apps/cs/src/pages/schedule/index.ts | 2 + .../schedule/model/schedule-interface.ts | 55 +++ apps/cs/src/pages/schedule/ui/create-page.tsx | 66 ++++ .../pages/schedule/ui/date-time-picker.tsx | 123 +++++++ .../src/pages/schedule/ui/schedule-form.tsx | 331 ++++++++++++++++++ .../src/pages/schedule/ui/schedule-page.tsx | 63 ++++ apps/cs/src/shared/config/api.ts | 1 + packages/common/components/package.json | 1 + .../components/src/ui/date-picker/index.tsx | 15 +- packages/common/components/src/ui/index.ts | 1 + .../components/src/ui/scroll-area/index.tsx | 48 +++ pnpm-lock.yaml | 56 +++ 16 files changed, 792 insertions(+), 3 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/schedule/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/schedule/register/page.tsx create mode 100644 apps/cs/src/pages/schedule/api/schedule-service.ts create mode 100644 apps/cs/src/pages/schedule/index.ts create mode 100644 apps/cs/src/pages/schedule/model/schedule-interface.ts create mode 100644 apps/cs/src/pages/schedule/ui/create-page.tsx create mode 100644 apps/cs/src/pages/schedule/ui/date-time-picker.tsx create mode 100644 apps/cs/src/pages/schedule/ui/schedule-form.tsx create mode 100644 apps/cs/src/pages/schedule/ui/schedule-page.tsx create mode 100644 packages/common/components/src/ui/scroll-area/index.tsx diff --git a/apps/cs/app/(with-base-layout)/schedule/page.tsx b/apps/cs/app/(with-base-layout)/schedule/page.tsx new file mode 100644 index 000000000..ec64bf14c --- /dev/null +++ b/apps/cs/app/(with-base-layout)/schedule/page.tsx @@ -0,0 +1,2 @@ +import { SchedulePage } from "@/pages/schedule"; +export default SchedulePage; diff --git a/apps/cs/app/(with-base-layout)/schedule/register/page.tsx b/apps/cs/app/(with-base-layout)/schedule/register/page.tsx new file mode 100644 index 000000000..7013caac7 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/schedule/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/schedule"; +export default CreatePage; diff --git a/apps/cs/package.json b/apps/cs/package.json index dce55ccd3..ee45de4a7 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -14,6 +14,10 @@ "@common/config": "workspace:*", "@fontsource-variable/noto-sans-kr": "^5.2.5", "@frontend-opensource/use-react-hooks": "^1.1.4", + "@fullcalendar/core": "^6.1.17", + "@fullcalendar/daygrid": "^6.1.17", + "@fullcalendar/interaction": "^6.1.17", + "@fullcalendar/react": "^6.1.17", "@hookform/resolvers": "^4.1.3", "@minoru/react-dnd-treeview": "3.5.1-alpha.1", "@radix-ui/react-icons": "^1.3.2", @@ -26,6 +30,7 @@ "chart.js": "^4.4.8", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", + "lucide-react": "^0.474.0", "next": "15.3.0", "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", diff --git a/apps/cs/src/pages/schedule/api/schedule-service.ts b/apps/cs/src/pages/schedule/api/schedule-service.ts new file mode 100644 index 000000000..e69878daf --- /dev/null +++ b/apps/cs/src/pages/schedule/api/schedule-service.ts @@ -0,0 +1,24 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + Schedule, + ScheduleFormData, + ScheduleParams, + ScheduleService, +} from "../model/schedule-interface"; + +class ScheduleApiService implements ScheduleService { + public async getSchedules(params?: ScheduleParams): Promise { + const response = await client.get( + ENDPOINT.CMS_SERVICE.SCHEDULES, + { params }, + ); + return response.data; + } + + public async createSchedule(data: ScheduleFormData): Promise { + await client.post(ENDPOINT.CMS_SERVICE.SCHEDULES, data); + } +} + +export const ScheduleApi = new ScheduleApiService(); diff --git a/apps/cs/src/pages/schedule/index.ts b/apps/cs/src/pages/schedule/index.ts new file mode 100644 index 000000000..7495f12e3 --- /dev/null +++ b/apps/cs/src/pages/schedule/index.ts @@ -0,0 +1,2 @@ +export { SchedulePage } from "./ui/schedule-page"; +export { CreatePage } from "./ui/create-page"; diff --git a/apps/cs/src/pages/schedule/model/schedule-interface.ts b/apps/cs/src/pages/schedule/model/schedule-interface.ts new file mode 100644 index 000000000..8064d9f87 --- /dev/null +++ b/apps/cs/src/pages/schedule/model/schedule-interface.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; + +export interface ScheduleParams { + siteId?: string; + schdlTtl?: string; + schdlSeCd?: string; + schdlBgngYmd?: string; + schdlEndYmd?: string; + reptYn?: string; + commSchdlYn?: string; + useYn?: string; +} + +export interface Schedule { + schdlSn: number; + siteId: string; + schdlTtl: string; + schdlSeCd: string; + schdlSeNm: string; + schdlBgngDt: string; + schdlEndDt: string; + reptYn: string; + commSchdlYn: string; +} + +const scheduleRepeatSchema = z.object({ + schdlSn: z.number().nullish(), + reptCycleCd: z.string().nullish(), + reptDayVl: z.number().nullish(), + reptDowVl: z.number().nullish(), + reptMmVl: z.number().nullish(), + reptQuVl: z.number().nullish(), + reptEndDt: z.string().nullish(), +}); + +export const scheduleFormSchema = z.object({ + siteId: z.string(), + schdlTtl: z.string(), + schdlSeCd: z.string(), + schdlBgngDt: z.string(), + schdlEndDt: z.string(), + schdlCn: z.string().nullish(), + reptYn: z.string().nullish(), + commSchdlYn: z.string().nullish(), + useYn: z.string().nullish(), + atflCd: z.string().nullish(), + reptDto: scheduleRepeatSchema.nullish(), +}); + +export type ScheduleFormData = z.infer; + +export interface ScheduleService { + getSchedules(params?: ScheduleParams): Promise; + createSchedule(data: ScheduleFormData): Promise; +} diff --git a/apps/cs/src/pages/schedule/ui/create-page.tsx b/apps/cs/src/pages/schedule/ui/create-page.tsx new file mode 100644 index 000000000..579689354 --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/create-page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import dayjs from "dayjs"; +import { useForm } from "react-hook-form"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useMutation } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useAlertStore } from "@/shared/lib"; +import { SITE_ID } from "@/shared/config"; +import { + scheduleFormSchema, + type ScheduleFormData, +} from "../model/schedule-interface"; +import { ScheduleApi } from "../api/schedule-service"; +import { ScheduleForm } from "./schedule-form"; + +function CreatePage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const dateStr = searchParams?.get("dateStr"); + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: createSchedule } = useMutation({ + mutationFn: ScheduleApi.createSchedule, + onSuccess: () => { + alert("일정이 등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(scheduleFormSchema), + defaultValues: { + siteId: SITE_ID, + schdlBgngDt: dateStr + ? dayjs(dateStr).format("YYYY-MM-DD 00:00:00") + : undefined, + schdlEndDt: dateStr + ? dayjs(dateStr).format("YYYY-MM-DD 23:59:59") + : undefined, + reptYn: "N", + commSchdlYn: "N", + useYn: "Y", + atflCd: null, + reptDto: null, + }, + }); + + const handleSave = async (data: ScheduleFormData) => { + // const reptDtoValid = Object.values(data.reptDto ?? {}).every( + // (value) => value === null, + // ); + // if (reptDtoValid) data.reptDto = null; + await createSchedule(data); + }; + + return ( +
+
+

스케줄 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/schedule/ui/date-time-picker.tsx b/apps/cs/src/pages/schedule/ui/date-time-picker.tsx new file mode 100644 index 000000000..0302253b9 --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/date-time-picker.tsx @@ -0,0 +1,123 @@ +"use client"; + +import dayjs from "dayjs"; +import { useState } from "react"; +import { CalendarIcon } from "lucide-react"; +import { + Button, + Calendar, + Popover, + PopoverContent, + PopoverTrigger, + ScrollArea, + ScrollBar, + cn, +} from "@common/components"; + +type DateTimePickerProps = { + date?: Date; + classes?: { trigger?: string }; + onDateChange?: (date: Date) => void; +}; + +function DateTimePicker({ date, classes, onDateChange }: DateTimePickerProps) { + const [isOpen, setIsOpen] = useState(false); + + const hours = Array.from({ length: 24 }, (_, i) => i); + const minutes = Array.from({ length: 60 }, (_, i) => i); + + const handleDateSelect = (selectedDate: Date | undefined) => { + if (!selectedDate) return; + onDateChange?.(selectedDate); + }; + + const handleTimeChange = (type: "hour" | "minute", value: string) => { + if (date) { + const newDate = new Date(date); + if (type === "hour") { + newDate.setHours(parseInt(value)); + } else if (type === "minute") { + newDate.setMinutes(parseInt(value)); + } + onDateChange?.(newDate); + } + }; + + return ( + + + + + +
+ +
+ +
+ {hours.reverse().map((hour) => ( + + ))} +
+ +
+ +
+ {minutes.map((minute) => ( + + ))} +
+ +
+
+
+
+
+ ); +} + +export { DateTimePicker }; diff --git a/apps/cs/src/pages/schedule/ui/schedule-form.tsx b/apps/cs/src/pages/schedule/ui/schedule-form.tsx new file mode 100644 index 000000000..bf34b02d1 --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/schedule-form.tsx @@ -0,0 +1,331 @@ +import dayjs from "dayjs"; +import { useState } from "react"; +import { type UseFormReturn } from "react-hook-form"; +import { + Checkbox, + DatePicker, + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Textarea, +} from "@common/components"; +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 { DateTimePicker } from "./date-time-picker"; + +type ScheduleFormProps = { + form: UseFormReturn; + scheduleId?: string; + handleSave: (data: ScheduleFormData) => Promise; + handleDelete?: () => Promise; +}; + +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); + }; + + return ( + +
+ + + + + + + + 제목 + + ( + + + + + + )} + /> + + + + 일정 기간 + +
+
+ ( + { + setBeginDate(date); + handleDateTimeChange("schdlBgngDt")(date); + }} + {...field} + /> + )} + /> + ~ + ( + { + setEndDate(date); + handleDateTimeChange("schdlEndDt")(date); + }} + {...field} + /> + )} + /> +
+
+ { + 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); + } + } + }} + /> + +
+
+
+
+ + 일정 반복 + +
+
+ ( + + +
+ { + field.onChange(checked ? "Y" : "N"); + }} + /> + +
+
+
+ )} + /> + ( + + + + value && field.onChange(value) + } + placeholder="반복 주기 선택" + disabled={form.watch("reptYn") === "N"} + /> + + + )} + /> + ( + + + + value && field.onChange(value) + } + className="w-auto px-4" + placeholder="반복 종료일 선택" + disabled={form.watch("reptYn") === "N"} + /> + + + )} + /> +
+
+
+
+ + 일정 유형 + + ( + + + + + + )} + /> + + + + 일정 내용 + + ( + + +