From 572f18cc1a2e8bb0ce20dbdd5ac5b821253e7d83 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 28 Mar 2025 15:22:14 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EB=93=B1=EB=A1=9D=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/menu/api/organization-service.ts | 17 ++ apps/cs/src/pages/menu/lib/utils.ts | 68 ++++++- .../menu/model/organization-interface.ts | 18 ++ .../menu/ui/dialog/organization-dialog.tsx | 182 ++++++++++++++++++ .../pages/menu/ui/form/content-menu-form.tsx | 50 ++++- apps/cs/src/pages/menu/ui/menu-page.tsx | 4 +- apps/cs/src/shared/config/api.ts | 1 + 7 files changed, 331 insertions(+), 9 deletions(-) create mode 100644 apps/cs/src/pages/menu/api/organization-service.ts create mode 100644 apps/cs/src/pages/menu/model/organization-interface.ts create mode 100644 apps/cs/src/pages/menu/ui/dialog/organization-dialog.tsx diff --git a/apps/cs/src/pages/menu/api/organization-service.ts b/apps/cs/src/pages/menu/api/organization-service.ts new file mode 100644 index 000000000..9ef3b55fa --- /dev/null +++ b/apps/cs/src/pages/menu/api/organization-service.ts @@ -0,0 +1,17 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + Organization, + OrganizationService, +} from "../model/organization-interface"; + +export class OrganizationApiService implements OrganizationService { + public async getOrganizationUserTreeList(): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_OGNZ}/treeuser`, + ); + return response.data; + } +} + +export const OrganizationApi = new OrganizationApiService(); diff --git a/apps/cs/src/pages/menu/lib/utils.ts b/apps/cs/src/pages/menu/lib/utils.ts index 304c7166a..b1dbafe5f 100644 --- a/apps/cs/src/pages/menu/lib/utils.ts +++ b/apps/cs/src/pages/menu/lib/utils.ts @@ -1,7 +1,10 @@ import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; import type { TreeMenu, TreeNode } from "../model/menu-interface"; +import type { Organization, User } from "../model/organization-interface"; -export const generateTreeData = (data: TreeMenu[]): NodeModel[] => { +export const generateMenuTreeData = ( + data: TreeMenu[], +): NodeModel[] => { return data.reduce[]>((acc, menu) => { const node: NodeModel = { id: menu.menuCd ?? 0, @@ -14,14 +17,14 @@ export const generateTreeData = (data: TreeMenu[]): NodeModel[] => { acc.push(node); if (menu.children?.length) { - acc.push(...generateTreeData(menu.children)); + acc.push(...generateMenuTreeData(menu.children)); } return acc; }, []); }; -export const generateNewTreeData = ( +export const generateNewMenuTreeData = ( treeData: NodeModel[], options: DropOptions, ): TreeNode | null => { @@ -82,7 +85,7 @@ export const handleSelectNode = export const handleDropNode = (callback: (newTree: TreeNode) => void) => (newTree: NodeModel[], options: DropOptions) => { - const newTreeData = generateNewTreeData(newTree, options); + const newTreeData = generateNewMenuTreeData(newTree, options); if (!newTreeData) return; callback(newTreeData); }; @@ -103,3 +106,60 @@ export const handleNodeTextChange = }); callback(newTree); }; + +export const generateOrganizationTreeData = ( + organizations: Organization[], +): NodeModel[] => { + return organizations.reduce[]>((acc, org) => { + const orgNode: NodeModel = { + id: org.ognzId, + parent: org.upOgnzId ?? 0, + text: org.ognzNm, + data: org, + droppable: false, + }; + + acc.push(orgNode); + + if (org.children?.length) { + acc.push(...generateOrganizationTreeData(org.children)); + } + + if (org.users?.length) { + acc.push( + ...org.users.map((user) => ({ + id: user.userUnqId, + parent: org.ognzId, + text: user.userNm, + data: user, + droppable: false, + })), + ); + } + + return acc; + }, []); +}; + +export const filterOrganizationTreeDataByKeyword = ( + treeData: NodeModel[], + keyword: string, +): NodeModel[] => { + return Array.from( + treeData.reduce((matchedNodes, node) => { + if (!node.text.includes(keyword)) return matchedNodes; + + matchedNodes.add(node); + + let parentId = node.parent; + while (parentId) { + const parentNode = treeData.find((n) => n.id === parentId); + if (!parentNode) break; + matchedNodes.add(parentNode); + parentId = parentNode.parent; + } + + return matchedNodes; + }, new Set>()), + ); +}; diff --git a/apps/cs/src/pages/menu/model/organization-interface.ts b/apps/cs/src/pages/menu/model/organization-interface.ts new file mode 100644 index 000000000..86a8b8e31 --- /dev/null +++ b/apps/cs/src/pages/menu/model/organization-interface.ts @@ -0,0 +1,18 @@ +export interface User { + userUnqId: string; + userNm: string; + jbpsSeCd: string; + jbgdSeCd: string; +} + +export interface Organization { + ognzId: string; + upOgnzId: string; + ognzNm: string; + users: User[]; + children: Organization[]; +} + +export interface OrganizationService { + getOrganizationUserTreeList(): Promise; +} diff --git a/apps/cs/src/pages/menu/ui/dialog/organization-dialog.tsx b/apps/cs/src/pages/menu/ui/dialog/organization-dialog.tsx new file mode 100644 index 000000000..47fecb1ba --- /dev/null +++ b/apps/cs/src/pages/menu/ui/dialog/organization-dialog.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { DndProvider } from "react-dnd"; +import { + Tree, + MultiBackend, + getBackendOptions, + type NodeModel, +} from "@minoru/react-dnd-treeview"; +import { + Button, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + Input, +} from "@common/components/ui"; +import { cn } from "@common/components/lib"; +import { ENDPOINT } from "@/shared/config"; +import { OrganizationApi } from "../../api/organization-service"; +import type { Organization, User } from "../../model/organization-interface"; +import { + filterOrganizationTreeDataByKeyword, + generateOrganizationTreeData, +} from "../../lib/utils"; + +type TreeModel = Organization | User; + +interface OrganizationDialogProps { + handleUserSelect: (user: User) => void; +} + +function OrganizationDialog({ handleUserSelect }: OrganizationDialogProps) { + const [keyword, setKeyword] = useState(""); + const [tree, setTree] = useState[]>([]); + const [selectedNode, setSelectedNode] = useState | null>( + null, + ); + + const { data: treeData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.USER_OGNZ], + queryFn: () => OrganizationApi.getOrganizationUserTreeList(), + select: (data) => generateOrganizationTreeData(data), + }); + + const handleSelectNode = (node: NodeModel) => { + setSelectedNode(node); + handleUserSelect(node.data as User); + }; + + useEffect(() => { + setTree(treeData ?? []); + }, [treeData]); + + return ( + + + + + + + 담당자 조회 + +
+
+ setKeyword(e.target.value)} + /> + +
+
+ + , + { depth, isOpen, onToggle }, + ) => ( + + )} + classes={{ + container: "flex flex-col", + }} + sort={false} + onDrop={() => {}} + /> + +
+
+
+
+ ); +} + +interface TreeNodeProps { + node: NodeModel; + depth: number; + isOpen: boolean; + selectedNode: NodeModel | null; + onToggle: (id: NodeModel["id"]) => void; + onSelect: (node: NodeModel) => void; +} + +const ORGANIZATION_PREFIX = "OGNZ"; + +function TreeNode({ + node, + depth, + isOpen, + selectedNode, + onToggle, + onSelect, +}: TreeNodeProps) { + const { id, text, data } = node; + const indent = depth * 15 + 5; + + const handleToggle = () => onToggle(id); + const handleSelect = () => { + if (id.toString().startsWith(ORGANIZATION_PREFIX)) return; + onSelect(node); + }; + + const isSelected = selectedNode?.id === id; + + return ( +
+
+
+ {data && ("children" in data || "users" in data) && ( + + )} + {text} +
+ {isSelected && } +
+
+ ); +} + +export { OrganizationDialog }; diff --git a/apps/cs/src/pages/menu/ui/form/content-menu-form.tsx b/apps/cs/src/pages/menu/ui/form/content-menu-form.tsx index ed16547f8..1e74109f3 100644 --- a/apps/cs/src/pages/menu/ui/form/content-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/form/content-menu-form.tsx @@ -1,5 +1,7 @@ -import { type UseFormReturn } from "react-hook-form"; +import dayjs from "dayjs"; +import type { UseFormReturn } from "react-hook-form"; import { + DatePicker, Label, RadioGroup, RadioGroupItem, @@ -17,7 +19,9 @@ import { } from "@/shared/ui/form"; import type { MenuFormData } from "../../model/menu-interface"; import type { Content } from "../../model/content-interface"; +import type { User } from "../../model/organization-interface"; import { ContentDialog } from "../dialog/content-dialog"; +import { OrganizationDialog } from "../dialog/organization-dialog"; import { BaseTypeMenuForm } from "./base-type-menu-form"; function ContentMenuForm({ form }: { form: UseFormReturn }) { @@ -26,6 +30,16 @@ function ContentMenuForm({ form }: { form: UseFormReturn }) { form.setValue("menuLnkgSnNm", content.contsTtl); }; + const handleUserSelect = (user: User) => { + form.setValue("menuPicUserUnqId", user.userUnqId); + form.setValue("menuPicUserUnqIdNm", user.userNm); + }; + + const handleDateSelect = (date: Date | undefined) => { + if (!date) return; + form.setValue("menuPicDsgnYmd", dayjs(date).format("YYYYMMDD")); + }; + return ( @@ -113,8 +127,38 @@ function ContentMenuForm({ form }: { form: UseFormReturn }) { 담당자 -
- {form.watch("menuLnkgSn")} +
+
+
+ {form.watch("menuPicUserUnqIdNm")} +
+ +
+
+ 지정일: + ( + + + { + field.onChange(date); + handleDateSelect(date); + }} + /> + + + )} + /> +
diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index c1a83ab3e..6a9580856 100644 --- a/apps/cs/src/pages/menu/ui/menu-page.tsx +++ b/apps/cs/src/pages/menu/ui/menu-page.tsx @@ -28,7 +28,7 @@ import { } from "../model/menu-interface"; import { MenuTree } from "./menu-tree"; import { MenuForm } from "./form/menu-form"; -import { generateTreeData } from "../lib/utils"; +import { generateMenuTreeData } from "../lib/utils"; function MenuPage() { const queryClient = useQueryClient(); @@ -51,7 +51,7 @@ function MenuPage() { queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId], queryFn: () => MenuApi.getTreeMenuList(siteId), select: (data) => { - const treeData = generateTreeData(data); + const treeData = generateMenuTreeData(data); const defaultSortSeq = treeData.filter((node) => node.data?.levelNo === 1).length + 1; form.setValue("sortSeq", defaultSortSeq); diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index b938ceaa9..f57b79adf 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -15,6 +15,7 @@ export const ENDPOINT = { DOMAINS: "/cms-service/api/v1/site-dmns", MENUS: "/cms-service/api/v1/menus", CONTENTS: "/cms-service/api/v1/contents", + USER_OGNZ: "/cms-service/api/v1/user-ognz", }, };