feat: 트리 메뉴 추가 기능 작업
This commit is contained in:
@@ -23,16 +23,16 @@ export interface TreeNode {
|
||||
|
||||
export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
iconNm: string;
|
||||
children: TreeMenu[];
|
||||
urlPath: string;
|
||||
menuEngNm?: string;
|
||||
iconNm?: string;
|
||||
children?: TreeMenu[];
|
||||
urlPath?: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
qrcd: string;
|
||||
menuLnkgSn?: number;
|
||||
qrcd?: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
requiredUrlPath?: boolean;
|
||||
}
|
||||
|
||||
export const menuFormSchema = z.object({
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { Button, Input } from "@common/components/ui";
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button, Input } from "@common/components/ui";
|
||||
|
||||
type EditableNodeProps = {
|
||||
depth?: number;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
} & ComponentProps<typeof Input>;
|
||||
|
||||
function EditableNode({ onSave, onCancel, ...props }: EditableNodeProps) {
|
||||
function EditableNode({
|
||||
depth,
|
||||
onSave,
|
||||
onCancel,
|
||||
...props
|
||||
}: EditableNodeProps) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-sm bg-accent p-2"
|
||||
style={{ marginInlineStart: !depth ? 0 : depth * 5 + 5 }}
|
||||
>
|
||||
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
@@ -28,7 +37,7 @@ function EditableNode({ onSave, onCancel, ...props }: EditableNodeProps) {
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
@@ -13,6 +13,8 @@ type MenuNodeProps = {
|
||||
depth: number;
|
||||
isOpen: boolean;
|
||||
isSelected: boolean;
|
||||
isAdditionalParent: boolean;
|
||||
treeMethods: TreeMethods | null;
|
||||
onToggle: (id: NodeModel["id"]) => void;
|
||||
onSelect: (node: NodeModel<TreeMenu>) => void;
|
||||
};
|
||||
@@ -22,6 +24,8 @@ function MenuNode({
|
||||
depth,
|
||||
isOpen,
|
||||
isSelected,
|
||||
isAdditionalParent,
|
||||
treeMethods,
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: MenuNodeProps) {
|
||||
@@ -58,6 +62,10 @@ function MenuNode({
|
||||
setNodeName(text);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdditionalParent) treeMethods?.open(id);
|
||||
}, [isAdditionalParent]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
|
||||
@@ -34,6 +34,8 @@ function MenuPage() {
|
||||
|
||||
const [siteId, setSiteId] = useState("");
|
||||
const [selectedMenu, setSelectedMenu] = useState<TreeMenu | null>(null);
|
||||
const [additionalNode, setAdditionalNode] =
|
||||
useState<NodeModel<TreeMenu> | null>(null);
|
||||
|
||||
const { data: menuInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, selectedMenu?.menuCd],
|
||||
@@ -65,7 +67,7 @@ function MenuPage() {
|
||||
})),
|
||||
});
|
||||
|
||||
const { mutate: createMenu } = useMutation({
|
||||
const { mutateAsync: createMenu } = useMutation({
|
||||
mutationFn: (data: MenuFormData) => MenuApi.createMenu(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
@@ -99,8 +101,8 @@ function MenuPage() {
|
||||
values: {
|
||||
...menuInfo,
|
||||
siteId,
|
||||
sortSeq: menuInfo?.sortSeq ?? 1,
|
||||
levelNo: menuInfo?.levelNo ?? 1,
|
||||
sortSeq: menuInfo?.sortSeq ?? 1,
|
||||
menuKornNm: menuInfo?.menuKornNm ?? "",
|
||||
menuLnkgTypeCd: menuInfo?.menuLnkgTypeCd ?? MENU_TYPE.EMPTY,
|
||||
otptYn: menuInfo?.otptYn ?? "Y",
|
||||
@@ -109,21 +111,64 @@ function MenuPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const additionalNodeForm = useForm<MenuFormData>({
|
||||
resolver: zodResolver(menuFormSchema),
|
||||
defaultValues: {
|
||||
menuLnkgTypeCd: MENU_TYPE.EMPTY,
|
||||
levelNo: 1,
|
||||
sortSeq: 1,
|
||||
otptYn: "Y",
|
||||
evlUseYn: "Y",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
const handleAdditionalNodeSave = async () => {
|
||||
await createMenu({
|
||||
...additionalNodeForm.getValues(),
|
||||
prntMenuCd: additionalNode?.parent as number,
|
||||
});
|
||||
setAdditionalNode(null);
|
||||
};
|
||||
|
||||
const handleAdditionalNodeCancel = () => {
|
||||
additionalNodeForm.setValue("menuKornNm", "");
|
||||
setAdditionalNode(null);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {};
|
||||
|
||||
const handleSiteIdChange = (value: string) => {
|
||||
setSiteId(value);
|
||||
form.setValue("siteId", value);
|
||||
additionalNodeForm.setValue("siteId", value);
|
||||
|
||||
setSiteId(value);
|
||||
setSelectedMenu(null);
|
||||
};
|
||||
|
||||
const handleNodeSelect = (node: NodeModel<TreeMenu>) => {
|
||||
if (!node.data) return;
|
||||
if (node.data.menuCd === selectedMenu?.menuCd) {
|
||||
setSelectedMenu(null);
|
||||
additionalNodeForm.setValue("levelNo", 1);
|
||||
return;
|
||||
}
|
||||
setSelectedMenu(node.data);
|
||||
additionalNodeForm.setValue("levelNo", (node.data.levelNo ?? 0) + 1);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {};
|
||||
const handleAddMenu = () => {
|
||||
if (additionalNode) return;
|
||||
setAdditionalNode({
|
||||
id: -1,
|
||||
parent: selectedMenu?.menuCd ?? 0,
|
||||
text: "",
|
||||
});
|
||||
};
|
||||
|
||||
const tree = additionalNode
|
||||
? [...(treeData ?? []), ...(additionalNode ? [additionalNode] : [])]
|
||||
: (treeData ?? []);
|
||||
|
||||
return (
|
||||
<PageLayout pageTitle="메뉴 관리">
|
||||
@@ -144,19 +189,32 @@ function MenuPage() {
|
||||
{siteId && (
|
||||
<>
|
||||
<div className="my-4 flex gap-2">
|
||||
<Button size="sm" color="primary" className="flex-1">
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="flex-1"
|
||||
onClick={handleAddMenu}
|
||||
>
|
||||
메뉴 추가
|
||||
</Button>
|
||||
<Button size="sm" color="red" className="flex-1">
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
className="flex-1"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
메뉴 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border !mx-0">
|
||||
<MenuTree
|
||||
treeData={treeData ?? []}
|
||||
tree={tree}
|
||||
siteId={siteId}
|
||||
selectedMenu={selectedMenu}
|
||||
additionalNodeForm={additionalNodeForm}
|
||||
onSelectNode={handleNodeSelect}
|
||||
onAdditionalNodeSave={handleAdditionalNodeSave}
|
||||
onAdditionalNodeCancel={handleAdditionalNodeCancel}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,33 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Tree,
|
||||
MultiBackend,
|
||||
getBackendOptions,
|
||||
type NodeModel,
|
||||
type TreeMethods,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import type { TreeMenu, TreeNode } from "../model/menu-interface";
|
||||
import type { MenuFormData, TreeMenu, TreeNode } from "../model/menu-interface";
|
||||
import { handleDropNode, handleSelectNode } from "../lib/utils";
|
||||
import { MenuNode } from "./menu-node";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
type MenuTreeProps = {
|
||||
treeData: NodeModel<TreeMenu>[];
|
||||
tree: NodeModel<TreeMenu>[];
|
||||
siteId: string;
|
||||
selectedMenu: TreeMenu | null;
|
||||
additionalNodeForm: UseFormReturn<MenuFormData>;
|
||||
onSelectNode: (node: NodeModel<TreeMenu>) => void;
|
||||
onAdditionalNodeSave: () => void;
|
||||
onAdditionalNodeCancel: () => void;
|
||||
};
|
||||
|
||||
function MenuTree({
|
||||
treeData,
|
||||
tree,
|
||||
siteId,
|
||||
selectedMenu,
|
||||
additionalNodeForm,
|
||||
onSelectNode,
|
||||
onAdditionalNodeSave,
|
||||
onAdditionalNodeCancel,
|
||||
}: MenuTreeProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const treeMethods = useRef<TreeMethods>(null);
|
||||
|
||||
const { mutate: modifyTreeMenu } = useMutation({
|
||||
mutationFn: (newTree: TreeNode) =>
|
||||
@@ -39,21 +50,46 @@ function MenuTree({
|
||||
},
|
||||
});
|
||||
|
||||
const additionalNode = tree.find((n) => n.id === -1);
|
||||
|
||||
const isAdditionalParent = (node: NodeModel<TreeMenu>): boolean => {
|
||||
if (!additionalNode) return false;
|
||||
return additionalNode?.parent === node.id;
|
||||
};
|
||||
|
||||
return (
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={treeData}
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
render={(node: NodeModel<TreeMenu>, { depth, isOpen, onToggle }) => (
|
||||
<MenuNode
|
||||
node={node}
|
||||
depth={depth}
|
||||
isOpen={isOpen}
|
||||
isSelected={selectedMenu?.menuCd === node.id}
|
||||
onToggle={onToggle}
|
||||
onSelect={handleSelectNode(onSelectNode)}
|
||||
/>
|
||||
)}
|
||||
ref={treeMethods}
|
||||
render={(node: NodeModel<TreeMenu>, { depth, isOpen, onToggle }) => {
|
||||
if (node.id === -1) {
|
||||
return (
|
||||
<EditableNode
|
||||
value={additionalNodeForm.watch("menuKornNm") ?? ""}
|
||||
onChange={(e) =>
|
||||
additionalNodeForm.setValue("menuKornNm", e.target.value)
|
||||
}
|
||||
depth={depth}
|
||||
onSave={onAdditionalNodeSave}
|
||||
onCancel={onAdditionalNodeCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MenuNode
|
||||
node={node}
|
||||
depth={depth}
|
||||
treeMethods={treeMethods.current}
|
||||
isOpen={isOpen}
|
||||
isSelected={selectedMenu?.menuCd === node.id}
|
||||
isAdditionalParent={isAdditionalParent(node)}
|
||||
onToggle={onToggle}
|
||||
onSelect={handleSelectNode(onSelectNode)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
onDrop={handleDropNode(modifyTreeMenu)}
|
||||
classes={{
|
||||
root: "p-4",
|
||||
|
||||
Reference in New Issue
Block a user