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;
}
.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 {
border: 1px solid #dbe2e6;
@@ -54,6 +95,7 @@
}
.card-header {
margin-top: -0.6rem;
margin-left: -0.6rem;
margin-right: -0.6rem;
display: flex;
+14 -1
View File
@@ -1,8 +1,21 @@
import { client } from "@/shared/api";
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 {
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[]> {
const response = await client.get<Menu[]>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
@@ -53,17 +53,18 @@ export const menuFormSchema = z.object({
MENU_TYPE.BOTTOM,
]),
menuLnkgSn: z.number().nullish(),
otptYn: z.enum(["Y", "N"]).default("Y").nullish(),
otptYn: z.string().default("Y").nullish(),
prntMenuCd: z.number().nullish(),
sortSeq: z.number(),
evlUseYn: z.enum(["Y", "N"]).nullish(),
evlUseYn: z.string().default("Y").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 interface MenuService {
createMenu: (data: MenuFormData) => Promise<void>;
getTreeMenuList: (siteId: string) => Promise<Menu[]>;
modifyTreeMenu: (siteId: string, data: TreeMenu[]) => 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 {
form: UseFormReturn<MenuFormData>;
menuId?: number;
menuCd?: number;
}
export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
return (
<Table variant="secondary">
<colgroup>
@@ -31,14 +31,16 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>메뉴ID</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">
{menuId}
</div>
</TableCell>
</TableRow>
{menuCd && (
<TableRow>
<TableHead>메뉴ID</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">
{menuCd}
</div>
</TableCell>
</TableRow>
)}
<TableRow>
<TableHead>메뉴명</TableHead>
<TableCell className="border">
@@ -50,6 +52,7 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="메뉴명을 입력하십시오."
/>
@@ -128,3 +131,5 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
</Table>
);
}
export { BaseMenuForm };
+3 -1
View File
@@ -17,7 +17,7 @@ import {
} from "@/shared/ui/form";
import type { MenuFormData } from "../model/menu-interface";
export function BaseTypeForm({ form }: { form: UseFormReturn<MenuFormData> }) {
function BaseTypeForm({ form }: { form: UseFormReturn<MenuFormData> }) {
const handleIconClick = (icon: string) => {
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 { BaseTypeForm } from "./base-type-form";
export function BoardMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
function BoardMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
return (
<Table variant="secondary">
<colgroup>
@@ -142,3 +142,5 @@ export function BoardMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
</Table>
);
}
export { BoardMenuForm };
@@ -3,11 +3,7 @@ import { Table, TableBody } from "@common/components";
import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form";
export function BottomMenuForm({
form,
}: {
form: UseFormReturn<MenuFormData>;
}) {
function BottomMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
return (
<Table variant="secondary">
<colgroup>
@@ -20,3 +16,5 @@ export function BottomMenuForm({
</Table>
);
}
export { BottomMenuForm };
@@ -18,11 +18,7 @@ import {
import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form";
export function ContentMenuForm({
form,
}: {
form: UseFormReturn<MenuFormData>;
}) {
function ContentMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
return (
<Table variant="secondary">
<colgroup>
@@ -146,3 +142,5 @@ export function ContentMenuForm({
</Table>
);
}
export { ContentMenuForm };
@@ -3,7 +3,7 @@ import { Table, TableBody } from "@common/components";
import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form";
export function EmptyMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
function EmptyMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
return (
<Table variant="secondary">
<colgroup>
@@ -16,3 +16,5 @@ export function EmptyMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
</Table>
);
}
export { EmptyMenuForm };
@@ -3,11 +3,7 @@ import { Table, TableBody } from "@common/components";
import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form";
export function InsideMenuForm({
form,
}: {
form: UseFormReturn<MenuFormData>;
}) {
function InsideMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
return (
<Table variant="secondary">
<colgroup>
@@ -20,3 +16,5 @@ export function InsideMenuForm({
</Table>
);
}
export { InsideMenuForm };
+14 -13
View File
@@ -12,21 +12,18 @@ import { BoardMenuForm } from "./board-menu-form";
import { InsideMenuForm } from "./inside-menu-form";
import { OutsideMenuForm } from "./outside-menu-form";
import { BottomMenuForm } from "./bottom-menu-form";
import { PermissionsManagementDialog } from "./permissions-management-dialog";
interface MenuFormProps {
form: UseFormReturn<MenuFormData>;
menuId?: number;
handleSave: (data: MenuFormData) => Promise<void>;
handleDelete?: () => Promise<void>;
menuCd?: number;
handleSave: (data: MenuFormData) => void;
handleDelete?: () => void;
}
export function MenuForm({
form,
menuId,
handleSave,
handleDelete,
}: MenuFormProps) {
const isModifyMode = !!menuId;
function MenuForm({ form, menuCd, handleSave, handleDelete }: MenuFormProps) {
const isModifyMode = !!menuCd;
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState(false);
@@ -53,6 +50,7 @@ export function MenuForm({
};
const menuType = form.watch("menuLnkgTypeCd");
const isSubmitDisabled = !form.watch("siteId");
return (
<>
@@ -61,18 +59,19 @@ export function MenuForm({
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">
메뉴 등록/수정
{isModifyMode ? "메뉴 수정" : "메뉴 등록"}
</h3>
<PermissionsManagementDialog menuCd={menuCd} />
</div>
<div className="card">
<BaseMenuForm form={form} menuId={menuId} />
<BaseMenuForm form={form} menuCd={menuCd} />
<div className="-mt-[0.1rem]">
{renderTypeForm(menuType, form)}
</div>
</div>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button size="lg" type="submit">
<Button size="lg" type="submit" disabled={isSubmitDisabled}>
저장
</Button>
{isModifyMode && (
@@ -106,3 +105,5 @@ function renderTypeForm(menuType: string, form: UseFormReturn<MenuFormData>) {
return null;
}
}
export { MenuForm };
+11 -3
View File
@@ -1,10 +1,11 @@
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 { cn } from "@common/components/lib";
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 type { Menu } from "../model/menu-interface";
type MenuNodeProps = {
node: NodeModel<Menu>;
@@ -26,11 +27,18 @@ function MenuNode({
const { id, text, data } = node;
const indent = depth * 10 + 5;
const queryClient = useQueryClient();
const { mutateAsync: modifyMenuName } = useMutation({
mutationFn: (nodeName: string) => {
const encodedName = encodeURIComponent(nodeName);
return MenuApi.modifyMenuName(id as number, encodedName);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
});
},
});
const [editMode, setEditMode] = useState(false);
+52 -7
View File
@@ -2,8 +2,9 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useQuery } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod";
import type { NodeModel } from "@minoru/react-dnd-treeview";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Button,
Select,
@@ -15,6 +16,8 @@ import {
import { SiteApi } from "@/entities/site";
import { ENDPOINT } from "@/shared/config";
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 {
MENU_TYPE,
@@ -26,12 +29,23 @@ import { MenuForm } from "./menu-form";
import { generateTreeData } from "../lib/utils";
function MenuPage() {
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
const [siteId, setSiteId] = useState("");
const [selectedMenu, setSelectedMenu] = useState<Menu | null>(null);
const { data: treeData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, 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,
});
@@ -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>({
resolver: zodResolver(menuFormSchema),
defaultValues: {
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) => {
console.log(data);
const handleSiteIdChange = (value: string) => {
setSiteId(value);
form.setValue("siteId", value);
};
const handleNodeSelect = (node: NodeModel<Menu>) => {
if (!node.data) return;
setSelectedMenu(node.data);
};
const handleDelete = async () => {};
@@ -62,7 +101,7 @@ function MenuPage() {
<PageLayout pageTitle="메뉴 관리">
<div className="card flex gap-2">
<div className="card card-border w-[24rem]">
<Select value={siteId} onValueChange={setSiteId}>
<Select value={siteId} onValueChange={handleSiteIdChange}>
<SelectTrigger>
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
@@ -85,7 +124,12 @@ function MenuPage() {
</Button>
</div>
<div className="card card-border !mx-0">
<MenuTree treeData={treeData ?? []} siteId={siteId} />
<MenuTree
treeData={treeData ?? []}
siteId={siteId}
selectedMenu={selectedMenu}
onSelectNode={handleNodeSelect}
/>
</div>
</>
)}
@@ -93,7 +137,8 @@ function MenuPage() {
<div className="card card-border flex-1">
<MenuForm
form={form}
handleSave={handleSave}
menuCd={selectedMenu?.menuCd}
handleSave={createMenu}
handleDelete={handleDelete}
/>
</div>
+10 -8
View File
@@ -1,6 +1,5 @@
"use client";
import { useState } from "react";
import { DndProvider } from "react-dnd";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
@@ -18,9 +17,16 @@ import { MenuNode } from "./menu-node";
type MenuTreeProps = {
treeData: NodeModel<Menu>[];
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 { mutate: modifyTreeMenu } = useMutation({
@@ -33,10 +39,6 @@ function MenuTree({ treeData, siteId }: MenuTreeProps) {
},
});
const [selectedNode, setSelectedNode] = useState<NodeModel<Menu> | null>(
null,
);
return (
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree
@@ -47,9 +49,9 @@ function MenuTree({ treeData, siteId }: MenuTreeProps) {
node={node}
depth={depth}
isOpen={isOpen}
isSelected={selectedNode?.id === node.id}
isSelected={selectedMenu?.menuCd === node.id}
onToggle={onToggle}
onSelect={handleSelectNode(setSelectedNode)}
onSelect={handleSelectNode(onSelectNode)}
/>
)}
onDrop={handleDropNode(modifyTreeMenu)}
@@ -3,11 +3,7 @@ import { Table, TableBody } from "@common/components";
import type { MenuFormData } from "../model/menu-interface";
import { BaseTypeForm } from "./base-type-form";
export function OutsideMenuForm({
form,
}: {
form: UseFormReturn<MenuFormData>;
}) {
function OutsideMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
return (
<Table variant="secondary">
<colgroup>
@@ -20,3 +16,5 @@ export function OutsideMenuForm({
</Table>
);
}
export { OutsideMenuForm };