feat: 일정 수정 기능

This commit is contained in:
2025-05-28 10:20:37 +09:00
parent a9887eaac6
commit 7097e9ace6
9 changed files with 295 additions and 78 deletions
@@ -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 <VModifyPage schdlSn={schdlSn} />;
}
@@ -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<Schedule> {
const response = await client.get<Schedule>(
`${ENDPOINT.CMS_SERVICE.SCHEDULES}/${schdlSn}`,
);
return response.data;
}
public async createSchedule(data: ScheduleFormData): Promise<void> {
await client.post(ENDPOINT.CMS_SERVICE.SCHEDULES, data);
}
public async updateSchedule(
schdlSn: number,
data: ScheduleUpdateFormData,
): Promise<void> {
await client.put(`${ENDPOINT.CMS_SERVICE.SCHEDULES}/${schdlSn}`, data);
}
}
export const ScheduleApi = new ScheduleApiService();
+1
View File
@@ -1,2 +1,3 @@
export { SchedulePage } from "./ui/schedule-page";
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
+67
View File
@@ -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;
};
@@ -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<typeof scheduleFormSchema>;
export type ScheduleUpdateFormData = z.infer<typeof scheduleUpdateFormSchema>;
export type RepeatDto = z.infer<typeof scheduleRepeatSchema>;
export interface ScheduleService {
getSchedules(params?: ScheduleParams): Promise<Schedule[]>;
getScheduleInfo(schdlSn: number): Promise<Schedule>;
createSchedule(data: ScheduleFormData): Promise<void>;
updateSchedule(schdlSn: number, data: ScheduleUpdateFormData): Promise<void>;
}
@@ -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 (
@@ -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<ScheduleUpdateFormData>({
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 (
<div className="card card-border !mb-0 !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">스케줄 수정</h3>
</div>
<ScheduleForm form={form} handleSave={handleSave} />
</div>
);
}
export { ModifyPage };
+88 -65
View File
@@ -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<ScheduleFormData>;
type ScheduleFormProps<T extends ScheduleFormData | ScheduleUpdateFormData> = {
form: UseFormReturn<T>;
scheduleId?: string;
handleSave: (data: ScheduleFormData) => Promise<void>;
handleSave: (data: T) => Promise<void>;
handleDelete?: () => Promise<void>;
};
function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
function ScheduleForm<T extends ScheduleFormData | ScheduleUpdateFormData>({
form,
scheduleId,
...props
}: ScheduleFormProps<T>) {
const isModifyMode = !!scheduleId;
const [beginDate, setBeginDate] = useState<Date | undefined>(
dayjs(form?.getValues("schdlBgngDt"))?.toDate(),
);
const [endDate, setEndDate] = useState<Date | undefined>(
dayjs(form?.getValues("schdlEndDt"))?.toDate(),
);
const [isAllDay, setIsAllDay] = useState(false);
const [deleteAttachmentIds, setDeleteAttachmentIds] = useState<string[]>([]);
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<T>) => (date?: Date) => {
if (!date) return;
form.setValue(
name,
dayjs(date).format("YYYY-MM-DD HH:mm:ss") as PathValue<T, Path<T>>,
);
setIsAllDay(false);
};
return (
<FormWrapper form={form} isModifyMode={isModifyMode} {...props}>
@@ -62,13 +65,13 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
<TableCell className="border">
<FormField
control={form.control}
name="schdlTtl"
name={"schdlTtl" as Path<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
value={(field.value as string) ?? ""}
className="!text-[1.3rem]"
placeholder="일정 제목을 입력하십시오."
/>
@@ -85,15 +88,18 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
<div className="flex items-center gap-2">
<FormField
control={form.control}
name="schdlBgngDt"
name={"schdlBgngDt" as Path<T>}
render={({ field }) => (
<DateTimePicker
date={beginDate}
date={
field.value
? dayjs(field.value as string).toDate()
: undefined
}
classes={{ trigger: "w-auto px-4 justify-start" }}
onDateChange={(date) => {
setBeginDate(date);
handleDateTimeChange("schdlBgngDt")(date);
}}
onDateChange={handleDateTimeChange(
"schdlBgngDt" as Path<T>,
)}
{...field}
/>
)}
@@ -101,15 +107,18 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
~
<FormField
control={form.control}
name="schdlEndDt"
name={"schdlEndDt" as Path<T>}
render={({ field }) => (
<DateTimePicker
date={endDate}
date={
field.value
? dayjs(field.value as string).toDate()
: undefined
}
classes={{ trigger: "w-auto px-4 justify-start" }}
onDateChange={(date) => {
setEndDate(date);
handleDateTimeChange("schdlEndDt")(date);
}}
onDateChange={handleDateTimeChange(
"schdlEndDt" as Path<T>,
)}
{...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<T>,
);
const schdlEndDt = form.getValues(
"schdlEndDt" as Path<T>,
);
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<T>,
allSchdlBgngDt as PathValue<T, Path<T>>,
);
form.setValue(
"schdlEndDt" as Path<T>,
allSchdlEndDt as PathValue<T, Path<T>>,
);
}
}}
/>
@@ -150,7 +166,7 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
<div className="flex items-center gap-2">
<FormField
control={form.control}
name="reptYn"
name={"reptYn" as Path<T>}
render={({ field }) => (
<FormItem>
<FormControl>
@@ -170,19 +186,17 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
/>
<FormField
control={form.control}
name="reptDto.reptCycleCd"
name={"reptDto.reptCycleCd" as Path<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<CodeSelect
{...field}
upCd="SCHDLRPTCD"
value={field.value ?? ""}
onValueChange={(value) =>
value && field.onChange(value)
}
value={(field.value as string) ?? ""}
onValueChange={field.onChange}
placeholder="반복 주기 선택"
disabled={form.watch("reptYn") === "N"}
disabled={form.watch("reptYn" as Path<T>) === "N"}
/>
</FormControl>
</FormItem>
@@ -190,7 +204,7 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
/>
<FormField
control={form.control}
name="reptDto.reptEndDt"
name={"reptDto.reptEndDt" as Path<T>}
render={({ field }) => (
<FormItem>
<FormControl>
@@ -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<T>,
dayjs(value).format(
"YYYY-MM-DD 23:59:59",
) as PathValue<T, Path<T>>,
);
}}
className="w-auto px-4"
placeholder="반복 종료일 선택"
disabled={form.watch("reptYn") === "N"}
disabled={form.watch("reptYn" as Path<T>) === "N"}
/>
</FormControl>
</FormItem>
@@ -221,14 +242,14 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
<TableCell className="border">
<FormField
control={form.control}
name="schdlSeCd"
name={"schdlSeCd" as Path<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<CodeSelect
{...field}
upCd="SCHDLCLCD"
value={field.value ?? ""}
value={(field.value as string) ?? ""}
onValueChange={field.onChange}
/>
</FormControl>
@@ -242,13 +263,13 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
<TableCell className="border">
<FormField
control={form.control}
name="schdlCn"
name={"schdlCn" as Path<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
{...field}
value={field.value ?? ""}
value={(field.value as string) ?? ""}
className="min-h-32 !text-[1.3rem]"
placeholder="일정 내용을 입력하십시오."
/>
@@ -263,7 +284,7 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
<TableCell className="border">
<FormField
control={form.control}
name="commSchdlYn"
name={"commSchdlYn" as Path<T>}
render={({ field }) => (
<FormItem>
<FormControl>
@@ -298,13 +319,15 @@ function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) {
<TableCell className="border">
<FormField
control={form.control}
name="useYn"
name={"useYn" as Path<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
className="flex items-center gap-4"
{...field}
value={(field.value as string) ?? ""}
onValueChange={field.onChange}
className="flex items-center gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="Y" id="useY" />
@@ -8,9 +8,11 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin, {
type DateClickArg,
} from "@fullcalendar/interaction";
import type { EventClickArg } from "@fullcalendar/core";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT, SITE_ID } from "@/shared/config";
import { ScheduleApi } from "../api/schedule-service";
import { calculateEventDuration } from "../lib/utils";
import type { ScheduleParams } from "../model/schedule-interface";
function SchedulePage() {
@@ -22,19 +24,33 @@ function SchedulePage() {
useYn: "Y",
});
const { data } = useQuery({
const { data: eventData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SCHEDULES, scheduleParams],
queryFn: () => ScheduleApi.getSchedules(scheduleParams),
select: (data) => {
return data.map((item) => ({
title: item.schdlTtl,
start: item.schdlBgngDt,
end: item.schdlEndDt,
}));
return data.map((item) => {
const minuteDiff = calculateEventDuration(
item.schdlBgngDt,
item.schdlEndDt,
);
return {
id: item.schdlSn.toString(),
title: item.schdlTtl,
start: item.schdlBgngDt,
end: item.schdlEndDt,
allDay: minuteDiff >= 1439,
classNames: ["cursor-pointer"],
};
});
},
enabled: !!scheduleParams.schdlBgngYmd && !!scheduleParams.schdlEndYmd,
});
const handleEventClick = (info: EventClickArg) => {
router.push(`/schedule/register/${info.event.id}`);
};
const handleDateClick = (info: DateClickArg) => {
router.push(`/schedule/register?dateStr=${info.dateStr}`);
};
@@ -44,8 +60,10 @@ function SchedulePage() {
<div className="p-5">
<FullCalendar
plugins={[dayGridPlugin, interactionPlugin]}
events={data}
events={eventData}
locale="ko"
dayCellClassNames="cursor-pointer hover:bg-accent"
eventClick={handleEventClick}
dateClick={handleDateClick}
datesSet={(dateInfo) => {
setScheduleParams({