feat: 메뉴 등록 및 수정 작업
This commit is contained in:
@@ -5,9 +5,17 @@ import type {
|
|||||||
MenuFormData,
|
MenuFormData,
|
||||||
MenuService,
|
MenuService,
|
||||||
TreeMenu,
|
TreeMenu,
|
||||||
|
TreeNode,
|
||||||
} from "../model/menu-interface";
|
} from "../model/menu-interface";
|
||||||
|
|
||||||
export class MenuApiService implements MenuService {
|
export class MenuApiService implements MenuService {
|
||||||
|
public async getMenuInfo(menuCd: number): Promise<Menu> {
|
||||||
|
const response = await client.get<Menu>(
|
||||||
|
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
public async createMenu(data: MenuFormData): Promise<void> {
|
public async createMenu(data: MenuFormData): Promise<void> {
|
||||||
const response = await client.post<void>(
|
const response = await client.post<void>(
|
||||||
`${ENDPOINT.CMS_SERVICE.MENUS}`,
|
`${ENDPOINT.CMS_SERVICE.MENUS}`,
|
||||||
@@ -27,14 +35,14 @@ export class MenuApiService implements MenuService {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTreeMenuList(siteId: string): Promise<Menu[]> {
|
public async getTreeMenuList(siteId: string): Promise<TreeMenu[]> {
|
||||||
const response = await client.get<Menu[]>(
|
const response = await client.get<TreeMenu[]>(
|
||||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async modifyTreeMenu(siteId: string, data: TreeMenu[]): Promise<void> {
|
public async modifyTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
|
||||||
const response = await client.put<void>(
|
const response = await client.put<void>(
|
||||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview";
|
import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview";
|
||||||
import type { Menu, TreeMenu } from "../model/menu-interface";
|
import type { TreeMenu, TreeNode } from "../model/menu-interface";
|
||||||
|
|
||||||
export const generateTreeData = (data: Menu[]): NodeModel<Menu>[] => {
|
export const generateTreeData = (data: TreeMenu[]): NodeModel<TreeMenu>[] => {
|
||||||
return data.reduce<NodeModel<Menu>[]>((acc, menu) => {
|
return data.reduce<NodeModel<TreeMenu>[]>((acc, menu) => {
|
||||||
const node: NodeModel<Menu> = {
|
const node: NodeModel<TreeMenu> = {
|
||||||
id: menu.menuCd ?? 0,
|
id: menu.menuCd ?? 0,
|
||||||
parent: menu.parentId ?? 0,
|
parent: menu.parentId ?? 0,
|
||||||
text: menu.menuKornNm,
|
text: menu.menuKornNm,
|
||||||
@@ -22,9 +22,9 @@ export const generateTreeData = (data: Menu[]): NodeModel<Menu>[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateNewTreeData = (
|
export const generateNewTreeData = (
|
||||||
treeData: NodeModel<Menu>[],
|
treeData: NodeModel<TreeMenu>[],
|
||||||
options: DropOptions<Menu>,
|
options: DropOptions<TreeMenu>,
|
||||||
): TreeMenu | null => {
|
): TreeNode | null => {
|
||||||
const { dragSourceId, dragSource, dropTargetId, dropTarget } = options;
|
const { dragSourceId, dragSource, dropTargetId, dropTarget } = options;
|
||||||
|
|
||||||
if (!dragSource?.data) return null;
|
if (!dragSource?.data) return null;
|
||||||
@@ -38,7 +38,7 @@ export const generateNewTreeData = (
|
|||||||
(node) => node.parent === dragSourceId && node.data,
|
(node) => node.parent === dragSourceId && node.data,
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseNode = (node: NodeModel<Menu>): TreeMenu => {
|
const baseNode = (node: NodeModel<TreeMenu>): TreeNode => {
|
||||||
if (!node.data) return {};
|
if (!node.data) return {};
|
||||||
return {
|
return {
|
||||||
menuCd: node.data?.menuCd,
|
menuCd: node.data?.menuCd,
|
||||||
@@ -47,20 +47,20 @@ export const generateNewTreeData = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const treeMenu: TreeMenu = {
|
const treeNode: TreeNode = {
|
||||||
...baseNode(dragSource),
|
...baseNode(dragSource),
|
||||||
parentId: dropTargetId === 0 ? null : (dropTargetId as number),
|
parentId: dropTargetId === 0 ? null : (dropTargetId as number),
|
||||||
sortSeq: newSortSeq,
|
sortSeq: newSortSeq,
|
||||||
levelNo: (dropTarget?.data?.levelNo ?? 0) + 1,
|
levelNo: (dropTarget?.data?.levelNo ?? 0) + 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result: TreeMenu = {
|
const result: TreeNode = {
|
||||||
...treeMenu,
|
...treeNode,
|
||||||
children: childNodes.map((child) => ({
|
children: childNodes.map((child) => ({
|
||||||
...baseNode(child),
|
...baseNode(child),
|
||||||
parentId: child.data?.parentId === 0 ? null : (dropTargetId as number),
|
parentId: child.data?.parentId === 0 ? null : (dropTargetId as number),
|
||||||
sortSeq: child.data?.sortSeq,
|
sortSeq: child.data?.sortSeq,
|
||||||
levelNo: (treeMenu?.levelNo ?? 0) + 1,
|
levelNo: (treeNode?.levelNo ?? 0) + 1,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,21 +68,22 @@ export const generateNewTreeData = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const handleSelectNode =
|
export const handleSelectNode =
|
||||||
(callback: (node: NodeModel<Menu>) => void) => (node: NodeModel<Menu>) => {
|
(callback: (node: NodeModel<TreeMenu>) => void) =>
|
||||||
|
(node: NodeModel<TreeMenu>) => {
|
||||||
callback(node);
|
callback(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleDropNode =
|
export const handleDropNode =
|
||||||
(callback: (newTree: TreeMenu) => void) =>
|
(callback: (newTree: TreeNode) => void) =>
|
||||||
(newTree: NodeModel<Menu>[], options: DropOptions<Menu>) => {
|
(newTree: NodeModel<TreeMenu>[], options: DropOptions<TreeMenu>) => {
|
||||||
const newTreeData = generateNewTreeData(newTree, options);
|
const newTreeData = generateNewTreeData(newTree, options);
|
||||||
if (!newTreeData) return;
|
if (!newTreeData) return;
|
||||||
callback(newTreeData);
|
callback(newTreeData);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleNodeTextChange =
|
export const handleNodeTextChange =
|
||||||
(treeData: NodeModel<Menu>[]) =>
|
(treeData: NodeModel<TreeMenu>[]) =>
|
||||||
(callback: (newTree: NodeModel<Menu>[]) => void) =>
|
(callback: (newTree: NodeModel<TreeMenu>[]) => void) =>
|
||||||
(id: NodeModel["id"], value: string) => {
|
(id: NodeModel["id"], value: string) => {
|
||||||
const newTree = treeData.map((node) => {
|
const newTree = treeData.map((node) => {
|
||||||
if (node.id === id) {
|
if (node.id === id) {
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const MENU_TYPE = {
|
export const MENU_TYPE = {
|
||||||
EMPTY: "EMPTY",
|
EMPTY: "empty",
|
||||||
CONTENTS: "CONTENTS",
|
CONTENTS: "contents",
|
||||||
BOARD: "BOARD",
|
BOARD: "board",
|
||||||
INSIDE: "INSIDE",
|
INSIDE: "inside",
|
||||||
OUTSIDE: "OUTSIDE",
|
OUTSIDE: "outside",
|
||||||
BOTTOM: "BOTTOM",
|
BOTTOM: "bottom",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
|
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
|
||||||
|
|
||||||
export interface TreeMenu {
|
export interface TreeNode {
|
||||||
menuCd?: number;
|
menuCd?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
sortSeq?: number;
|
sortSeq?: number;
|
||||||
parentId?: number | null;
|
parentId?: number | null;
|
||||||
levelNo?: number;
|
levelNo?: number;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
children?: TreeMenu[];
|
children?: TreeNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Menu extends Omit<TreeMenu, "name" | "icon" | "children"> {
|
export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
|
||||||
menuKornNm: string;
|
menuKornNm: string;
|
||||||
menuEngNm: string;
|
menuEngNm: string;
|
||||||
iconNm: string;
|
iconNm: string;
|
||||||
children: Menu[];
|
children: TreeMenu[];
|
||||||
urlPath: string;
|
urlPath: string;
|
||||||
menuLnkgTypeCd: string;
|
menuLnkgTypeCd: string;
|
||||||
menuLnkgSn: number;
|
menuLnkgSn: number;
|
||||||
@@ -53,12 +53,12 @@ export const menuFormSchema = z.object({
|
|||||||
MENU_TYPE.BOTTOM,
|
MENU_TYPE.BOTTOM,
|
||||||
]),
|
]),
|
||||||
menuLnkgSn: z.number().nullish(),
|
menuLnkgSn: z.number().nullish(),
|
||||||
otptYn: z.string().default("Y").nullish(),
|
otptYn: z.string().default("Y"),
|
||||||
prntMenuCd: z.number().nullish(),
|
prntMenuCd: z.number().nullish(),
|
||||||
sortSeq: z.number(),
|
sortSeq: z.number(),
|
||||||
evlUseYn: z.string().default("Y").nullish(),
|
evlUseYn: z.string().default("Y"),
|
||||||
qrcd: z.string().nullish(),
|
qrcd: z.string().nullish(),
|
||||||
useYn: z.string().default("Y").nullish(),
|
useYn: z.string().default("Y"),
|
||||||
menuLnkgSnNm: z.string().nullish(),
|
menuLnkgSnNm: z.string().nullish(),
|
||||||
menuPicUserUnqId: z.string().nullish(),
|
menuPicUserUnqId: z.string().nullish(),
|
||||||
menuPicUserUnqIdNm: z.string().nullish(),
|
menuPicUserUnqIdNm: z.string().nullish(),
|
||||||
@@ -66,14 +66,16 @@ export const menuFormSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||||
|
export type Menu = MenuFormData & { menuCd: number };
|
||||||
|
|
||||||
export interface MenuService {
|
export interface MenuService {
|
||||||
|
getMenuInfo: (menuCd: number) => Promise<Menu>;
|
||||||
createMenu: (data: MenuFormData) => Promise<void>;
|
createMenu: (data: MenuFormData) => Promise<void>;
|
||||||
modifyMenu: (
|
modifyMenu: (
|
||||||
menuCd: number,
|
menuCd: number,
|
||||||
data: Omit<MenuFormData, "siteId">,
|
data: Omit<MenuFormData, "siteId">,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getTreeMenuList: (siteId: string) => Promise<Menu[]>;
|
getTreeMenuList: (siteId: string) => Promise<TreeMenu[]>;
|
||||||
modifyTreeMenu: (siteId: string, data: TreeMenu[]) => Promise<void>;
|
modifyTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
|
||||||
modifyMenuName: (menuCd: number, name: string) => Promise<void>;
|
modifyMenuName: (menuCd: number, name: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,22 +48,22 @@ function MenuForm({
|
|||||||
|
|
||||||
const onSubmit = async (data: MenuFormData) => {
|
const onSubmit = async (data: MenuFormData) => {
|
||||||
if (isModifyMode) {
|
if (isModifyMode) {
|
||||||
onModify(data);
|
await onModify(data);
|
||||||
} else {
|
} else {
|
||||||
onCreate(data);
|
await onCreate(data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCreate = (data: MenuFormData) => {
|
const onCreate = async (data: MenuFormData) => {
|
||||||
handleConfirm(async () => {
|
await handleConfirm(async () => {
|
||||||
if (await confirm("저장하시겠습니까?")) {
|
if (await confirm("저장하시겠습니까?")) {
|
||||||
handleCreate(data);
|
handleCreate(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onModify = (data: MenuFormData) => {
|
const onModify = async (data: MenuFormData) => {
|
||||||
handleConfirm(async () => {
|
await handleConfirm(async () => {
|
||||||
if (await confirm("수정하시겠습니까?")) {
|
if (await confirm("수정하시겠습니까?")) {
|
||||||
handleModify({ menuCd: menuCd!, data });
|
handleModify({ menuCd: menuCd!, data });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import { Button, Input } from "@common/components/ui";
|
|||||||
import { cn } from "@common/components/lib";
|
import { cn } from "@common/components/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { MenuApi } from "../api/menu-service";
|
import { MenuApi } from "../api/menu-service";
|
||||||
import type { Menu } from "../model/menu-interface";
|
import type { TreeMenu } from "../model/menu-interface";
|
||||||
|
|
||||||
type MenuNodeProps = {
|
type MenuNodeProps = {
|
||||||
node: NodeModel<Menu>;
|
node: NodeModel<TreeMenu>;
|
||||||
depth: number;
|
depth: number;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onToggle: (id: NodeModel["id"]) => void;
|
onToggle: (id: NodeModel["id"]) => void;
|
||||||
onSelect: (node: NodeModel<Menu>) => void;
|
onSelect: (node: NodeModel<TreeMenu>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function MenuNode({
|
function MenuNode({
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ 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 { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||||
import type { Menu, MenuType } from "../model/menu-interface";
|
import type { TreeMenu } from "../model/menu-interface";
|
||||||
import { MenuApi } from "../api/menu-service";
|
import { MenuApi } from "../api/menu-service";
|
||||||
import {
|
import {
|
||||||
MENU_TYPE,
|
MENU_TYPE,
|
||||||
@@ -33,7 +33,13 @@ function MenuPage() {
|
|||||||
const { setMessage: alert } = useAlertStore((state) => state);
|
const { setMessage: alert } = useAlertStore((state) => state);
|
||||||
|
|
||||||
const [siteId, setSiteId] = useState("");
|
const [siteId, setSiteId] = useState("");
|
||||||
const [selectedMenu, setSelectedMenu] = useState<Menu | null>(null);
|
const [selectedMenu, setSelectedMenu] = useState<TreeMenu | null>(null);
|
||||||
|
|
||||||
|
const { data: menuInfo } = useQuery({
|
||||||
|
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, selectedMenu?.menuCd],
|
||||||
|
queryFn: () => MenuApi.getMenuInfo(selectedMenu?.menuCd ?? 0),
|
||||||
|
enabled: !!selectedMenu?.menuCd,
|
||||||
|
});
|
||||||
|
|
||||||
const { data: treeData } = useQuery({
|
const { data: treeData } = useQuery({
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||||
@@ -91,13 +97,15 @@ function MenuPage() {
|
|||||||
menuLnkgTypeCd: MENU_TYPE.EMPTY,
|
menuLnkgTypeCd: MENU_TYPE.EMPTY,
|
||||||
},
|
},
|
||||||
values: {
|
values: {
|
||||||
...selectedMenu,
|
...menuInfo,
|
||||||
siteId,
|
siteId,
|
||||||
sortSeq: selectedMenu?.sortSeq ?? 1,
|
sortSeq: menuInfo?.sortSeq ?? 1,
|
||||||
levelNo: selectedMenu?.levelNo ?? 1,
|
levelNo: menuInfo?.levelNo ?? 1,
|
||||||
menuKornNm: selectedMenu?.menuKornNm ?? "",
|
menuKornNm: menuInfo?.menuKornNm ?? "",
|
||||||
menuLnkgTypeCd:
|
menuLnkgTypeCd: menuInfo?.menuLnkgTypeCd ?? MENU_TYPE.EMPTY,
|
||||||
(selectedMenu?.menuLnkgTypeCd as MenuType) ?? MENU_TYPE.EMPTY,
|
otptYn: menuInfo?.otptYn ?? "Y",
|
||||||
|
evlUseYn: menuInfo?.evlUseYn ?? "Y",
|
||||||
|
useYn: menuInfo?.useYn ?? "Y",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,7 +114,7 @@ function MenuPage() {
|
|||||||
form.setValue("siteId", value);
|
form.setValue("siteId", value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNodeSelect = (node: NodeModel<Menu>) => {
|
const handleNodeSelect = (node: NodeModel<TreeMenu>) => {
|
||||||
if (!node.data) return;
|
if (!node.data) return;
|
||||||
setSelectedMenu(node.data);
|
setSelectedMenu(node.data);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import {
|
|||||||
} from "@minoru/react-dnd-treeview";
|
} from "@minoru/react-dnd-treeview";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { MenuApi } from "../api/menu-service";
|
import { MenuApi } from "../api/menu-service";
|
||||||
import type { Menu, TreeMenu } from "../model/menu-interface";
|
import type { TreeMenu, TreeNode } from "../model/menu-interface";
|
||||||
import { handleDropNode, handleSelectNode } from "../lib/utils";
|
import { handleDropNode, handleSelectNode } from "../lib/utils";
|
||||||
import { MenuNode } from "./menu-node";
|
import { MenuNode } from "./menu-node";
|
||||||
|
|
||||||
type MenuTreeProps = {
|
type MenuTreeProps = {
|
||||||
treeData: NodeModel<Menu>[];
|
treeData: NodeModel<TreeMenu>[];
|
||||||
siteId: string;
|
siteId: string;
|
||||||
selectedMenu: Menu | null;
|
selectedMenu: TreeMenu | null;
|
||||||
onSelectNode: (node: NodeModel<Menu>) => void;
|
onSelectNode: (node: NodeModel<TreeMenu>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function MenuTree({
|
function MenuTree({
|
||||||
@@ -30,7 +30,7 @@ function MenuTree({
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { mutate: modifyTreeMenu } = useMutation({
|
const { mutate: modifyTreeMenu } = useMutation({
|
||||||
mutationFn: (newTree: TreeMenu) =>
|
mutationFn: (newTree: TreeNode) =>
|
||||||
MenuApi.modifyTreeMenu(siteId, [newTree]),
|
MenuApi.modifyTreeMenu(siteId, [newTree]),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
@@ -44,7 +44,7 @@ function MenuTree({
|
|||||||
<Tree
|
<Tree
|
||||||
tree={treeData}
|
tree={treeData}
|
||||||
rootId={0}
|
rootId={0}
|
||||||
render={(node: NodeModel<Menu>, { depth, isOpen, onToggle }) => (
|
render={(node: NodeModel<TreeMenu>, { depth, isOpen, onToggle }) => (
|
||||||
<MenuNode
|
<MenuNode
|
||||||
node={node}
|
node={node}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
|
|||||||
Reference in New Issue
Block a user