feat: 메뉴 저장 및 수정 작업

This commit is contained in:
2025-03-26 15:07:53 +09:00
parent 5b4e674214
commit d4f7cf140e
15 changed files with 202 additions and 87 deletions
+61 -19
View File
@@ -9,43 +9,84 @@
color: #666; color: #666;
} }
.card_width10 {width:calc(10% - 12px);} .card_width10 {
width: calc(10% - 12px);
}
.card_width20 {width:calc(20% - 12px);} .card_width20 {
width: calc(20% - 12px);
}
.card_width25 {width:calc(25% - 12px);} .card_width25 {
width: calc(25% - 12px);
}
.card_width30 {width:calc(30% - 12px);} .card_width30 {
width: calc(30% - 12px);
}
.card_width33 {width:calc(33.333% - 12px);} .card_width33 {
width: calc(33.333% - 12px);
}
.card_width40 {width:calc(40% - 12px);} .card_width40 {
width: calc(40% - 12px);
}
.card_width45 {width:calc(45% - 12px);} .card_width45 {
width: calc(45% - 12px);
}
.card_width50 {width:calc(50% - 12px);} .card_width50 {
width: calc(50% - 12px);
}
.card_width60 {width:calc(60% - 12px);} .card_width60 {
width: calc(60% - 12px);
}
.card_width66 {width:calc(66.666% - 12px);} .card_width66 {
width: calc(66.666% - 12px);
}
.card_width70 {width:calc(70% - 12px);} .card_width70 {
width: calc(70% - 12px);
}
.card_width75 {width:calc(75% - 12px);} .card_width75 {
width: calc(75% - 12px);
}
.card_width80 {width:calc(80% - 12px);} .card_width80 {
width: calc(80% - 12px);
}
.card_width90 {width:calc(90% - 12px);} .card_width90 {
width: calc(90% - 12px);
}
.card_width100 {width:calc(100% - 12px);} .card_width100 {
width: calc(100% - 12px);
}
.card_nopadding {padding:0 !important;} .card_nopadding {
padding: 0 !important;
}
.card_nomargin {width:100%;margin:0 !important;padding:0 !important;} .card_nomargin {
width: 100%;
margin: 0 !important;
padding: 0 !important;
}
.topline {border-top: 1px solid #ccc;} .topline {
border-top: 1px solid #ccc;
}
.bottomline {border-bottom: 1px solid #e6eef3;background: #fff;} .bottomline {
border-bottom: 1px solid #e6eef3;
background: #fff;
}
.card-border { .card-border {
border: 1px solid #dbe2e6; border: 1px solid #dbe2e6;
@@ -54,6 +95,7 @@
} }
.card-header { .card-header {
margin-top: -0.6rem;
margin-left: -0.6rem; margin-left: -0.6rem;
margin-right: -0.6rem; margin-right: -0.6rem;
display: flex; display: flex;
+14 -1
View File
@@ -1,8 +1,21 @@
import { client } from "@/shared/api"; import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import type { Menu, MenuService, TreeMenu } from "../model/menu-interface"; import type {
Menu,
MenuFormData,
MenuService,
TreeMenu,
} from "../model/menu-interface";
export class MenuApiService implements MenuService { export class MenuApiService implements MenuService {
public async createMenu(data: MenuFormData): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}`,
data,
);
return response.data;
}
public async getTreeMenuList(siteId: string): Promise<Menu[]> { public async getTreeMenuList(siteId: string): Promise<Menu[]> {
const response = await client.get<Menu[]>( const response = await client.get<Menu[]>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`, `${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
@@ -53,17 +53,18 @@ export const menuFormSchema = z.object({
MENU_TYPE.BOTTOM, MENU_TYPE.BOTTOM,
]), ]),
menuLnkgSn: z.number().nullish(), menuLnkgSn: z.number().nullish(),
otptYn: z.enum(["Y", "N"]).default("Y").nullish(), otptYn: z.string().default("Y").nullish(),
prntMenuCd: z.number().nullish(), prntMenuCd: z.number().nullish(),
sortSeq: z.number(), sortSeq: z.number(),
evlUseYn: z.enum(["Y", "N"]).nullish(), evlUseYn: z.string().default("Y").nullish(),
qrcd: z.string().nullish(), qrcd: z.string().nullish(),
useYn: z.enum(["Y", "N"]).default("Y").nullish(), useYn: z.string().default("Y").nullish(),
}); });
export type MenuFormData = z.infer<typeof menuFormSchema>; export type MenuFormData = z.infer<typeof menuFormSchema>;
export interface MenuService { export interface MenuService {
createMenu: (data: MenuFormData) => Promise<void>;
getTreeMenuList: (siteId: string) => Promise<Menu[]>; getTreeMenuList: (siteId: string) => Promise<Menu[]>;
modifyTreeMenu: (siteId: string, data: TreeMenu[]) => Promise<void>; modifyTreeMenu: (siteId: string, data: TreeMenu[]) => Promise<void>;
modifyMenuName: (menuCd: number, name: string) => Promise<void>; modifyMenuName: (menuCd: number, name: string) => Promise<void>;
+15 -10
View File
@@ -20,10 +20,10 @@ import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
interface BaseMenuFormProps { interface BaseMenuFormProps {
form: UseFormReturn<MenuFormData>; form: UseFormReturn<MenuFormData>;
menuId?: number; menuCd?: number;
} }
export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) { function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
return ( return (
<Table variant="secondary"> <Table variant="secondary">
<colgroup> <colgroup>
@@ -31,14 +31,16 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
<col width="80%" /> <col width="80%" />
</colgroup> </colgroup>
<TableBody> <TableBody>
<TableRow> {menuCd && (
<TableHead>메뉴ID</TableHead> <TableRow>
<TableCell className="border"> <TableHead>메뉴ID</TableHead>
<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"> <TableCell className="border">
{menuId} <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">
</div> {menuCd}
</TableCell> </div>
</TableRow> </TableCell>
</TableRow>
)}
<TableRow> <TableRow>
<TableHead>메뉴명</TableHead> <TableHead>메뉴명</TableHead>
<TableCell className="border"> <TableCell className="border">
@@ -50,6 +52,7 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
value={field.value ?? ""}
className="!text-[1.3rem]" className="!text-[1.3rem]"
placeholder="메뉴명을 입력하십시오." placeholder="메뉴명을 입력하십시오."
/> />
@@ -128,3 +131,5 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
</Table> </Table>
); );
} }
export { BaseMenuForm };
+3 -1
View File
@@ -17,7 +17,7 @@ import {
} from "@/shared/ui/form"; } from "@/shared/ui/form";
import type { MenuFormData } from "../model/menu-interface"; import type { MenuFormData } from "../model/menu-interface";
export function BaseTypeForm({ form }: { form: UseFormReturn<MenuFormData> }) { function BaseTypeForm({ form }: { form: UseFormReturn<MenuFormData> }) {
const handleIconClick = (icon: string) => { const handleIconClick = (icon: string) => {
form.setValue("iconNm", icon); form.setValue("iconNm", icon);
}; };
@@ -124,3 +124,5 @@ export function BaseTypeForm({ form }: { form: UseFormReturn<MenuFormData> }) {
</> </>
); );
} }
export { BaseTypeForm };
@@ -18,7 +18,7 @@ import {
import type { MenuFormData } from "../model/menu-interface"; import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form"; import { BaseTypeForm } from "./base-type-form";
export function BoardMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) { function BoardMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
return ( return (
<Table variant="secondary"> <Table variant="secondary">
<colgroup> <colgroup>
@@ -142,3 +142,5 @@ export function BoardMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
</Table> </Table>
); );
} }
export { BoardMenuForm };
@@ -3,11 +3,7 @@ import { Table, TableBody } from "@common/components";
import type { MenuFormData } from "../model/menu-interface"; import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form"; import { BaseTypeForm } from "./base-type-form";
export function BottomMenuForm({ function BottomMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
form,
}: {
form: UseFormReturn<MenuFormData>;
}) {
return ( return (
<Table variant="secondary"> <Table variant="secondary">
<colgroup> <colgroup>
@@ -20,3 +16,5 @@ export function BottomMenuForm({
</Table> </Table>
); );
} }
export { BottomMenuForm };
@@ -18,11 +18,7 @@ import {
import type { MenuFormData } from "../model/menu-interface"; import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form"; import { BaseTypeForm } from "./base-type-form";
export function ContentMenuForm({ function ContentMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
form,
}: {
form: UseFormReturn<MenuFormData>;
}) {
return ( return (
<Table variant="secondary"> <Table variant="secondary">
<colgroup> <colgroup>
@@ -146,3 +142,5 @@ export function ContentMenuForm({
</Table> </Table>
); );
} }
export { ContentMenuForm };
@@ -3,7 +3,7 @@ import { Table, TableBody } from "@common/components";
import type { MenuFormData } from "../model/menu-interface"; import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form"; import { BaseTypeForm } from "./base-type-form";
export function EmptyMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) { function EmptyMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
return ( return (
<Table variant="secondary"> <Table variant="secondary">
<colgroup> <colgroup>
@@ -16,3 +16,5 @@ export function EmptyMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
</Table> </Table>
); );
} }
export { EmptyMenuForm };
@@ -3,11 +3,7 @@ import { Table, TableBody } from "@common/components";
import type { MenuFormData } from "../model/menu-interface"; import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form"; import { BaseTypeForm } from "./base-type-form";
export function InsideMenuForm({ function InsideMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
form,
}: {
form: UseFormReturn<MenuFormData>;
}) {
return ( return (
<Table variant="secondary"> <Table variant="secondary">
<colgroup> <colgroup>
@@ -20,3 +16,5 @@ export function InsideMenuForm({
</Table> </Table>
); );
} }
export { InsideMenuForm };
+14 -13
View File
@@ -12,21 +12,18 @@ import { BoardMenuForm } from "./board-menu-form";
import { InsideMenuForm } from "./inside-menu-form"; import { InsideMenuForm } from "./inside-menu-form";
import { OutsideMenuForm } from "./outside-menu-form"; import { OutsideMenuForm } from "./outside-menu-form";
import { BottomMenuForm } from "./bottom-menu-form"; import { BottomMenuForm } from "./bottom-menu-form";
import { PermissionsManagementDialog } from "./permissions-management-dialog";
interface MenuFormProps { interface MenuFormProps {
form: UseFormReturn<MenuFormData>; form: UseFormReturn<MenuFormData>;
menuId?: number; menuCd?: number;
handleSave: (data: MenuFormData) => Promise<void>; handleSave: (data: MenuFormData) => void;
handleDelete?: () => Promise<void>; handleDelete?: () => void;
} }
export function MenuForm({ function MenuForm({ form, menuCd, handleSave, handleDelete }: MenuFormProps) {
form, const isModifyMode = !!menuCd;
menuId,
handleSave,
handleDelete,
}: MenuFormProps) {
const isModifyMode = !!menuId;
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState(false); const [onConfirm, setOnConfirm] = useState(false);
@@ -53,6 +50,7 @@ export function MenuForm({
}; };
const menuType = form.watch("menuLnkgTypeCd"); const menuType = form.watch("menuLnkgTypeCd");
const isSubmitDisabled = !form.watch("siteId");
return ( return (
<> <>
@@ -61,18 +59,19 @@ export function MenuForm({
<div className="card card-border"> <div className="card card-border">
<div className="card-header"> <div className="card-header">
<h3 className="text-2xl font-medium text-label"> <h3 className="text-2xl font-medium text-label">
메뉴 등록/수정 {isModifyMode ? "메뉴 수정" : "메뉴 등록"}
</h3> </h3>
<PermissionsManagementDialog menuCd={menuCd} />
</div> </div>
<div className="card"> <div className="card">
<BaseMenuForm form={form} menuId={menuId} /> <BaseMenuForm form={form} menuCd={menuCd} />
<div className="-mt-[0.1rem]"> <div className="-mt-[0.1rem]">
{renderTypeForm(menuType, form)} {renderTypeForm(menuType, form)}
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-center gap-4 pb-12 pt-10"> <div className="flex justify-center gap-4 pb-12 pt-10">
<Button size="lg" type="submit"> <Button size="lg" type="submit" disabled={isSubmitDisabled}>
저장 저장
</Button> </Button>
{isModifyMode && ( {isModifyMode && (
@@ -106,3 +105,5 @@ function renderTypeForm(menuType: string, form: UseFormReturn<MenuFormData>) {
return null; return null;
} }
} }
export { MenuForm };
+11 -3
View File
@@ -1,10 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import { useMutation } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { NodeModel } from "@minoru/react-dnd-treeview"; import type { NodeModel } from "@minoru/react-dnd-treeview";
import { cn } from "@common/components/lib";
import { Button, Input } from "@common/components/ui"; import { Button, Input } from "@common/components/ui";
import type { Menu } from "../model/menu-interface"; import { cn } from "@common/components/lib";
import { ENDPOINT } from "@/shared/config";
import { MenuApi } from "../api/menu-service"; import { MenuApi } from "../api/menu-service";
import type { Menu } from "../model/menu-interface";
type MenuNodeProps = { type MenuNodeProps = {
node: NodeModel<Menu>; node: NodeModel<Menu>;
@@ -26,11 +27,18 @@ function MenuNode({
const { id, text, data } = node; const { id, text, data } = node;
const indent = depth * 10 + 5; const indent = depth * 10 + 5;
const queryClient = useQueryClient();
const { mutateAsync: modifyMenuName } = useMutation({ const { mutateAsync: modifyMenuName } = useMutation({
mutationFn: (nodeName: string) => { mutationFn: (nodeName: string) => {
const encodedName = encodeURIComponent(nodeName); const encodedName = encodeURIComponent(nodeName);
return MenuApi.modifyMenuName(id as number, encodedName); return MenuApi.modifyMenuName(id as number, encodedName);
}, },
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
});
},
}); });
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
+52 -7
View File
@@ -2,8 +2,9 @@
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useQuery } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import type { NodeModel } from "@minoru/react-dnd-treeview";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
Button, Button,
Select, Select,
@@ -15,6 +16,8 @@ import {
import { SiteApi } from "@/entities/site"; import { SiteApi } from "@/entities/site";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import { PageLayout } from "@/shared/ui/page-layout"; import { PageLayout } from "@/shared/ui/page-layout";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import type { Menu, MenuType } from "../model/menu-interface";
import { MenuApi } from "../api/menu-service"; import { MenuApi } from "../api/menu-service";
import { import {
MENU_TYPE, MENU_TYPE,
@@ -26,12 +29,23 @@ import { MenuForm } from "./menu-form";
import { generateTreeData } from "../lib/utils"; import { generateTreeData } from "../lib/utils";
function MenuPage() { function MenuPage() {
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
const [siteId, setSiteId] = useState(""); const [siteId, setSiteId] = useState("");
const [selectedMenu, setSelectedMenu] = useState<Menu | null>(null);
const { data: treeData } = useQuery({ const { data: treeData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId], queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
queryFn: () => MenuApi.getTreeMenuList(siteId), queryFn: () => MenuApi.getTreeMenuList(siteId),
select: (data) => generateTreeData(data), select: (data) => {
const treeData = generateTreeData(data);
const defaultSortSeq =
treeData.filter((node) => node.data?.levelNo === 1).length + 1;
form.setValue("sortSeq", defaultSortSeq);
return treeData;
},
enabled: !!siteId, enabled: !!siteId,
}); });
@@ -45,15 +59,40 @@ function MenuPage() {
})), })),
}); });
const { mutate: createMenu } = useMutation({
mutationFn: (data: MenuFormData) => MenuApi.createMenu(data),
onSuccess: () => {
alert("저장되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
});
},
});
const form = useForm<MenuFormData>({ const form = useForm<MenuFormData>({
resolver: zodResolver(menuFormSchema), resolver: zodResolver(menuFormSchema),
defaultValues: { defaultValues: {
menuLnkgTypeCd: MENU_TYPE.EMPTY, menuLnkgTypeCd: MENU_TYPE.EMPTY,
}, },
values: {
...selectedMenu,
siteId,
sortSeq: selectedMenu?.sortSeq ?? 1,
levelNo: selectedMenu?.levelNo ?? 1,
menuKornNm: selectedMenu?.menuKornNm ?? "",
menuLnkgTypeCd:
(selectedMenu?.menuLnkgTypeCd as MenuType) ?? MENU_TYPE.EMPTY,
},
}); });
const handleSave = async (data: MenuFormData) => { const handleSiteIdChange = (value: string) => {
console.log(data); setSiteId(value);
form.setValue("siteId", value);
};
const handleNodeSelect = (node: NodeModel<Menu>) => {
if (!node.data) return;
setSelectedMenu(node.data);
}; };
const handleDelete = async () => {}; const handleDelete = async () => {};
@@ -62,7 +101,7 @@ function MenuPage() {
<PageLayout pageTitle="메뉴 관리"> <PageLayout pageTitle="메뉴 관리">
<div className="card flex gap-2"> <div className="card flex gap-2">
<div className="card card-border w-[24rem]"> <div className="card card-border w-[24rem]">
<Select value={siteId} onValueChange={setSiteId}> <Select value={siteId} onValueChange={handleSiteIdChange}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="선택하세요" /> <SelectValue placeholder="선택하세요" />
</SelectTrigger> </SelectTrigger>
@@ -85,7 +124,12 @@ function MenuPage() {
</Button> </Button>
</div> </div>
<div className="card card-border !mx-0"> <div className="card card-border !mx-0">
<MenuTree treeData={treeData ?? []} siteId={siteId} /> <MenuTree
treeData={treeData ?? []}
siteId={siteId}
selectedMenu={selectedMenu}
onSelectNode={handleNodeSelect}
/>
</div> </div>
</> </>
)} )}
@@ -93,7 +137,8 @@ function MenuPage() {
<div className="card card-border flex-1"> <div className="card card-border flex-1">
<MenuForm <MenuForm
form={form} form={form}
handleSave={handleSave} menuCd={selectedMenu?.menuCd}
handleSave={createMenu}
handleDelete={handleDelete} handleDelete={handleDelete}
/> />
</div> </div>
+10 -8
View File
@@ -1,6 +1,5 @@
"use client"; "use client";
import { useState } from "react";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { import {
@@ -18,9 +17,16 @@ import { MenuNode } from "./menu-node";
type MenuTreeProps = { type MenuTreeProps = {
treeData: NodeModel<Menu>[]; treeData: NodeModel<Menu>[];
siteId: string; siteId: string;
selectedMenu: Menu | null;
onSelectNode: (node: NodeModel<Menu>) => void;
}; };
function MenuTree({ treeData, siteId }: MenuTreeProps) { function MenuTree({
treeData,
siteId,
selectedMenu,
onSelectNode,
}: MenuTreeProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutate: modifyTreeMenu } = useMutation({ const { mutate: modifyTreeMenu } = useMutation({
@@ -33,10 +39,6 @@ function MenuTree({ treeData, siteId }: MenuTreeProps) {
}, },
}); });
const [selectedNode, setSelectedNode] = useState<NodeModel<Menu> | null>(
null,
);
return ( return (
<DndProvider backend={MultiBackend} options={getBackendOptions()}> <DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree <Tree
@@ -47,9 +49,9 @@ function MenuTree({ treeData, siteId }: MenuTreeProps) {
node={node} node={node}
depth={depth} depth={depth}
isOpen={isOpen} isOpen={isOpen}
isSelected={selectedNode?.id === node.id} isSelected={selectedMenu?.menuCd === node.id}
onToggle={onToggle} onToggle={onToggle}
onSelect={handleSelectNode(setSelectedNode)} onSelect={handleSelectNode(onSelectNode)}
/> />
)} )}
onDrop={handleDropNode(modifyTreeMenu)} onDrop={handleDropNode(modifyTreeMenu)}
@@ -3,11 +3,7 @@ import { Table, TableBody } from "@common/components";
import type { MenuFormData } from "../model/menu-interface"; import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form"; import { BaseTypeForm } from "./base-type-form";
export function OutsideMenuForm({ function OutsideMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
form,
}: {
form: UseFormReturn<MenuFormData>;
}) {
return ( return (
<Table variant="secondary"> <Table variant="secondary">
<colgroup> <colgroup>
@@ -20,3 +16,5 @@ export function OutsideMenuForm({
</Table> </Table>
); );
} }
export { OutsideMenuForm };