feat: 메뉴 폼 작업

This commit is contained in:
2025-03-21 18:22:34 +09:00
parent d8d5c16d31
commit c4a9aabce7
6 changed files with 586 additions and 12 deletions
+23 -4
View File
@@ -1,5 +1,16 @@
import { z } from "zod";
export const MENU_TYPE = {
EMPTY: "EMPTY",
CONTENTS: "CONTENTS",
BOARD: "BOARD",
INSIDE: "INSIDE",
OUTSIDE: "OUTSIDE",
BOTTOM: "BOTTOM",
} as const;
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
export interface Menu {
menuCd: number;
menuKornNm: string;
@@ -19,6 +30,7 @@ export interface Menu {
}
export const menuFormSchema = z.object({
siteId: z.string(),
menuKornNm: z.string(),
menuSubKornNm: z.string().nullish(),
menuEngNm: z.string().nullish(),
@@ -26,14 +38,21 @@ export const menuFormSchema = z.object({
levelNo: z.number(),
urlPath: z.string().nullish(),
iconNm: z.string().nullish(),
menuLnkgTypeCd: z.string().nullish(),
menuLnkgTypeCd: z.enum([
MENU_TYPE.EMPTY,
MENU_TYPE.CONTENTS,
MENU_TYPE.BOARD,
MENU_TYPE.INSIDE,
MENU_TYPE.OUTSIDE,
MENU_TYPE.BOTTOM,
]),
menuLnkgSn: z.number().nullish(),
otptYn: z.string().nullish(),
otptYn: z.enum(["Y", "N"]).default("Y"),
prntMenuCd: z.number().nullish(),
sortSeq: z.number(),
evlUseYn: z.string().nullish(),
evlUseYn: z.enum(["Y", "N"]).nullish(),
qrcd: z.string().nullish(),
useYn: z.string().nullish(),
useYn: z.enum(["Y", "N"]).default("Y"),
});
export type MenuFormData = z.infer<typeof menuFormSchema>;
@@ -0,0 +1,121 @@
import { type UseFormReturn } from "react-hook-form";
import {
Input,
Label,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@common/components";
import {
FormItem,
FormControl,
FormField,
FormMessage,
} from "@/shared/ui/form";
import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
interface BaseMenuFormProps {
form: UseFormReturn<MenuFormData>;
}
export function BaseMenuForm({ form }: BaseMenuFormProps) {
return (
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>메뉴명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="menuKornNm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="!text-[1.3rem]"
placeholder="메뉴명을 입력하십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>메뉴 영문명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="menuEngNm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="메뉴 영문명을 입력하십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>메뉴 유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="menuLnkgTypeCd"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
value={field.value}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-4">
{Object.entries(MENU_TYPE).map(([key, value]) => (
<div
key={key}
className="flex items-center space-x-2"
>
<RadioGroupItem
value={value}
id={`menuType${key}`}
/>
<Label htmlFor={`menuType${key}`}>
{key === "EMPTY" && "빈메뉴"}
{key === "CONTENTS" && "콘텐츠"}
{key === "BOARD" && "게시판"}
{key === "INSIDE" && "내부링크"}
{key === "OUTSIDE" && "외부링크"}
{key === "BOTTOM" && "하단메뉴"}
</Label>
</div>
))}
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -0,0 +1,198 @@
import { type UseFormReturn } from "react-hook-form";
import {
Input,
Label,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@common/components";
import {
FormItem,
FormControl,
FormField,
FormMessage,
} from "@/shared/ui/form";
import type { MenuFormData } from "../model/menu-interface";
export function ContentMenuForm({
form,
}: {
form: UseFormReturn<MenuFormData>;
}) {
return (
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>콘텐츠</TableHead>
<TableCell className="border">
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
{form.watch("menuLnkgSn")}
</div>
</TableCell>
</TableRow>
<TableRow>
<TableHead>메뉴 평가 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="evlUseYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="evlUseY" />
<Label htmlFor="evlUseY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="evlUseN" />
<Label htmlFor="evlUseN">사용안함</Label>
</div>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>메뉴 서브명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="menuSubKornNm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="메뉴 서브명을 입력하십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>메뉴 아이콘</TableHead>
<TableCell className="border">
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
{form.watch("iconNm")}
</div>
</TableCell>
</TableRow>
<TableRow>
<TableHead>출력 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="otptYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="otptY" />
<Label htmlFor="otptY">출력</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="otptN" />
<Label htmlFor="otptN">출력안함</Label>
</div>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="useYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="useY" />
<Label htmlFor="useY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="useN" />
<Label htmlFor="useN">사용안함</Label>
</div>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>QR코드</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="qrcd"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="qrcdY" />
<Label htmlFor="qrcdY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="qrcdN" />
<Label htmlFor="qrcdN">사용안함</Label>
</div>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -0,0 +1,118 @@
import { type UseFormReturn } from "react-hook-form";
import {
Input,
Label,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@common/components";
import {
FormItem,
FormControl,
FormField,
FormMessage,
} from "@/shared/ui/form";
import type { MenuFormData } from "../model/menu-interface";
export function EmptyMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
return (
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>메뉴 서브명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="menuSubKornNm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="메뉴 서브명을 입력하십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>메뉴 아이콘</TableHead>
<TableCell className="border">
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
{form.watch("iconNm")}
</div>
</TableCell>
</TableRow>
<TableRow>
<TableHead>출력 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="otptYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="otptY" />
<Label htmlFor="otptY">출력</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="otptN" />
<Label htmlFor="otptN">출력안함</Label>
</div>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="useYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="useY" />
<Label htmlFor="useY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="useN" />
<Label htmlFor="useN">사용안함</Label>
</div>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
+99
View File
@@ -0,0 +1,99 @@
import { useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { type UseFormReturn } from "react-hook-form";
import { Button } from "@common/components";
import { Form } from "@/shared/ui/form";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
import { BaseMenuForm } from "./base-menu-form";
import { EmptyMenuForm } from "./empty-menu-form";
import { ContentMenuForm } from "./content-menu-form";
interface MenuFormProps {
form: UseFormReturn<MenuFormData>;
menuId?: string;
handleSave: (data: MenuFormData) => Promise<void>;
handleDelete?: () => Promise<void>;
}
export function MenuForm({
form,
menuId,
handleSave,
handleDelete,
}: MenuFormProps) {
const isModifyMode = !!menuId;
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState(false);
const menuType = form.watch("menuLnkgTypeCd");
const onSubmit = async (data: MenuFormData) => {
const confirmMessage = isModifyMode
? "수정하시겠습니까?"
: "저장하시겠습니까?";
setOnConfirm(true);
if (await confirm(confirmMessage)) {
setOnConfirm(false);
handleSave(data);
}
setOnConfirm(false);
};
const onDelete = async () => {
setOnConfirm(true);
if (await confirm("삭제하시겠습니까?")) {
setOnConfirm(false);
handleDelete?.();
}
setOnConfirm(false);
};
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">
메뉴 등록/수정
</h3>
</div>
<div className="card">
<BaseMenuForm form={form} />
<div className="-mt-[0.1rem]">
{renderMenuTypeComponent(menuType, form)}
</div>
</div>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
<ConfirmDialog open={onConfirm} />
</>
);
}
function renderMenuTypeComponent(
menuType: string,
form: UseFormReturn<MenuFormData>,
) {
switch (menuType) {
case MENU_TYPE.EMPTY:
return <EmptyMenuForm form={form} />;
case MENU_TYPE.CONTENTS:
return <ContentMenuForm form={form} />;
default:
return null;
}
}
+27 -8
View File
@@ -1,7 +1,9 @@
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useQuery } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Button,
Select,
@@ -10,11 +12,13 @@ import {
SelectTrigger,
SelectValue,
} from "@common/components/ui";
import { PageLayout } from "@/shared/ui/page-layout";
import { ENDPOINT } from "@/shared/config";
import { SiteApi } from "@/entities/site";
import { ENDPOINT } from "@/shared/config";
import { PageLayout } from "@/shared/ui/page-layout";
import { MenuApi } from "../api/menu-service";
import { menuFormSchema, type MenuFormData } from "../model/menu-interface";
import { MenuTree } from "./menu-tree";
import { MenuForm } from "./menu-form";
function MenuPage() {
const [siteId, setSiteId] = useState("");
@@ -22,15 +26,14 @@ function MenuPage() {
const { data: treeData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
queryFn: () => MenuApi.getMenuList(siteId),
select: (data) => {
return data.map((menu) => ({
select: (data) =>
data.map((menu) => ({
id: menu.menuCd,
parent: menu.parentId ?? 0,
text: menu.menuKornNm,
droppable: true,
data: menu,
}));
},
})),
enabled: !!siteId,
});
@@ -44,9 +47,19 @@ function MenuPage() {
})),
});
const form = useForm<MenuFormData>({
resolver: zodResolver(menuFormSchema),
});
const handleSave = async (data: MenuFormData) => {
console.log(data);
};
const handleDelete = async () => {};
return (
<PageLayout pageTitle="메뉴 관리">
<div className="flex gap-4">
<div className="card flex gap-2">
<div className="card card-border">
<Select value={siteId} onValueChange={setSiteId}>
<SelectTrigger className="w-[18rem]">
@@ -76,7 +89,13 @@ function MenuPage() {
</>
)}
</div>
<div className="card card-border"></div>
<div className="card card-border flex-1">
<MenuForm
form={form}
handleSave={handleSave}
handleDelete={handleDelete}
/>
</div>
</div>
</PageLayout>
);