feat: 트리 메뉴 dnd 작업
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { Menu, MenuFormData, MenuService } from "../model/menu-interface";
|
||||
import type { Menu, MenuService, TreeMenu } from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
public async getMenuList(siteId: string): Promise<Menu[]> {
|
||||
@@ -10,9 +10,9 @@ export class MenuApiService implements MenuService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async modifyMenu(menuCd: number, data: MenuFormData): Promise<void> {
|
||||
public async modifyMenu(siteId: string, data: TreeMenu[]): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type { Menu, TreeMenu } from "../model/menu-interface";
|
||||
|
||||
export const generateTreeData = (data: Menu[]): NodeModel<Menu>[] => {
|
||||
return data.reduce<NodeModel<Menu>[]>((acc, menu) => {
|
||||
const node: NodeModel<Menu> = {
|
||||
id: menu.menuCd ?? 0,
|
||||
parent: menu.parentId ?? 0,
|
||||
text: menu.menuKornNm,
|
||||
droppable: true,
|
||||
data: menu,
|
||||
};
|
||||
|
||||
acc.push(node);
|
||||
|
||||
if (menu.children?.length) {
|
||||
acc.push(...generateTreeData(menu.children));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const generateNewTreeData = (
|
||||
treeData: NodeModel<Menu>[],
|
||||
options: DropOptions<Menu>,
|
||||
): TreeMenu | null => {
|
||||
const { dragSourceId, dragSource, dropTargetId, dropTarget } = options;
|
||||
if (!dragSource?.data) return null;
|
||||
|
||||
const childNodes = treeData.filter(
|
||||
(node) => node.parent === dragSourceId && node.data,
|
||||
);
|
||||
|
||||
const baseNode = (node: NodeModel<Menu>): TreeMenu => {
|
||||
if (!node.data) return {};
|
||||
return {
|
||||
menuCd: node.data?.menuCd,
|
||||
name: node.data?.menuKornNm,
|
||||
sortSeq: node.data?.sortSeq,
|
||||
icon: node.data?.iconNm,
|
||||
};
|
||||
};
|
||||
|
||||
const treeMenu: TreeMenu = {
|
||||
...baseNode(dragSource),
|
||||
parentId: dropTargetId === 0 ? null : (dropTargetId as number),
|
||||
levelNo: (dropTarget?.data?.levelNo ?? 0) + 1,
|
||||
};
|
||||
|
||||
const result: TreeMenu = {
|
||||
...treeMenu,
|
||||
children: childNodes.map((child) => ({
|
||||
...baseNode(child),
|
||||
parentId: child.data?.parentId === 0 ? null : (dropTargetId as number),
|
||||
levelNo: (treeMenu?.levelNo ?? 0) + 1,
|
||||
})),
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -11,15 +11,21 @@ export const MENU_TYPE = {
|
||||
|
||||
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
|
||||
|
||||
export interface Menu {
|
||||
menuCd: number;
|
||||
export interface TreeMenu {
|
||||
menuCd?: number;
|
||||
name?: string;
|
||||
sortSeq?: number;
|
||||
parentId?: number | null;
|
||||
levelNo?: number;
|
||||
icon?: string;
|
||||
children?: TreeMenu[];
|
||||
}
|
||||
|
||||
export interface Menu extends Omit<TreeMenu, "name" | "icon" | "children"> {
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number;
|
||||
sortSeq: number;
|
||||
iconNm: string;
|
||||
children: Menu[];
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
@@ -59,5 +65,5 @@ export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||
|
||||
export interface MenuService {
|
||||
getMenuList: (siteId: string) => Promise<Menu[]>;
|
||||
modifyMenu: (menuCd: number, data: MenuFormData) => Promise<void>;
|
||||
modifyMenu: (siteId: string, data: TreeMenu[]) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../model/menu-interface";
|
||||
import { MenuTree } from "./menu-tree";
|
||||
import { MenuForm } from "./menu-form";
|
||||
import { generateTreeData } from "../lib/utils";
|
||||
|
||||
function MenuPage() {
|
||||
const [siteId, setSiteId] = useState("");
|
||||
@@ -30,14 +31,7 @@ function MenuPage() {
|
||||
const { data: treeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
queryFn: () => MenuApi.getMenuList(siteId),
|
||||
select: (data) =>
|
||||
data.map((menu) => ({
|
||||
id: menu.menuCd,
|
||||
parent: menu.parentId ?? 0,
|
||||
text: menu.menuKornNm,
|
||||
droppable: true,
|
||||
data: menu,
|
||||
})),
|
||||
select: (data) => generateTreeData(data),
|
||||
enabled: !!siteId,
|
||||
});
|
||||
|
||||
@@ -91,7 +85,7 @@ function MenuPage() {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border !mx-0">
|
||||
<MenuTree treeData={treeData ?? []} />
|
||||
<MenuTree treeData={treeData ?? []} siteId={siteId} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Tree,
|
||||
MultiBackend,
|
||||
@@ -12,16 +12,26 @@ import {
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import {
|
||||
menuFormSchema,
|
||||
type Menu,
|
||||
type MenuFormData,
|
||||
} from "../model/menu-interface";
|
||||
import type { Menu, TreeMenu } from "../model/menu-interface";
|
||||
import { generateNewTreeData } from "../lib/utils";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
type MenuTreeProps = {
|
||||
treeData: NodeModel<Menu>[];
|
||||
siteId: string;
|
||||
};
|
||||
|
||||
function MenuTree({ treeData, siteId }: MenuTreeProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
function MenuTree({ treeData }: { treeData: NodeModel<Menu>[] }) {
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: ({ menuCd, data }: { menuCd: number; data: MenuFormData }) =>
|
||||
MenuApi.modifyMenu(menuCd, data),
|
||||
mutationFn: ({ data }: { data: TreeMenu[] }) =>
|
||||
MenuApi.modifyMenu(siteId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedNode, setSelectedNode] = useState<NodeModel<Menu> | null>(
|
||||
@@ -34,20 +44,9 @@ function MenuTree({ treeData }: { treeData: NodeModel<Menu>[] }) {
|
||||
newTree: NodeModel<Menu>[],
|
||||
options: DropOptions<Menu>,
|
||||
) => {
|
||||
let saveData: MenuFormData | null = null;
|
||||
const dragSourceNode = findNode(options.dragSourceId) ?? options.dragSource;
|
||||
const dropTargetNode = findNode(options.dropTargetId) ?? options.dropTarget;
|
||||
|
||||
if (!dropTargetNode) return;
|
||||
|
||||
saveData = menuFormSchema.parse({
|
||||
...dragSourceNode?.data,
|
||||
});
|
||||
console.log(saveData);
|
||||
};
|
||||
|
||||
const findNode = (id?: NodeModel["id"]) => {
|
||||
return id ? treeData.find((node) => node.id === id) : null;
|
||||
const newTreeData = generateNewTreeData(newTree, options);
|
||||
if (!newTreeData) return;
|
||||
mutate({ data: [newTreeData] });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -67,7 +66,7 @@ function MenuTree({ treeData }: { treeData: NodeModel<Menu>[] }) {
|
||||
)}
|
||||
onDrop={handleDrop}
|
||||
classes={{
|
||||
root: "",
|
||||
root: "p-4",
|
||||
container: "flex flex-col",
|
||||
listItem: "",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
@@ -96,8 +95,8 @@ function MenuNode({
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: MenuNodeProps) {
|
||||
const { droppable, id, text } = node;
|
||||
const indent = depth * 24 + 5;
|
||||
const { id, text, data } = node;
|
||||
const indent = depth * 10 + 5;
|
||||
|
||||
const handleToggle = () => onToggle(id);
|
||||
const handleSelect = () => onSelect(node);
|
||||
@@ -112,10 +111,10 @@ function MenuNode({
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{droppable && (
|
||||
{data?.children?.length && (
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward px-2 text-label",
|
||||
"diveicon di-arrow-forward mr-2 text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
|
||||
Reference in New Issue
Block a user