From e337854904d60816205b23c9a40b0c0a6e895918 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 17 Jul 2025 15:39:34 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[apps/cs]=20chore:=20steiger=20lint=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/apilog/ui/apilog-page.tsx | 3 +-- apps/cs/src/pages/loginlog/ui/loginlog-page.tsx | 3 +-- apps/cs/src/pages/privacylog/ui/privacylog-page.tsx | 6 ++++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/cs/src/pages/apilog/ui/apilog-page.tsx b/apps/cs/src/pages/apilog/ui/apilog-page.tsx index cbcb3dc9a..532752840 100644 --- a/apps/cs/src/pages/apilog/ui/apilog-page.tsx +++ b/apps/cs/src/pages/apilog/ui/apilog-page.tsx @@ -15,8 +15,7 @@ import { SelectTrigger, SelectValue, } from "@common/components"; -import { ApiLogApi } from "@/entities/apilog"; -import { type ApiLogParams } from "@/entities/apilog/model/apilog-interface"; +import { ApiLogApi, type ApiLogParams } from "@/entities/apilog"; import { useDataTable } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { columns } from "@/pages/apilog/model/table-columns"; diff --git a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx index cbfb53a07..4bad7cba6 100644 --- a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx +++ b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx @@ -16,8 +16,7 @@ import { SelectValue, LoadingWithImage, } from "@common/components"; -import { LoginlogApi } from "@/entities/loginlog"; -import { type LoginlogParams } from "@/entities/loginlog/model/loginlog-interface"; +import { LoginlogApi, type LoginlogParams } from "@/entities/loginlog"; import { useDataTable } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { columns } from "@/pages/loginlog/model/table-columns"; diff --git a/apps/cs/src/pages/privacylog/ui/privacylog-page.tsx b/apps/cs/src/pages/privacylog/ui/privacylog-page.tsx index 8207ca9e6..c063fb683 100644 --- a/apps/cs/src/pages/privacylog/ui/privacylog-page.tsx +++ b/apps/cs/src/pages/privacylog/ui/privacylog-page.tsx @@ -15,8 +15,10 @@ import { SelectTrigger, SelectValue, } from "@common/components"; -import { PrivacyLogPrivacy } from "@/entities/privacylog"; -import type { PrivacyLogParams } from "@/entities/privacylog/model/privacylog-interface"; +import { + PrivacyLogPrivacy, + type PrivacyLogParams, +} from "@/entities/privacylog"; import { useDataTable } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { columns } from "@/pages/privacylog/model/table-columns"; From f15dd02cc99406fa039ac597760235385735182e Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 17 Jul 2025 16:19:36 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[apps/cs]=20refactor:=20=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=20=EB=A9=94=EB=89=B4=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu => entities/tree-menu}/api/tree-menu-service.ts | 0 apps/cs/src/entities/tree-menu/index.ts | 2 ++ .../tree-menu}/model/tree-menu-interface.ts | 0 apps/cs/src/pages/menu/lib/utils.ts | 2 +- apps/cs/src/pages/menu/ui/menu-node.tsx | 3 +-- apps/cs/src/pages/menu/ui/menu-page.tsx | 3 +-- apps/cs/src/pages/menu/ui/menu-tree.tsx | 7 +++++-- 7 files changed, 10 insertions(+), 7 deletions(-) rename apps/cs/src/{pages/menu => entities/tree-menu}/api/tree-menu-service.ts (100%) create mode 100644 apps/cs/src/entities/tree-menu/index.ts rename apps/cs/src/{pages/menu => entities/tree-menu}/model/tree-menu-interface.ts (100%) diff --git a/apps/cs/src/pages/menu/api/tree-menu-service.ts b/apps/cs/src/entities/tree-menu/api/tree-menu-service.ts similarity index 100% rename from apps/cs/src/pages/menu/api/tree-menu-service.ts rename to apps/cs/src/entities/tree-menu/api/tree-menu-service.ts diff --git a/apps/cs/src/entities/tree-menu/index.ts b/apps/cs/src/entities/tree-menu/index.ts new file mode 100644 index 000000000..80ddcfe08 --- /dev/null +++ b/apps/cs/src/entities/tree-menu/index.ts @@ -0,0 +1,2 @@ +export { TreeMenuApi } from "./api/tree-menu-service"; +export type { TreeMenu, TreeNode } from "./model/tree-menu-interface"; diff --git a/apps/cs/src/pages/menu/model/tree-menu-interface.ts b/apps/cs/src/entities/tree-menu/model/tree-menu-interface.ts similarity index 100% rename from apps/cs/src/pages/menu/model/tree-menu-interface.ts rename to apps/cs/src/entities/tree-menu/model/tree-menu-interface.ts diff --git a/apps/cs/src/pages/menu/lib/utils.ts b/apps/cs/src/pages/menu/lib/utils.ts index a5cb921a6..c9b06dd72 100644 --- a/apps/cs/src/pages/menu/lib/utils.ts +++ b/apps/cs/src/pages/menu/lib/utils.ts @@ -3,7 +3,7 @@ import type { TreeUserOrganization, TreeUser, } from "@/entities/user-organization"; -import type { TreeMenu, TreeNode } from "../model/tree-menu-interface"; +import type { TreeMenu, TreeNode } from "@/entities/tree-menu"; export const generateMenuTreeData = ( data: TreeMenu[], diff --git a/apps/cs/src/pages/menu/ui/menu-node.tsx b/apps/cs/src/pages/menu/ui/menu-node.tsx index 4a2b3624f..d035b8c0b 100644 --- a/apps/cs/src/pages/menu/ui/menu-node.tsx +++ b/apps/cs/src/pages/menu/ui/menu-node.tsx @@ -2,10 +2,9 @@ import { useEffect, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview"; import { Button, cn } from "@common/components"; +import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu"; import { useMutationWrapper } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; -import { TreeMenuApi } from "../api/tree-menu-service"; -import type { TreeMenu } from "../model/tree-menu-interface"; import { EditableNode } from "./editable-node"; type MenuNodeProps = { diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index 2d3dedc16..d02b1b700 100644 --- a/apps/cs/src/pages/menu/ui/menu-page.tsx +++ b/apps/cs/src/pages/menu/ui/menu-page.tsx @@ -21,11 +21,10 @@ import { type MenuFormData, } from "@/entities/menu"; import { SiteApi } from "@/entities/site"; +import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { generateMenuTreeData } from "../lib/utils"; -import { TreeMenuApi } from "../api/tree-menu-service"; -import type { TreeMenu } from "../model/tree-menu-interface"; import { MenuTree } from "./menu-tree"; import { MenuForm } from "./menu-form"; diff --git a/apps/cs/src/pages/menu/ui/menu-tree.tsx b/apps/cs/src/pages/menu/ui/menu-tree.tsx index 5ef15062c..36a7505c9 100644 --- a/apps/cs/src/pages/menu/ui/menu-tree.tsx +++ b/apps/cs/src/pages/menu/ui/menu-tree.tsx @@ -12,8 +12,11 @@ import { Button } from "@common/components"; import { ENDPOINT } from "@/shared/config"; import { useMutationWrapper } from "@/shared/lib"; import type { MenuFormData } from "@/entities/menu"; -import { TreeMenuApi } from "../api/tree-menu-service"; -import type { TreeMenu, TreeNode } from "../model/tree-menu-interface"; +import { + TreeMenuApi, + type TreeMenu, + type TreeNode, +} from "@/entities/tree-menu"; import { handleDropNode, handleSelectNode } from "../lib/utils"; import { MenuNode } from "./menu-node"; import { EditableNode } from "./editable-node"; From 617eb217e186f9d3a2b73a517170d04cac52a180 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Jul 2025 00:25:16 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[apps/cs]=20feat:=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=EB=A7=B5=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/menu/api/menu-service.ts | 9 ++ apps/cs/src/entities/menu/index.ts | 3 + .../src/entities/menu/model/menu-interface.ts | 14 +- .../tree-menu/model/tree-menu-interface.ts | 1 + apps/cs/src/pages/site/lib/utils.ts | 24 ++++ .../cs/src/pages/site/model/table-columns.tsx | 103 ++++++++++++++ apps/cs/src/pages/site/ui/editable-node.tsx | 45 ++++++ apps/cs/src/pages/site/ui/menu-node.tsx | 128 ++++++++++++++++++ apps/cs/src/pages/site/ui/menu-tree.tsx | 70 ++++++++++ 9 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 apps/cs/src/pages/site/lib/utils.ts create mode 100644 apps/cs/src/pages/site/ui/editable-node.tsx create mode 100644 apps/cs/src/pages/site/ui/menu-node.tsx create mode 100644 apps/cs/src/pages/site/ui/menu-tree.tsx diff --git a/apps/cs/src/entities/menu/api/menu-service.ts b/apps/cs/src/entities/menu/api/menu-service.ts index 33ad06837..07e9e552a 100644 --- a/apps/cs/src/entities/menu/api/menu-service.ts +++ b/apps/cs/src/entities/menu/api/menu-service.ts @@ -6,6 +6,7 @@ import type { MenuItem, MenuFormData, MenuTemporarilyStoredData, + SiteMapFormData, } from "../model/menu-interface"; export class MenuApiService implements MenuService { @@ -65,6 +66,14 @@ export class MenuApiService implements MenuService { ); return response.data; } + + public async publishSiteMap(data: SiteMapFormData["siteMap"]): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENUS}/sitemap/publish`, + data, + ); + return response.data; + } } export const MenuApi = new MenuApiService(); diff --git a/apps/cs/src/entities/menu/index.ts b/apps/cs/src/entities/menu/index.ts index e4a9cec40..e4c61dbb7 100644 --- a/apps/cs/src/entities/menu/index.ts +++ b/apps/cs/src/entities/menu/index.ts @@ -2,8 +2,11 @@ export { MenuApi } from "./api/menu-service"; export { MENU_TYPE, menuFormSchema, + siteMapFormSchema, type MenuType, type MenuItem, type MenuFormData, + type SiteMapFormData, + type SiteMapPayloadData, } from "./model/menu-interface"; export { MenuWrapper } from "./lib/menu-wrapper"; diff --git a/apps/cs/src/entities/menu/model/menu-interface.ts b/apps/cs/src/entities/menu/model/menu-interface.ts index b1693f616..0c5f4b54c 100644 --- a/apps/cs/src/entities/menu/model/menu-interface.ts +++ b/apps/cs/src/entities/menu/model/menu-interface.ts @@ -72,8 +72,19 @@ export const menuFormSchema = z.object({ menuPicDsgnYmd: z.string().nullish(), }); -export type MenuFormData = z.infer; +export const siteMapPayloadSchema = z.object({ + menuCd: z.number(), + menuSubKornNm: z.string(), + otptYn: z.string(), +}); +export const siteMapFormSchema = z.object({ + siteMap: z.array(siteMapPayloadSchema), +}); + +export type MenuFormData = z.infer; +export type SiteMapFormData = z.infer; +export type SiteMapPayloadData = z.infer; export type Menu = Omit & { menuCd: number; iconName?: string; @@ -92,4 +103,5 @@ export interface MenuService { getMenuTemporarilyStoredData: ( menuCd: number, ) => Promise; + publishSiteMap: (data: SiteMapFormData["siteMap"]) => Promise; } diff --git a/apps/cs/src/entities/tree-menu/model/tree-menu-interface.ts b/apps/cs/src/entities/tree-menu/model/tree-menu-interface.ts index 39a6f92c9..d0b28b8fa 100644 --- a/apps/cs/src/entities/tree-menu/model/tree-menu-interface.ts +++ b/apps/cs/src/entities/tree-menu/model/tree-menu-interface.ts @@ -19,6 +19,7 @@ export interface TreeMenu extends Omit { qrcd?: string; useYn: string; otptYn: string; + menuSubKornNm?: string; requiredUrlPath?: boolean; } diff --git a/apps/cs/src/pages/site/lib/utils.ts b/apps/cs/src/pages/site/lib/utils.ts new file mode 100644 index 000000000..aa809a4be --- /dev/null +++ b/apps/cs/src/pages/site/lib/utils.ts @@ -0,0 +1,24 @@ +import type { NodeModel } from "@minoru/react-dnd-treeview"; +import type { TreeMenu } from "@/entities/tree-menu"; + +export const generateMenuTreeData = ( + data: TreeMenu[], +): NodeModel[] => { + return data.reduce[]>((acc, menu) => { + const node: NodeModel = { + id: menu.menuCd ?? 0, + parent: menu.parentId ?? 0, + text: menu.menuSubKornNm ?? "", + droppable: true, + data: menu, + }; + + acc.push(node); + + if (menu.children?.length) { + acc.push(...generateMenuTreeData(menu.children)); + } + + return acc; + }, []); +}; diff --git a/apps/cs/src/pages/site/model/table-columns.tsx b/apps/cs/src/pages/site/model/table-columns.tsx index 4418e20a0..6cf3effba 100644 --- a/apps/cs/src/pages/site/model/table-columns.tsx +++ b/apps/cs/src/pages/site/model/table-columns.tsx @@ -1,6 +1,32 @@ import dayjs from "dayjs"; +import { memo, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { type ColumnDef } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Checkbox, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; import type { Site } from "@/entities/site"; +import { TreeMenuApi } from "@/entities/tree-menu"; +import { + MenuApi, + siteMapFormSchema, + type SiteMapFormData, +} from "@/entities/menu"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { MenuTree } from "../ui/menu-tree"; +import { generateMenuTreeData } from "../lib/utils"; export const columns: ColumnDef[] = [ { @@ -45,4 +71,81 @@ export const columns: ColumnDef[] = [ return date.format("YYYY-MM-DD"); }, }, + { + id: "action", + header: "관리", + cell: ({ row }) => { + return ( +
e.stopPropagation()}> + +
+ ); + }, + }, ]; + +const ActionCell = memo(({ data }: { data: Site }) => { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + const [open, setOpen] = useState(false); + + const { data: menuTreeData, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENUS, data.siteId], + queryFn: () => TreeMenuApi.getTreeMenuList(data.siteId), + select: (data) => generateMenuTreeData(data), + enabled: !!data.siteId && open, + }); + + const { mutate: publishSiteMap } = useMutationWrapper({ + mutationFn: (data: SiteMapFormData["siteMap"]) => + MenuApi.publishSiteMap(data), + onSuccess: () => { + alert("사이트맵이 저장되었습니다.", () => { + setOpen(false); + refetch(); + }); + }, + }); + + const form = useForm({ + resolver: zodResolver(siteMapFormSchema), + }); + const fieldArray = useFieldArray({ + control: form.control, + name: "siteMap", + }); + + const handlePublishSiteMap = async () => { + if (await confirm("저장하시겠습니까?")) { + publishSiteMap(form.getValues("siteMap")); + } + }; + + return ( + + + + + + + {data.siteNm} - 사이트맵 관리 + +
+
+ 표시여부 + +
+ +
+ + + + + + +
+
+ ); +}); + +ActionCell.displayName = "ActionCell"; diff --git a/apps/cs/src/pages/site/ui/editable-node.tsx b/apps/cs/src/pages/site/ui/editable-node.tsx new file mode 100644 index 000000000..20ac90812 --- /dev/null +++ b/apps/cs/src/pages/site/ui/editable-node.tsx @@ -0,0 +1,45 @@ +import type { ComponentProps } from "react"; +import { Button, Input } from "@common/components"; + +type EditableNodeProps = { + depth?: number; + onSave: () => void; + onCancel: () => void; +} & ComponentProps; + +function EditableNode({ + depth, + onSave, + onCancel, + ...props +}: EditableNodeProps) { + return ( +
e.stopPropagation()} + > + +
+ + +
+
+ ); +} + +export { EditableNode }; diff --git a/apps/cs/src/pages/site/ui/menu-node.tsx b/apps/cs/src/pages/site/ui/menu-node.tsx new file mode 100644 index 000000000..fbb5e9391 --- /dev/null +++ b/apps/cs/src/pages/site/ui/menu-node.tsx @@ -0,0 +1,128 @@ +import { useState } from "react"; +import type { NodeModel } from "@minoru/react-dnd-treeview"; +import type { UseFieldArrayReturn } from "react-hook-form"; +import { Button, Checkbox, cn } from "@common/components"; +import { type TreeMenu } from "@/entities/tree-menu"; +import type { SiteMapFormData, SiteMapPayloadData } from "@/entities/menu"; +import { EditableNode } from "./editable-node"; + +type MenuNodeProps = { + node: NodeModel; + depth: number; + isOpen: boolean; + fieldArray: UseFieldArrayReturn; + onToggle: (id: NodeModel["id"]) => void; +}; + +function MenuNode({ + node, + depth, + isOpen, + fieldArray, + onToggle, +}: MenuNodeProps) { + const { id, text, data } = node; + const indent = depth * 10 + 5; + + const [editMode, setEditMode] = useState(false); + const [nodeName, setNodeName] = useState(text); + const [otptYn, setOtptYn] = useState(data?.otptYn ?? "N"); + + const updateField = (id: number, newField: SiteMapPayloadData) => { + const existingFieldIndex = fieldArray.fields.findIndex( + (field) => field.menuCd === id, + ); + if (existingFieldIndex !== -1) { + fieldArray.update(existingFieldIndex, newField); + } else { + fieldArray.append(newField); + } + }; + + const handleToggle = () => onToggle(id); + + const handleSave = async () => { + const newField = { + menuCd: id as number, + menuSubKornNm: nodeName, + otptYn, + }; + updateField(id as number, newField); + setEditMode(false); + }; + + const handleCancel = () => { + setEditMode(false); + setNodeName(text); + }; + + const handleOtptYnChange = (checked: boolean) => { + const newField = { + menuCd: id as number, + menuSubKornNm: nodeName, + otptYn: checked ? "Y" : "N", + }; + updateField(id as number, newField); + setOtptYn(checked ? "Y" : "N"); + }; + + return ( +
{ + e.stopPropagation(); + handleToggle(); + }} + > + {editMode ? ( + setNodeName(e.target.value)} + onSave={handleSave} + onCancel={handleCancel} + /> + ) : ( +
+
+ {data?.children?.length && ( + + )} + + {nodeName} + +
+ +
+ )} + { + e.stopPropagation(); + }} + onCheckedChange={handleOtptYnChange} + /> +
+ ); +} + +export { MenuNode }; diff --git a/apps/cs/src/pages/site/ui/menu-tree.tsx b/apps/cs/src/pages/site/ui/menu-tree.tsx new file mode 100644 index 000000000..daaa278c0 --- /dev/null +++ b/apps/cs/src/pages/site/ui/menu-tree.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { useRef } from "react"; +import type { UseFieldArrayReturn } from "react-hook-form"; +import { + Tree, + type NodeModel, + type TreeMethods, +} from "@minoru/react-dnd-treeview"; +import { Button } from "@common/components"; +import type { SiteMapFormData } from "@/entities/menu"; +import { type TreeMenu } from "@/entities/tree-menu"; +import { MenuNode } from "./menu-node"; + +type MenuTreeProps = { + tree: NodeModel[]; + fieldArray: UseFieldArrayReturn; +}; + +function MenuTree({ tree, fieldArray }: MenuTreeProps) { + const treeMethods = useRef(null); + + return ( + <> +
+ , props) => { + return ; + }} + onDrop={() => {}} + canDrag={() => false} + canDrop={() => false} + classes={{ + container: "flex flex-col", + dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm", + placeholder: "relative", + }} + sort={false} + /> +
+
+ + +
+ + ); +} + +export { MenuTree }; From d7d2e2d3efe39a12772c164c6f645d24c7f9c53e Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Jul 2025 09:30:22 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[common/components]=20chore:=20LNB=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=ED=99=9C=EC=84=B1=ED=99=94=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/components/src/ui/lnb/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/components/src/ui/lnb/index.tsx b/packages/common/components/src/ui/lnb/index.tsx index 650fd2474..30698d17f 100644 --- a/packages/common/components/src/ui/lnb/index.tsx +++ b/packages/common/components/src/ui/lnb/index.tsx @@ -63,7 +63,7 @@ export function LNB({ function isPathMatched(path: string) { if (!path) return false; - return currentPath?.startsWith(path); + return currentPath === path; } useEffect(() => { From 4d9b6235b3c354c382a77cfd317be101a49f8201 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Jul 2025 17:18:46 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[apps/cs]=20feat:=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=93=B1=EB=A1=9D=20=EC=8B=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/role/api/site-authority-service.ts | 28 ++++++ .../role/model/site-authority-interface.ts | 24 +++++ apps/cs/src/pages/role/ui/role-form.tsx | 91 ++++++++++++++++++- apps/cs/src/shared/config/api.ts | 1 + 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 apps/cs/src/pages/role/api/site-authority-service.ts create mode 100644 apps/cs/src/pages/role/model/site-authority-interface.ts diff --git a/apps/cs/src/pages/role/api/site-authority-service.ts b/apps/cs/src/pages/role/api/site-authority-service.ts new file mode 100644 index 000000000..c9d0513da --- /dev/null +++ b/apps/cs/src/pages/role/api/site-authority-service.ts @@ -0,0 +1,28 @@ +import { client } from "@/shared/api"; +import type { + SiteAuthorityCreatePayload, + SiteAuthorityList, + SiteAuthoritySearchParams, + SiteAuthorityService, +} from "../model/site-authority-interface"; +import { ENDPOINT } from "@/shared/config"; + +export class SiteAuthorityApiService implements SiteAuthorityService { + public async getSiteAuthorityList( + params: SiteAuthoritySearchParams, + ): Promise { + const response = await client.get( + ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, + { params }, + ); + return response.data; + } + + public async createSiteAuthority( + data: SiteAuthorityCreatePayload[], + ): Promise { + await client.post(ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, data); + } +} + +export const SiteAuthorityApi = new SiteAuthorityApiService(); diff --git a/apps/cs/src/pages/role/model/site-authority-interface.ts b/apps/cs/src/pages/role/model/site-authority-interface.ts new file mode 100644 index 000000000..70ac79cfa --- /dev/null +++ b/apps/cs/src/pages/role/model/site-authority-interface.ts @@ -0,0 +1,24 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface SiteAuthoritySearchParams extends PageableParams { + siteId?: string; + authrtId?: string; +} + +export interface SiteAuthority { + siteId: string; + authrtId: string; + siteNm: string; + mode: string; +} + +export type SiteAuthorityCreatePayload = Omit; + +export type SiteAuthorityList = PageableList; + +export interface SiteAuthorityService { + getSiteAuthorityList: ( + params: SiteAuthoritySearchParams, + ) => Promise; + createSiteAuthority: (data: SiteAuthorityCreatePayload[]) => Promise; +} diff --git a/apps/cs/src/pages/role/ui/role-form.tsx b/apps/cs/src/pages/role/ui/role-form.tsx index 355872c9e..998ebbc2c 100644 --- a/apps/cs/src/pages/role/ui/role-form.tsx +++ b/apps/cs/src/pages/role/ui/role-form.tsx @@ -1,7 +1,9 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import type { UseFormReturn } from "react-hook-form"; import { Button, + Checkbox, Input, Label, RadioGroup, @@ -16,6 +18,9 @@ import { FormWrapper } from "@/widgets/form-wrapper"; import { RoleApi, type RoleFormData } from "@/entities/role"; import { FormField, FormItem, FormControl } from "@/shared/ui/form"; import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; +import { SiteAuthorityApi } from "../api/site-authority-service"; +import type { SiteAuthorityCreatePayload } from "../model/site-authority-interface"; type RoleFormProps = { roleId?: string; @@ -24,12 +29,24 @@ type RoleFormProps = { handleDelete?: () => Promise; }; -function RoleForm({ roleId, form, ...props }: RoleFormProps) { +function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) { const isModifyMode = !!roleId; - + const queryClient = useQueryClient(); const alert = useAlertStore((state) => state.setMessage); - const [isValidRoleId, setIsValidRoleId] = useState(false); + const [checkedSiteAuthority, setCheckedSiteAuthority] = useState( + [], + ); + + const { data: siteAuthorityList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, roleId], + queryFn: () => + SiteAuthorityApi.getSiteAuthorityList({ + authrtId: roleId, + page: 0, + size: 100, + }), + }); const { mutateAsync: checkDuplicateRoleId } = useMutationWrapper({ mutationFn: (roleId: string) => RoleApi.checkDuplicateRoleId(roleId), @@ -43,8 +60,49 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) { }, }); + const handleSiteAuthorityChecked = (checked: boolean, siteId: string) => { + if (checked) { + setCheckedSiteAuthority((prev) => [...prev, siteId]); + } else { + setCheckedSiteAuthority((prev) => prev.filter((id) => id !== siteId)); + } + }; + + const saveInterceptor = async (data: RoleFormData) => { + const siteAuthorityPayload: SiteAuthorityCreatePayload[] = + checkedSiteAuthority.map((siteId) => ({ + siteId, + authrtId: data.roleId, + mode: "C", + })); + + try { + await handleSave(data); + if (data.mngrYn === "N") return; + await SiteAuthorityApi.createSiteAuthority(siteAuthorityPayload); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY], + }); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + if (!siteAuthorityList?.content.length) return; + const checkedSiteAuthority = siteAuthorityList.content.filter( + (item) => item.mode === "R", + ); + setCheckedSiteAuthority(checkedSiteAuthority.map((item) => item.siteId)); + }, [siteAuthorityList]); + return ( - +
@@ -165,6 +223,29 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) { /> + {form.watch("mngrYn") === "Y" && ( + + 관리사이트 + +
+ {siteAuthorityList?.content.map((item) => ( +
+ { + handleSiteAuthorityChecked(checked, item.siteId); + }} + /> + {item.siteNm} +
+ ))} +
+
+
+ )} 사용여부 diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index cd639a628..120a45b2e 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -53,6 +53,7 @@ export const ENDPOINT = { GROUP_USERS: "/cms-service/api/v1/group-users", MANAGER_IP: "/cms-service/api/v1/mgrip", APILOG: "/cms-service/api/v1/api-logs", + SITE_AUTHORITY: "/cms-service/api/v1/site-authrt", }, BOARD_SERVICE: { BBS: "/board-service/api/v1/bbs", From 823ed50f37837106b84966fa9c04e1ccec5ae07b Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Jul 2025 17:21:11 +0900 Subject: [PATCH 6/8] =?UTF-8?q?Revert=20"[common/components]=20chore:=20LN?= =?UTF-8?q?B=20=EB=A9=94=EB=89=B4=20=ED=99=9C=EC=84=B1=ED=99=94=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d7d2e2d3efe39a12772c164c6f645d24c7f9c53e. --- packages/common/components/src/ui/lnb/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/components/src/ui/lnb/index.tsx b/packages/common/components/src/ui/lnb/index.tsx index 30698d17f..650fd2474 100644 --- a/packages/common/components/src/ui/lnb/index.tsx +++ b/packages/common/components/src/ui/lnb/index.tsx @@ -63,7 +63,7 @@ export function LNB({ function isPathMatched(path: string) { if (!path) return false; - return currentPath === path; + return currentPath?.startsWith(path); } useEffect(() => { From 950355717c5e1d4e0b3465c0522ac789068e87b7 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Jul 2025 18:47:24 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[apps/cs]=20feat:=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=EB=A7=B5=ED=95=91=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EB=82=B4=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/role/ui/mapping-role-dialog.tsx | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx b/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx index c7f8cdcdb..46f3d45f0 100644 --- a/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx +++ b/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx @@ -1,5 +1,6 @@ -import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table"; +import { useState, useRef } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table"; import { Dialog, DialogTrigger, @@ -8,6 +9,7 @@ import { DialogTitle, Button, Switch, + Input, } from "@common/components"; import { RoleAuthorizationsApi, @@ -31,12 +33,18 @@ function MappingRoleDialog({ }: MappingRoleDialogProps) { const queryClient = useQueryClient(); const alert = useAlertStore((state) => state.setMessage); + const inputRef = useRef(null); + const [searchParams, setSearchParams] = useState({ + keyword: "", + keywordType: "authorizationName", + }); const query = useQuery({ - queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS], + queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, searchParams], queryFn: () => RoleAuthorizationsApi.getRoleAuthorizationsListByRoleId({ roleId, + ...searchParams, }), enabled: open, }); @@ -46,7 +54,7 @@ function MappingRoleDialog({ RoleAuthorizationsApi.createRoleAuthorizations(data), onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS], + queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS], }); alert("권한 맵핑이 완료되었습니다."); }, @@ -66,6 +74,26 @@ function MappingRoleDialog({ ); }; + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const keyword = formData.get("keyword") as string; + setSearchParams({ + ...searchParams, + keyword, + }); + }; + + const handleReset = () => { + if (inputRef.current) { + inputRef.current.value = ""; + } + setSearchParams({ + ...searchParams, + keyword: "", + }); + }; + const { tableComponent } = useDataTable({ query, tableOptions: { @@ -79,21 +107,31 @@ function MappingRoleDialog({ - + 모듈 목록 -
-
- + - +
- {tableComponent}
+
{tableComponent}
); From 58c435d67e36591cfeb55f72bc4d4852d30a7b80 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Jul 2025 18:59:45 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[apps/cs]=20chore:=20=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EB=A9=94=EB=89=B4=EC=97=90=20=ED=88=B4=ED=8C=81=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/menu/ui/editable-node.tsx | 45 +++++++++++++-------- apps/cs/src/pages/site/ui/editable-node.tsx | 45 +++++++++++++-------- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/apps/cs/src/pages/menu/ui/editable-node.tsx b/apps/cs/src/pages/menu/ui/editable-node.tsx index f2274f4d8..4f1fe859c 100644 --- a/apps/cs/src/pages/menu/ui/editable-node.tsx +++ b/apps/cs/src/pages/menu/ui/editable-node.tsx @@ -1,5 +1,6 @@ import type { ComponentProps } from "react"; import { Button, Input } from "@common/components"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; type EditableNodeProps = { depth?: number; @@ -20,22 +21,34 @@ function EditableNode({ >
- - + + + + + 저장 + + + + + + 취소 +
); diff --git a/apps/cs/src/pages/site/ui/editable-node.tsx b/apps/cs/src/pages/site/ui/editable-node.tsx index 20ac90812..b297fa51b 100644 --- a/apps/cs/src/pages/site/ui/editable-node.tsx +++ b/apps/cs/src/pages/site/ui/editable-node.tsx @@ -1,5 +1,6 @@ import type { ComponentProps } from "react"; import { Button, Input } from "@common/components"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; type EditableNodeProps = { depth?: number; @@ -21,22 +22,34 @@ function EditableNode({ >
- - + + + + + 저장 + + + + + + 취소 +
);