Merge pull request 'iipbs_cs' (#13) from iipbs_cs into main
Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { BaseLayout } from "@common/components/ui";
|
||||
|
||||
export default function WithBaseLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <BaseLayout>{children}</BaseLayout>;
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<BaseLayout currentPath={pathname ?? ""} routeHandler={router.push}>
|
||||
{children}
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,43 +9,84 @@
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card_width10 {width:calc(10% - 12px);}
|
||||
.card_width10 {
|
||||
width: calc(10% - 12px);
|
||||
}
|
||||
|
||||
.card_width20 {width:calc(20% - 12px);}
|
||||
.card_width20 {
|
||||
width: calc(20% - 12px);
|
||||
}
|
||||
|
||||
.card_width25 {width:calc(25% - 12px);}
|
||||
.card_width25 {
|
||||
width: calc(25% - 12px);
|
||||
}
|
||||
|
||||
.card_width30 {width:calc(30% - 12px);}
|
||||
.card_width30 {
|
||||
width: calc(30% - 12px);
|
||||
}
|
||||
|
||||
.card_width33 {width:calc(33.333% - 12px);}
|
||||
.card_width33 {
|
||||
width: calc(33.333% - 12px);
|
||||
}
|
||||
|
||||
.card_width40 {width:calc(40% - 12px);}
|
||||
.card_width40 {
|
||||
width: calc(40% - 12px);
|
||||
}
|
||||
|
||||
.card_width45 {width:calc(45% - 12px);}
|
||||
.card_width45 {
|
||||
width: calc(45% - 12px);
|
||||
}
|
||||
|
||||
.card_width50 {width:calc(50% - 12px);}
|
||||
.card_width50 {
|
||||
width: calc(50% - 12px);
|
||||
}
|
||||
|
||||
.card_width60 {width:calc(60% - 12px);}
|
||||
.card_width60 {
|
||||
width: calc(60% - 12px);
|
||||
}
|
||||
|
||||
.card_width66 {width:calc(66.666% - 12px);}
|
||||
.card_width66 {
|
||||
width: calc(66.666% - 12px);
|
||||
}
|
||||
|
||||
.card_width70 {width:calc(70% - 12px);}
|
||||
.card_width70 {
|
||||
width: calc(70% - 12px);
|
||||
}
|
||||
|
||||
.card_width75 {width:calc(75% - 12px);}
|
||||
.card_width75 {
|
||||
width: calc(75% - 12px);
|
||||
}
|
||||
|
||||
.card_width80 {width:calc(80% - 12px);}
|
||||
.card_width80 {
|
||||
width: calc(80% - 12px);
|
||||
}
|
||||
|
||||
.card_width90 {width:calc(90% - 12px);}
|
||||
.card_width90 {
|
||||
width: calc(90% - 12px);
|
||||
}
|
||||
|
||||
.card_width100 {width:calc(100% - 12px);}
|
||||
.card_width100 {
|
||||
width: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.card_nopadding {padding:0 !important;}
|
||||
.card_nopadding {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.card_nomargin {width:100%;margin:0 !important;padding:0 !important;}
|
||||
.card_nomargin {
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.topline {border-top: 1px solid #ccc;}
|
||||
.topline {
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.bottomline {border-bottom: 1px solid #e6eef3;background: #fff;}
|
||||
.bottomline {
|
||||
border-bottom: 1px solid #e6eef3;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-border {
|
||||
border: 1px solid #dbe2e6;
|
||||
@@ -54,6 +95,7 @@
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-top: -0.6rem;
|
||||
margin-left: -0.6rem;
|
||||
margin-right: -0.6rem;
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
BoardPopupList,
|
||||
BoardPopupParams,
|
||||
BoardService,
|
||||
} from "../model/board-interface";
|
||||
|
||||
export class BoardApiService implements BoardService {
|
||||
public async getBoardPopupList(
|
||||
params: BoardPopupParams,
|
||||
): Promise<BoardPopupList> {
|
||||
const response = await client.get<BoardPopupList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.BBS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const BoardApi = new BoardApiService();
|
||||
@@ -0,0 +1,2 @@
|
||||
export { BoardApi } from "./api/board-service";
|
||||
export { type BoardPopup } from "./model/board-interface";
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const BOARD_TYPE = {
|
||||
BBSNORMAL: "BBSNORMAL",
|
||||
BBSFAQ: "BBSFAQ",
|
||||
BBSIMAGE: "BBSIMAGE",
|
||||
BBSMOVIE: "BBSMOVIE",
|
||||
} as const;
|
||||
|
||||
export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE];
|
||||
|
||||
export interface BoardPopupParams extends PageableParams {
|
||||
bbsNm?: string;
|
||||
}
|
||||
|
||||
export interface BoardPopup {
|
||||
bbsId: number;
|
||||
bbsTypeCd: BoardType;
|
||||
bbsTypeNm: string;
|
||||
bbsNm: string;
|
||||
bbsExpln: string;
|
||||
rfrncUseYn: string;
|
||||
atflUseYn: string;
|
||||
atflMaxCnt: number;
|
||||
atflMaxFileSz: number;
|
||||
ansUseYn: string;
|
||||
cmntUseYn: string;
|
||||
dgstfnEvlUseYn: string;
|
||||
smsUseYn: string;
|
||||
upndFixUseYn: string;
|
||||
pstgPrdUseYn: string;
|
||||
popupUseYn: string;
|
||||
delYn: string;
|
||||
regDt: string;
|
||||
pstCnt: number;
|
||||
}
|
||||
|
||||
export type BoardPopupList = PageableList<BoardPopup>;
|
||||
|
||||
export interface BoardService {
|
||||
getBoardPopupList: (params: BoardPopupParams) => Promise<BoardPopupList>;
|
||||
}
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
siteFormSchema,
|
||||
type SiteService,
|
||||
type SiteList,
|
||||
type ISiteDetail,
|
||||
type ISiteParams,
|
||||
type SiteDetail,
|
||||
type SiteParams,
|
||||
type SiteFormData,
|
||||
} from "../model/site-interface";
|
||||
|
||||
export class SiteApiService implements SiteService {
|
||||
public async getSiteList(params: ISiteParams): Promise<SiteList> {
|
||||
public async getSiteList(params: SiteParams): Promise<SiteList> {
|
||||
const response = await client.get<SiteList>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}`,
|
||||
{ params },
|
||||
@@ -18,8 +18,8 @@ export class SiteApiService implements SiteService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async siteDetail(siteId: string): Promise<ISiteDetail> {
|
||||
const response = await client.get<ISiteDetail>(
|
||||
public async siteDetail(siteId: string): Promise<SiteDetail> {
|
||||
const response = await client.get<SiteDetail>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
|
||||
);
|
||||
return response.data;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { SiteApi } from "./api/site-service";
|
||||
export {
|
||||
siteFormSchema,
|
||||
type ISiteParams,
|
||||
type ISite,
|
||||
type SiteParams,
|
||||
type Site,
|
||||
type SiteFormData,
|
||||
} from "./model/site-interface";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import type { IPageableParams, PageableList } from "@/shared/model";
|
||||
import type { PageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface ISiteParams extends IPageableParams {
|
||||
export interface SiteParams extends PageableParams {
|
||||
siteNm?: string;
|
||||
siteKndCd?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface ISite {
|
||||
export interface Site {
|
||||
siteId: string;
|
||||
siteNm: string;
|
||||
siteKindCode: string;
|
||||
@@ -19,7 +19,7 @@ export interface ISite {
|
||||
dmnAddr: string;
|
||||
}
|
||||
|
||||
export interface ISiteDetail extends SiteFormData {
|
||||
export interface SiteDetail extends SiteFormData {
|
||||
siteId: string;
|
||||
siteKindNm: string;
|
||||
basicSiteYn: string;
|
||||
@@ -49,11 +49,11 @@ export const siteFormSchema = z.object({
|
||||
});
|
||||
|
||||
export type SiteFormData = z.infer<typeof siteFormSchema>;
|
||||
export type SiteList = PageableList<ISite[]>;
|
||||
export type SiteList = PageableList<Site>;
|
||||
|
||||
export interface SiteService {
|
||||
getSiteList(params: ISiteParams): Promise<SiteList>;
|
||||
siteDetail(siteId: string): Promise<ISiteDetail>;
|
||||
getSiteList(params: SiteParams): Promise<SiteList>;
|
||||
siteDetail(siteId: string): Promise<SiteDetail>;
|
||||
modifySite(siteId: string, data: SiteFormData): Promise<void>;
|
||||
createSite(data: SiteFormData): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
ContentService,
|
||||
ContentParams,
|
||||
ContentList,
|
||||
} from "../model/content-interface";
|
||||
|
||||
export class ContentApiService implements ContentService {
|
||||
public async getContentList(params: ContentParams): Promise<ContentList> {
|
||||
const response = await client.get<ContentList>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const ContentApi = new ContentApiService();
|
||||
@@ -1,16 +1,55 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { Menu, MenuService, TreeMenu } from "../model/menu-interface";
|
||||
import type {
|
||||
Menu,
|
||||
MenuFormData,
|
||||
MenuService,
|
||||
TreeMenu,
|
||||
TreeNode,
|
||||
} from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
public async getTreeMenuList(siteId: string): Promise<Menu[]> {
|
||||
const response = await client.get<Menu[]>(
|
||||
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> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async modifyMenu(
|
||||
menuCd: number,
|
||||
data: Omit<MenuFormData, "siteId">,
|
||||
): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteMenu(menuCd: number): Promise<void> {
|
||||
const response = await client.delete<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getTreeMenuList(siteId: string): Promise<TreeMenu[]> {
|
||||
const response = await client.get<TreeMenu[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
);
|
||||
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>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
data,
|
||||
|
||||
@@ -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<Organization[]> {
|
||||
const response = await client.get<Organization[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_OGNZ}/treeuser`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const OrganizationApi = new OrganizationApiService();
|
||||
@@ -1,9 +1,12 @@
|
||||
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";
|
||||
import type { Organization, User } from "../model/organization-interface";
|
||||
|
||||
export const generateTreeData = (data: Menu[]): NodeModel<Menu>[] => {
|
||||
return data.reduce<NodeModel<Menu>[]>((acc, menu) => {
|
||||
const node: NodeModel<Menu> = {
|
||||
export const generateMenuTreeData = (
|
||||
data: TreeMenu[],
|
||||
): NodeModel<TreeMenu>[] => {
|
||||
return data.reduce<NodeModel<TreeMenu>[]>((acc, menu) => {
|
||||
const node: NodeModel<TreeMenu> = {
|
||||
id: menu.menuCd ?? 0,
|
||||
parent: menu.parentId ?? 0,
|
||||
text: menu.menuKornNm,
|
||||
@@ -14,31 +17,37 @@ export const generateTreeData = (data: Menu[]): NodeModel<Menu>[] => {
|
||||
acc.push(node);
|
||||
|
||||
if (menu.children?.length) {
|
||||
acc.push(...generateTreeData(menu.children));
|
||||
acc.push(...generateMenuTreeData(menu.children));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const generateNewTreeData = (
|
||||
treeData: NodeModel<Menu>[],
|
||||
options: DropOptions<Menu>,
|
||||
): TreeMenu | null => {
|
||||
export const generateNewMenuTreeData = (
|
||||
treeData: NodeModel<TreeMenu>[],
|
||||
options: DropOptions<TreeMenu>,
|
||||
): TreeNode | null => {
|
||||
const { dragSourceId, dragSource, dropTargetId, dropTarget } = options;
|
||||
|
||||
if (!dragSource?.data) return null;
|
||||
|
||||
const siblings = treeData.filter((node) => node.parent === dropTargetId);
|
||||
|
||||
const dragSourceIndex = siblings.findIndex(
|
||||
(node) => node.id === dragSourceId,
|
||||
);
|
||||
|
||||
const prevSibling = siblings[dragSourceIndex - 1];
|
||||
|
||||
const newSortSeq =
|
||||
siblings.findIndex((sibling) => sibling.id === dragSourceId) + 1;
|
||||
dragSourceIndex === 0 ? 1 : (prevSibling?.data?.sortSeq ?? 0) + 1;
|
||||
|
||||
const childNodes = treeData.filter(
|
||||
(node) => node.parent === dragSourceId && node.data,
|
||||
);
|
||||
|
||||
const baseNode = (node: NodeModel<Menu>): TreeMenu => {
|
||||
const baseNode = (node: NodeModel<TreeMenu>): TreeNode => {
|
||||
if (!node.data) return {};
|
||||
return {
|
||||
menuCd: node.data?.menuCd,
|
||||
@@ -47,20 +56,20 @@ export const generateNewTreeData = (
|
||||
};
|
||||
};
|
||||
|
||||
const treeMenu: TreeMenu = {
|
||||
const treeNode: TreeNode = {
|
||||
...baseNode(dragSource),
|
||||
parentId: dropTargetId === 0 ? null : (dropTargetId as number),
|
||||
sortSeq: newSortSeq,
|
||||
levelNo: (dropTarget?.data?.levelNo ?? 0) + 1,
|
||||
};
|
||||
|
||||
const result: TreeMenu = {
|
||||
...treeMenu,
|
||||
const result: TreeNode = {
|
||||
...treeNode,
|
||||
children: childNodes.map((child) => ({
|
||||
...baseNode(child),
|
||||
parentId: child.data?.parentId === 0 ? null : (dropTargetId as number),
|
||||
sortSeq: child.data?.sortSeq,
|
||||
levelNo: (treeMenu?.levelNo ?? 0) + 1,
|
||||
levelNo: (treeNode?.levelNo ?? 0) + 1,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -68,21 +77,22 @@ export const generateNewTreeData = (
|
||||
};
|
||||
|
||||
export const handleSelectNode =
|
||||
(callback: (node: NodeModel<Menu>) => void) => (node: NodeModel<Menu>) => {
|
||||
(callback: (node: NodeModel<TreeMenu>) => void) =>
|
||||
(node: NodeModel<TreeMenu>) => {
|
||||
callback(node);
|
||||
};
|
||||
|
||||
export const handleDropNode =
|
||||
(callback: (newTree: TreeMenu) => void) =>
|
||||
(newTree: NodeModel<Menu>[], options: DropOptions<Menu>) => {
|
||||
const newTreeData = generateNewTreeData(newTree, options);
|
||||
(callback: (newTree: TreeNode) => void) =>
|
||||
(newTree: NodeModel<TreeMenu>[], options: DropOptions<TreeMenu>) => {
|
||||
const newTreeData = generateNewMenuTreeData(newTree, options);
|
||||
if (!newTreeData) return;
|
||||
callback(newTreeData);
|
||||
};
|
||||
|
||||
export const handleNodeTextChange =
|
||||
(treeData: NodeModel<Menu>[]) =>
|
||||
(callback: (newTree: NodeModel<Menu>[]) => void) =>
|
||||
(treeData: NodeModel<TreeMenu>[]) =>
|
||||
(callback: (newTree: NodeModel<TreeMenu>[]) => void) =>
|
||||
(id: NodeModel["id"], value: string) => {
|
||||
const newTree = treeData.map((node) => {
|
||||
if (node.id === id) {
|
||||
@@ -96,3 +106,60 @@ export const handleNodeTextChange =
|
||||
});
|
||||
callback(newTree);
|
||||
};
|
||||
|
||||
export const generateOrganizationTreeData = (
|
||||
organizations: Organization[],
|
||||
): NodeModel<Organization | User>[] => {
|
||||
return organizations.reduce<NodeModel<Organization | User>[]>((acc, org) => {
|
||||
const orgNode: NodeModel<Organization> = {
|
||||
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<Organization | User>[],
|
||||
keyword: string,
|
||||
): NodeModel<Organization | User>[] => {
|
||||
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<NodeModel<Organization | User>>()),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { BoardPopup } from "@/entities/board";
|
||||
|
||||
const columnHelper = createColumnHelper<BoardPopup>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor((_, index) => index + 1, {
|
||||
id: "no",
|
||||
header: "NO",
|
||||
}),
|
||||
columnHelper.accessor("bbsNm", {
|
||||
id: "bbsNm",
|
||||
header: "게시판명",
|
||||
}),
|
||||
columnHelper.accessor("bbsTypeNm", {
|
||||
id: "bbsTypeNm",
|
||||
header: "게시판유형",
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
id: "regDt",
|
||||
header: "최초생성일",
|
||||
}),
|
||||
columnHelper.accessor("pstCnt", {
|
||||
id: "pstCnt",
|
||||
header: "게시물건수",
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: "선택",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={row.getToggleSelectedHandler()}
|
||||
className="mx-4 flex justify-center"
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export { columns };
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface ContentParams extends PageableParams {
|
||||
contsTtl?: string;
|
||||
aprvContsVer?: number;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface Content {
|
||||
contsSn: number;
|
||||
contsTtl: string;
|
||||
aprvContsVer: number;
|
||||
useYn: string;
|
||||
rgtr: string;
|
||||
regDt: string;
|
||||
}
|
||||
|
||||
export type ContentList = PageableList<Content>;
|
||||
|
||||
export interface ContentService {
|
||||
getContentList(params: ContentParams): Promise<ContentList>;
|
||||
}
|
||||
@@ -11,28 +11,28 @@ export const MENU_TYPE = {
|
||||
|
||||
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
|
||||
|
||||
export interface TreeMenu {
|
||||
export interface TreeNode {
|
||||
menuCd?: number;
|
||||
name?: string;
|
||||
sortSeq?: number;
|
||||
parentId?: number | null;
|
||||
levelNo?: number;
|
||||
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;
|
||||
menuEngNm: string;
|
||||
iconNm: string;
|
||||
children: Menu[];
|
||||
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({
|
||||
@@ -53,18 +53,34 @@ export const menuFormSchema = z.object({
|
||||
MENU_TYPE.BOTTOM,
|
||||
]),
|
||||
menuLnkgSn: z.number().nullish(),
|
||||
otptYn: z.enum(["Y", "N"]).default("Y").nullish(),
|
||||
otptYn: z.string().default("Y"),
|
||||
prntMenuCd: z.number().nullish(),
|
||||
sortSeq: z.number(),
|
||||
evlUseYn: z.enum(["Y", "N"]).nullish(),
|
||||
evlUseYn: z.string().default("Y"),
|
||||
qrcd: z.string().nullish(),
|
||||
useYn: z.enum(["Y", "N"]).default("Y").nullish(),
|
||||
useYn: z.string().default("Y"),
|
||||
newWndwYn: z.string().nullish(),
|
||||
menuLnkgSnNm: z.string().nullish(),
|
||||
menuPicUserUnqId: z.string().nullish(),
|
||||
menuPicUserUnqIdNm: z.string().nullish(),
|
||||
menuPicDsgnYmd: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||
export type Menu = Omit<MenuFormData, "iconNm"> & {
|
||||
menuCd: number;
|
||||
iconName?: string;
|
||||
};
|
||||
|
||||
export interface MenuService {
|
||||
getTreeMenuList: (siteId: string) => Promise<Menu[]>;
|
||||
modifyTreeMenu: (siteId: string, data: TreeMenu[]) => Promise<void>;
|
||||
getMenuInfo: (menuCd: number) => Promise<Menu>;
|
||||
createMenu: (data: MenuFormData) => Promise<void>;
|
||||
modifyMenu: (
|
||||
menuCd: number,
|
||||
data: Omit<MenuFormData, "siteId">,
|
||||
) => Promise<void>;
|
||||
deleteMenu: (menuCd: number) => Promise<void>;
|
||||
getTreeMenuList: (siteId: string) => Promise<TreeMenu[]>;
|
||||
modifyTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
|
||||
modifyMenuName: (menuCd: number, name: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -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<Organization[]>;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Content } from "../model/content-interface";
|
||||
|
||||
const columnHelper = createColumnHelper<Content>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor((_, index) => index + 1, {
|
||||
id: "no",
|
||||
header: "NO",
|
||||
}),
|
||||
columnHelper.accessor("contsTtl", {
|
||||
id: "contsTtl",
|
||||
header: "콘텐츠명",
|
||||
}),
|
||||
// TODO: 최종수정자, 최종수정일, 승인일시 추가
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: "선택",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={row.getToggleSelectedHandler()}
|
||||
className="mx-4 flex justify-center"
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export { columns };
|
||||
@@ -1,130 +0,0 @@
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
IconFinder,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
|
||||
export function BaseBottomMenuForm({
|
||||
form,
|
||||
}: {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
}) {
|
||||
const handleIconClick = (icon: string) => {
|
||||
form.setValue("iconNm", icon);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead>메뉴 서브명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuSubKornNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="메뉴 서브명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>메뉴 아이콘</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{form.watch("iconNm")}
|
||||
</div>
|
||||
<IconFinder handleClick={handleIconClick} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>출력 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="otptYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="otptY" />
|
||||
<Label htmlFor="otptY">출력</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="otptN" />
|
||||
<Label htmlFor="otptN">출력안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,10 +20,10 @@ import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
|
||||
|
||||
interface BaseMenuFormProps {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuId?: number;
|
||||
menuCd?: number;
|
||||
}
|
||||
|
||||
export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
|
||||
function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
@@ -31,14 +31,16 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>메뉴ID</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{menuId}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{menuCd && (
|
||||
<TableRow>
|
||||
<TableHead>메뉴ID</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{menuCd}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>메뉴명</TableHead>
|
||||
<TableCell className="border">
|
||||
@@ -50,6 +52,7 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="메뉴명을 입력하십시오."
|
||||
/>
|
||||
@@ -101,11 +104,8 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
|
||||
key={key}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={value}
|
||||
id={`menuType${key}`}
|
||||
/>
|
||||
<Label htmlFor={`menuType${key}`}>
|
||||
<RadioGroupItem value={value} id={key} />
|
||||
<Label htmlFor={key}>
|
||||
{key === "EMPTY" && "빈메뉴"}
|
||||
{key === "CONTENTS" && "콘텐츠"}
|
||||
{key === "BOARD" && "게시판"}
|
||||
@@ -128,3 +128,5 @@ export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { BaseMenuForm };
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
import { BaseBottomMenuForm } from "./base-bottom-menu-form";
|
||||
|
||||
export function BoardMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>게시판</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{form.watch("menuLnkgSn")}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>연결 형태</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="evlUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="self" id="self" />
|
||||
<Label htmlFor="self">현재창</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="blank" id="blank" />
|
||||
<Label htmlFor="blank">새창</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>메뉴 평가 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="evlUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="evlUseY" />
|
||||
<Label htmlFor="evlUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="evlUseN" />
|
||||
<Label htmlFor="evlUseN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<BaseBottomMenuForm form={form} />
|
||||
|
||||
<TableRow>
|
||||
<TableHead>QR코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="qrcd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="qrcdY" />
|
||||
<Label htmlFor="qrcdY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="qrcdN" />
|
||||
<Label htmlFor="qrcdN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>담당자</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{form.watch("menuLnkgSn")}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
} from "@common/components/ui";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { BoardApi, type BoardPopup } from "@/entities/board";
|
||||
import { columns } from "../model/board-table-columns";
|
||||
|
||||
type BoardSearchDialogProps = {
|
||||
handleBoardSelect: (board: BoardPopup) => void;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function BoardSearchDialog({
|
||||
handleBoardSelect,
|
||||
...props
|
||||
}: BoardSearchDialogProps) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [bbsNm, setBbsNm] = useState("");
|
||||
|
||||
const { data: boardList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS],
|
||||
queryFn: () => BoardApi.getBoardPopupList({ bbsNm }),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: boardList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const boardData = table.getSelectedRowModel().rows[0]
|
||||
?.original as BoardPopup;
|
||||
if (!boardData) return;
|
||||
handleBoardSelect(boardData);
|
||||
}, [table.getSelectedRowModel().rows]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3">
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>게시판 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||
<Input
|
||||
placeholder="검색어를 입력하세요."
|
||||
className="!text-[1.3rem]"
|
||||
value={bbsNm}
|
||||
onChange={(e) => setBbsNm(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[40rem] w-full overflow-y-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { BoardSearchDialog };
|
||||
@@ -1,22 +0,0 @@
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import { Table, TableBody } from "@common/components";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
import { BaseBottomMenuForm } from "./base-bottom-menu-form";
|
||||
|
||||
export function BottomMenuForm({
|
||||
form,
|
||||
}: {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
}) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<BaseBottomMenuForm form={form} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
import { BaseBottomMenuForm } from "./base-bottom-menu-form";
|
||||
|
||||
export function ContentMenuForm({
|
||||
form,
|
||||
}: {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
}) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>콘텐츠</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{form.watch("menuLnkgSn")}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>연결 형태</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="evlUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="self" id="self" />
|
||||
<Label htmlFor="self">현재창</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="blank" id="blank" />
|
||||
<Label htmlFor="blank">새창</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>메뉴 평가 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="evlUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="evlUseY" />
|
||||
<Label htmlFor="evlUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="evlUseN" />
|
||||
<Label htmlFor="evlUseN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<BaseBottomMenuForm form={form} />
|
||||
|
||||
<TableRow>
|
||||
<TableHead>QR코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="qrcd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="qrcdY" />
|
||||
<Label htmlFor="qrcdY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="qrcdN" />
|
||||
<Label htmlFor="qrcdN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>담당자</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{form.watch("menuLnkgSn")}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
} from "@common/components/ui";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ContentApi } from "../api/content-service";
|
||||
import type { Content } from "../model/content-interface";
|
||||
import { columns } from "../model/table-colums";
|
||||
|
||||
type ContentSearchDialogProps = {
|
||||
handleContentSelect: (content: Content) => void;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function ContentSearchDialog({
|
||||
handleContentSelect,
|
||||
...props
|
||||
}: ContentSearchDialogProps) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [contsTtl, setContsTtl] = useState("");
|
||||
|
||||
const { data: contentList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS],
|
||||
queryFn: () =>
|
||||
ContentApi.getContentList({ contsTtl, useYn: "Y", page: 0, size: 100 }),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: contentList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const contentData = table.getSelectedRowModel().rows[0]
|
||||
?.original as Content;
|
||||
if (!contentData) return;
|
||||
handleContentSelect(contentData);
|
||||
}, [table.getSelectedRowModel().rows]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3">
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>콘텐츠 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||
<Input
|
||||
placeholder="검색어를 입력하세요."
|
||||
className="!text-[1.3rem]"
|
||||
value={contsTtl}
|
||||
onChange={(e) => setContsTtl(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[40rem] w-full overflow-y-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ContentSearchDialog };
|
||||
@@ -0,0 +1,44 @@
|
||||
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({
|
||||
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
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { EditableNode };
|
||||
@@ -1,18 +0,0 @@
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import { Table, TableBody } from "@common/components";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
import { BaseBottomMenuForm } from "./base-bottom-menu-form";
|
||||
|
||||
export function EmptyMenuForm({ form }: { form: UseFormReturn<MenuFormData> }) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<BaseBottomMenuForm form={form} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import { Table, TableBody } from "@common/components";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
import { BaseBottomMenuForm } from "./base-bottom-menu-form";
|
||||
|
||||
export function InsideMenuForm({
|
||||
form,
|
||||
}: {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
}) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<BaseBottomMenuForm form={form} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +1,80 @@
|
||||
import { useState } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import { Button } from "@common/components";
|
||||
import { Form } from "@/shared/ui/form";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
import { BaseMenuForm } from "./base-menu-form";
|
||||
import { EmptyMenuForm } from "./empty-menu-form";
|
||||
import { ContentMenuForm } from "./content-menu-form";
|
||||
import { BoardMenuForm } from "./board-menu-form";
|
||||
import { InsideMenuForm } from "./inside-menu-form";
|
||||
import { OutsideMenuForm } from "./outside-menu-form";
|
||||
import { BottomMenuForm } from "./bottom-menu-form";
|
||||
import { TypeMenuForm } from "./type-menu-form";
|
||||
import { PermissionsManagementDialog } from "./permissions-management-dialog";
|
||||
|
||||
interface MenuFormProps {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuId?: number;
|
||||
handleSave: (data: MenuFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
menuCd?: number;
|
||||
handleCreate: (data: MenuFormData) => void;
|
||||
handleModify: ({
|
||||
menuCd,
|
||||
data,
|
||||
}: {
|
||||
menuCd: number;
|
||||
data: Omit<MenuFormData, "siteId">;
|
||||
}) => void;
|
||||
handleDelete?: (menuCd: number) => void;
|
||||
}
|
||||
|
||||
export function MenuForm({
|
||||
function MenuForm({
|
||||
form,
|
||||
menuId,
|
||||
handleSave,
|
||||
menuCd,
|
||||
handleCreate,
|
||||
handleModify,
|
||||
handleDelete,
|
||||
}: MenuFormProps) {
|
||||
const isModifyMode = !!menuId;
|
||||
const isModifyMode = !!menuCd;
|
||||
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
|
||||
const onSubmit = async (data: MenuFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
const handleConfirm = async (fn: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm(confirmMessage)) {
|
||||
setOnConfirm(false);
|
||||
handleSave(data);
|
||||
}
|
||||
await fn();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleDelete?.();
|
||||
const onSubmit = async (data: MenuFormData) => {
|
||||
if (isModifyMode) {
|
||||
await onModify(data);
|
||||
} else {
|
||||
await onCreate(data);
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onCreate = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
handleCreate(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onModify = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("수정하시겠습니까?")) {
|
||||
handleModify({ menuCd: menuCd!, data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.(menuCd!);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const menuType = form.watch("menuLnkgTypeCd");
|
||||
const isSubmitDisabled = !form.watch("siteId");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -61,18 +83,17 @@ export function MenuForm({
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
메뉴 등록/수정
|
||||
{isModifyMode ? "메뉴 수정" : "메뉴 등록"}
|
||||
</h3>
|
||||
<PermissionsManagementDialog menuCd={menuCd} />
|
||||
</div>
|
||||
<div className="card">
|
||||
<BaseMenuForm form={form} menuId={menuId} />
|
||||
<div className="-mt-[0.1rem]">
|
||||
{renderMenuTypeComponent(menuType, form)}
|
||||
</div>
|
||||
<BaseMenuForm form={form} menuCd={menuCd} />
|
||||
<TypeMenuForm form={form} menuType={menuType} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button size="lg" type="submit">
|
||||
<Button size="lg" type="submit" disabled={isSubmitDisabled}>
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
@@ -88,24 +109,4 @@ export function MenuForm({
|
||||
);
|
||||
}
|
||||
|
||||
function renderMenuTypeComponent(
|
||||
menuType: string,
|
||||
form: UseFormReturn<MenuFormData>,
|
||||
) {
|
||||
switch (menuType) {
|
||||
case MENU_TYPE.EMPTY:
|
||||
return <EmptyMenuForm form={form} />;
|
||||
case MENU_TYPE.CONTENTS:
|
||||
return <ContentMenuForm form={form} />;
|
||||
case MENU_TYPE.BOARD:
|
||||
return <BoardMenuForm form={form} />;
|
||||
case MENU_TYPE.INSIDE:
|
||||
return <InsideMenuForm form={form} />;
|
||||
case MENU_TYPE.OUTSIDE:
|
||||
return <OutsideMenuForm form={form} />;
|
||||
case MENU_TYPE.BOTTOM:
|
||||
return <BottomMenuForm form={form} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export { MenuForm };
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { Button, Input } from "@common/components/ui";
|
||||
import type { Menu } from "../model/menu-interface";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import type { TreeMenu } from "../model/menu-interface";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
type MenuNodeProps = {
|
||||
node: NodeModel<Menu>;
|
||||
node: NodeModel<TreeMenu>;
|
||||
depth: number;
|
||||
isOpen: boolean;
|
||||
isSelected: boolean;
|
||||
isAdditionalParent: boolean;
|
||||
treeMethods: TreeMethods | null;
|
||||
onToggle: (id: NodeModel["id"]) => void;
|
||||
onSelect: (node: NodeModel<Menu>) => void;
|
||||
onSelect: (node: NodeModel<TreeMenu>) => void;
|
||||
};
|
||||
|
||||
function MenuNode({
|
||||
@@ -20,17 +24,26 @@ function MenuNode({
|
||||
depth,
|
||||
isOpen,
|
||||
isSelected,
|
||||
isAdditionalParent,
|
||||
treeMethods,
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: MenuNodeProps) {
|
||||
const { id, text, data } = node;
|
||||
const indent = depth * 10 + 5;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutateAsync: modifyMenuName } = useMutation({
|
||||
mutationFn: (nodeName: string) => {
|
||||
const encodedName = encodeURIComponent(nodeName);
|
||||
return MenuApi.modifyMenuName(id as number, encodedName);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
@@ -44,6 +57,15 @@ function MenuNode({
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditMode(false);
|
||||
setNodeName(text);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdditionalParent) treeMethods?.open(id);
|
||||
}, [isAdditionalParent]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
@@ -51,63 +73,40 @@ function MenuNode({
|
||||
"group flex cursor-pointer items-center justify-between gap-2 rounded-sm p-2 hover:bg-accent",
|
||||
isSelected && "!bg-[rgba(32,148,250,0.2)]",
|
||||
)}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{editMode ? (
|
||||
<>
|
||||
<Input
|
||||
value={nodeName}
|
||||
onChange={(e) => setNodeName(e.target.value)}
|
||||
className="h-[2.5rem] !text-[1.3rem] text-foreground"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{editMode ? (
|
||||
<EditableNode
|
||||
value={nodeName}
|
||||
onChange={(e) => setNodeName(e.target.value)}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center" onClick={handleSelect}>
|
||||
{data?.children?.length && (
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward mr-2 text-base text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[1.4rem] font-medium text-label">{text}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{editMode ? (
|
||||
<div className="flex items-center gap-1">
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={handleSave}
|
||||
className="invisible !bg-transparent group-hover:visible"
|
||||
onClick={() => setEditMode(true)}
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={() => {
|
||||
setEditMode(false);
|
||||
setNodeName(text);
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="invisible !bg-transparent group-hover:visible"
|
||||
onClick={() => setEditMode(true)}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
@@ -15,6 +17,9 @@ import {
|
||||
import { SiteApi } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import type { TreeMenu } from "../model/menu-interface";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import {
|
||||
MENU_TYPE,
|
||||
@@ -23,15 +28,36 @@ import {
|
||||
} from "../model/menu-interface";
|
||||
import { MenuTree } from "./menu-tree";
|
||||
import { MenuForm } from "./menu-form";
|
||||
import { generateTreeData } from "../lib/utils";
|
||||
import { generateMenuTreeData } from "../lib/utils";
|
||||
|
||||
function MenuPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [siteId, setSiteId] = useState("");
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
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],
|
||||
queryFn: () => MenuApi.getMenuInfo(selectedMenu?.menuCd ?? 0),
|
||||
enabled: !!selectedMenu?.menuCd,
|
||||
});
|
||||
|
||||
const { data: treeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
queryFn: () => MenuApi.getTreeMenuList(siteId),
|
||||
select: (data) => generateTreeData(data),
|
||||
select: (data) => {
|
||||
const treeData = generateMenuTreeData(data);
|
||||
const defaultSortSeq =
|
||||
treeData.filter((node) => node.data?.levelNo === 1).length + 1;
|
||||
form.setValue("sortSeq", defaultSortSeq);
|
||||
|
||||
return treeData;
|
||||
},
|
||||
enabled: !!siteId,
|
||||
});
|
||||
|
||||
@@ -45,60 +71,195 @@ function MenuPage() {
|
||||
})),
|
||||
});
|
||||
|
||||
const { mutateAsync: createMenu } = useMutation({
|
||||
mutationFn: (data: MenuFormData) => MenuApi.createMenu(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: modifyMenu } = useMutation({
|
||||
mutationFn: ({
|
||||
menuCd,
|
||||
data,
|
||||
}: {
|
||||
menuCd: number;
|
||||
data: Omit<MenuFormData, "siteId">;
|
||||
}) => MenuApi.modifyMenu(menuCd, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteMenu } = useMutation({
|
||||
mutationFn: (menuCd: number) => MenuApi.deleteMenu(menuCd),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<MenuFormData>({
|
||||
resolver: zodResolver(menuFormSchema),
|
||||
defaultValues: {
|
||||
menuLnkgTypeCd: MENU_TYPE.EMPTY,
|
||||
},
|
||||
values: {
|
||||
...menuInfo,
|
||||
siteId,
|
||||
levelNo: menuInfo?.levelNo ?? 1,
|
||||
sortSeq: menuInfo?.sortSeq ?? 1,
|
||||
menuKornNm: menuInfo?.menuKornNm ?? "",
|
||||
menuLnkgTypeCd: menuInfo?.menuLnkgTypeCd ?? MENU_TYPE.EMPTY,
|
||||
iconNm: menuInfo?.iconName,
|
||||
otptYn: menuInfo?.otptYn ?? "Y",
|
||||
evlUseYn: menuInfo?.evlUseYn ?? "Y",
|
||||
useYn: menuInfo?.useYn ?? "Y",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSave = async (data: MenuFormData) => {
|
||||
console.log(data);
|
||||
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 () => {
|
||||
const prntMenuCd = !additionalNode?.parent
|
||||
? null
|
||||
: (additionalNode?.parent as number);
|
||||
|
||||
await createMenu({
|
||||
...additionalNodeForm.getValues(),
|
||||
prntMenuCd,
|
||||
});
|
||||
resetAdditionalNode();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {};
|
||||
const resetAdditionalNode = () => {
|
||||
additionalNodeForm.setValue("menuKornNm", "");
|
||||
setAdditionalNode(null);
|
||||
};
|
||||
|
||||
const handleSiteIdChange = (value: string) => {
|
||||
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 handleNodeDelete = async () => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
deleteMenu(selectedMenu!.menuCd!);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddMenu = () => {
|
||||
if (additionalNode) return;
|
||||
setAdditionalNode({
|
||||
id: -1,
|
||||
parent: selectedMenu?.menuCd ?? 0,
|
||||
text: "",
|
||||
});
|
||||
};
|
||||
|
||||
const tree = additionalNode
|
||||
? [...(additionalNode ? [additionalNode] : []), ...(treeData ?? [])]
|
||||
: (treeData ?? []);
|
||||
|
||||
return (
|
||||
<PageLayout pageTitle="메뉴 관리">
|
||||
<div className="card flex gap-2">
|
||||
<div className="card card-border w-[24rem]">
|
||||
<Select value={siteId} onValueChange={setSiteId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options?.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{treeData && (
|
||||
<>
|
||||
<div className="my-4 flex gap-2">
|
||||
<Button size="sm" color="primary" className="flex-1">
|
||||
메뉴 추가
|
||||
</Button>
|
||||
<Button size="sm" color="red" className="flex-1">
|
||||
메뉴 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border !mx-0">
|
||||
<MenuTree treeData={treeData ?? []} siteId={siteId} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<PageLayout pageTitle="메뉴 관리">
|
||||
<div className="card !mb-0 flex !min-h-[calc(100vh-16.8rem)] gap-2">
|
||||
<div className="card card-border w-[24rem]">
|
||||
<Select value={siteId} onValueChange={handleSiteIdChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options?.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{siteId && (
|
||||
<>
|
||||
<div className="my-4 flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="flex-1"
|
||||
onClick={handleAddMenu}
|
||||
>
|
||||
메뉴 추가
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
className="flex-1"
|
||||
onClick={handleNodeDelete}
|
||||
disabled={!selectedMenu}
|
||||
>
|
||||
메뉴 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border !mx-0">
|
||||
<MenuTree
|
||||
tree={tree}
|
||||
siteId={siteId}
|
||||
selectedMenu={selectedMenu}
|
||||
additionalNodeForm={additionalNodeForm}
|
||||
onSelectNode={handleNodeSelect}
|
||||
onAdditionalNodeSave={handleAdditionalNodeSave}
|
||||
onAdditionalNodeCancel={resetAdditionalNode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="card card-border flex-1">
|
||||
<MenuForm
|
||||
form={form}
|
||||
menuCd={selectedMenu?.menuCd}
|
||||
handleCreate={createMenu}
|
||||
handleModify={modifyMenu}
|
||||
handleDelete={deleteMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-border flex-1">
|
||||
<MenuForm
|
||||
form={form}
|
||||
handleSave={handleSave}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</PageLayout>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
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 { Button } from "@common/components/ui";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import type { Menu, TreeMenu } 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<Menu>[];
|
||||
tree: NodeModel<TreeMenu>[];
|
||||
siteId: string;
|
||||
selectedMenu: TreeMenu | null;
|
||||
additionalNodeForm: UseFormReturn<MenuFormData>;
|
||||
onSelectNode: (node: NodeModel<TreeMenu>) => void;
|
||||
onAdditionalNodeSave: () => void;
|
||||
onAdditionalNodeCancel: () => void;
|
||||
};
|
||||
|
||||
function MenuTree({ treeData, siteId }: MenuTreeProps) {
|
||||
function MenuTree({
|
||||
tree,
|
||||
siteId,
|
||||
selectedMenu,
|
||||
additionalNodeForm,
|
||||
onSelectNode,
|
||||
onAdditionalNodeSave,
|
||||
onAdditionalNodeCancel,
|
||||
}: MenuTreeProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const treeMethods = useRef<TreeMethods>(null);
|
||||
|
||||
const { mutate: modifyTreeMenu } = useMutation({
|
||||
mutationFn: (newTree: TreeMenu) =>
|
||||
mutationFn: (newTree: TreeNode) =>
|
||||
MenuApi.modifyTreeMenu(siteId, [newTree]),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -33,50 +51,100 @@ function MenuTree({ treeData, siteId }: MenuTreeProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedNode, setSelectedNode] = useState<NodeModel<Menu> | null>(
|
||||
null,
|
||||
);
|
||||
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}
|
||||
rootId={0}
|
||||
render={(node: NodeModel<Menu>, { depth, isOpen, onToggle }) => (
|
||||
<MenuNode
|
||||
node={node}
|
||||
depth={depth}
|
||||
isOpen={isOpen}
|
||||
isSelected={selectedNode?.id === node.id}
|
||||
onToggle={onToggle}
|
||||
onSelect={handleSelectNode(setSelectedNode)}
|
||||
<>
|
||||
<div className="max-h-[calc(100vh-33.5rem)] overflow-auto">
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
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",
|
||||
container: "flex flex-col",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
placeholder: "relative",
|
||||
}}
|
||||
sort={false}
|
||||
dropTargetOffset={5}
|
||||
insertDroppableFirst={false}
|
||||
canDrop={(_, { dragSource, dropTargetId }) => {
|
||||
if (dragSource?.parent === dropTargetId) {
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
placeholderRender={(_, { depth }) => {
|
||||
return (
|
||||
<div
|
||||
className="absolute right-0 top-0 h-[0.2rem] bg-primary"
|
||||
style={{ left: depth * 10 + 5 }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onDrop={handleDropNode(modifyTreeMenu)}
|
||||
classes={{
|
||||
root: "p-4",
|
||||
container: "flex flex-col",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
placeholder: "relative",
|
||||
}}
|
||||
sort={false}
|
||||
dropTargetOffset={5}
|
||||
insertDroppableFirst={false}
|
||||
canDrop={(_, { dragSource, dropTargetId }) => {
|
||||
if (dragSource?.parent === dropTargetId) {
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
placeholderRender={(_, { depth }) => {
|
||||
return (
|
||||
<div
|
||||
className="absolute right-0 top-0 h-[0.2rem] bg-primary"
|
||||
style={{ left: depth * 10 + 5 }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
</DndProvider>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex-1 items-center px-0"
|
||||
onClick={() => {
|
||||
treeMethods.current?.openAll();
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-arrows-angle-expand" />
|
||||
전체펼침
|
||||
</Button>
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex-1 items-center px-0"
|
||||
onClick={() => {
|
||||
treeMethods.current?.closeAll();
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-arrows-angle-contract" />
|
||||
전체접기
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
"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 OrganizationSearchDialogProps {
|
||||
handleUserSelect: (user: User) => void;
|
||||
}
|
||||
|
||||
function OrganizationSearchDialog({
|
||||
handleUserSelect,
|
||||
}: OrganizationSearchDialogProps) {
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [tree, setTree] = useState<NodeModel<TreeModel>[]>([]);
|
||||
const [selectedNode, setSelectedNode] = useState<NodeModel<TreeModel> | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const { data: treeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.USER_OGNZ],
|
||||
queryFn: () => OrganizationApi.getOrganizationUserTreeList(),
|
||||
select: (data) => generateOrganizationTreeData(data),
|
||||
});
|
||||
|
||||
const handleSelectNode = (node: NodeModel<TreeModel>) => {
|
||||
setSelectedNode(node);
|
||||
handleUserSelect(node.data as User);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTree(treeData ?? []);
|
||||
}, [treeData]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3">
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[30rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>담당자 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||
<Input
|
||||
className="h-[2.5rem] !text-[1.3rem]"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setTree(filterOrganizationTreeDataByKeyword(tree, keyword))
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-search text-base" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[36rem] overflow-auto">
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
render={(
|
||||
node: NodeModel<TreeModel>,
|
||||
{ depth, isOpen, onToggle },
|
||||
) => (
|
||||
<TreeNode
|
||||
node={node}
|
||||
depth={depth}
|
||||
isOpen={isOpen}
|
||||
selectedNode={selectedNode}
|
||||
onToggle={onToggle}
|
||||
onSelect={handleSelectNode}
|
||||
/>
|
||||
)}
|
||||
classes={{
|
||||
container: "flex flex-col",
|
||||
}}
|
||||
sort={false}
|
||||
onDrop={() => {}}
|
||||
/>
|
||||
</DndProvider>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
interface TreeNodeProps {
|
||||
node: NodeModel<TreeModel>;
|
||||
depth: number;
|
||||
isOpen: boolean;
|
||||
selectedNode: NodeModel<TreeModel> | null;
|
||||
onToggle: (id: NodeModel["id"]) => void;
|
||||
onSelect: (node: NodeModel<TreeModel>) => 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 (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
className="group flex cursor-pointer items-center justify-between gap-2 rounded-sm p-2 hover:bg-accent"
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
{data && ("children" in data || "users" in data) && (
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-6 w-6 !bg-transparent"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward mr-2 text-base text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
<span className="text-[1.4rem] font-medium text-label">{text}</span>
|
||||
</div>
|
||||
{isSelected && <i className="diveicon di-check text-xl text-primary" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { OrganizationSearchDialog };
|
||||
@@ -1,22 +0,0 @@
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import { Table, TableBody } from "@common/components";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
import { BaseBottomMenuForm } from "./base-bottom-menu-form";
|
||||
|
||||
export function OutsideMenuForm({
|
||||
form,
|
||||
}: {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
}) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<BaseBottomMenuForm form={form} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Button, Dialog, DialogTrigger } from "@common/components/ui";
|
||||
|
||||
type PermissionsManagementDialogProps = {
|
||||
menuCd?: number;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function PermissionsManagementDialog({
|
||||
...props
|
||||
}: PermissionsManagementDialogProps) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3">
|
||||
<i className="diveicon di-personal-information" />
|
||||
권한관리
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PermissionsManagementDialog };
|
||||
@@ -0,0 +1,380 @@
|
||||
import dayjs from "dayjs";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
DatePicker,
|
||||
IconFinder,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import type { BoardPopup } from "@/entities/board";
|
||||
import type { Content } from "../model/content-interface";
|
||||
import type { User } from "../model/organization-interface";
|
||||
import type { MenuFormData, MenuType } from "../model/menu-interface";
|
||||
import { OrganizationSearchDialog } from "./organization-search-dialog";
|
||||
import { ContentSearchDialog } from "./content-search-dialog";
|
||||
import { BoardSearchDialog } from "./board-search-dialog";
|
||||
|
||||
type TypeMenuFormProps = {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuType: MenuType;
|
||||
};
|
||||
|
||||
function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
const handleIconClick = (icon: string) => {
|
||||
form.setValue("iconNm", icon);
|
||||
};
|
||||
|
||||
const handleContentSelect = (content: Content) => {
|
||||
form.setValue("menuLnkgSn", content.contsSn);
|
||||
form.setValue("menuLnkgSnNm", content.contsTtl);
|
||||
};
|
||||
|
||||
const handleUserSelect = (user: User) => {
|
||||
form.setValue("menuPicUserUnqId", user.userUnqId);
|
||||
form.setValue("menuPicUserUnqIdNm", user.userNm);
|
||||
};
|
||||
|
||||
const handleBoardSelect = (board: BoardPopup) => {
|
||||
form.setValue("menuLnkgSn", board.bbsId);
|
||||
form.setValue("menuLnkgSnNm", board.bbsNm);
|
||||
};
|
||||
|
||||
const handleDateSelect = (date: Date | undefined) => {
|
||||
if (!date) return;
|
||||
form.setValue("menuPicDsgnYmd", dayjs(date).format("YYYYMMDD"));
|
||||
};
|
||||
|
||||
return (
|
||||
<Table variant="secondary" className="-mt-[0.1rem]">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
{menuType === "CONTENTS" && (
|
||||
<TableRow>
|
||||
<TableHead>콘텐츠</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-[3.2rem] w-1/2 cursor-not-allowed overflow-hidden whitespace-nowrap break-keep rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{!form.watch("menuLnkgSnNm") &&
|
||||
"메뉴에 표시될 콘텐츠를 조회 후 선택하십시오."}
|
||||
{form.watch("menuLnkgSnNm")}
|
||||
</div>
|
||||
<ContentSearchDialog
|
||||
handleContentSelect={handleContentSelect}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{menuType === "BOARD" && (
|
||||
<TableRow>
|
||||
<TableHead>게시판</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-[3.2rem] w-1/2 cursor-not-allowed overflow-hidden whitespace-nowrap break-keep rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{!form.watch("menuLnkgSnNm") &&
|
||||
"메뉴에 표시될 게시판을 조회 후 선택하십시오."}
|
||||
{form.watch("menuLnkgSnNm")}
|
||||
</div>
|
||||
<BoardSearchDialog handleBoardSelect={handleBoardSelect} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{(menuType === "INSIDE" || menuType === "OUTSIDE") && (
|
||||
<TableRow>
|
||||
<TableHead>연결 URL</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="urlPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder={
|
||||
menuType === "INSIDE"
|
||||
? "메뉴에 연결된 페이지의 경로를 입력하십시오."
|
||||
: "'http://' 또는 'https://'를 포함하여 입력하십시오."
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{(menuType === "CONTENTS" ||
|
||||
menuType === "BOARD" ||
|
||||
menuType === "INSIDE" ||
|
||||
menuType === "OUTSIDE") && (
|
||||
<TableRow>
|
||||
<TableHead>연결 형태</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newWndwYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="newWndwY" />
|
||||
<Label htmlFor="newWndwY">새창</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="newWndwN" />
|
||||
<Label htmlFor="newWndwN">현재창</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{(menuType === "CONTENTS" || menuType === "BOARD") && (
|
||||
<TableRow>
|
||||
<TableHead>메뉴 평가 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="evlUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="evlUseY" />
|
||||
<Label htmlFor="evlUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="evlUseN" />
|
||||
<Label htmlFor="evlUseN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{/* 메뉴 유형 공통 폼 S */}
|
||||
<TableRow>
|
||||
<TableHead>메뉴 서브명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuSubKornNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트맵에서 다른 이름으로 사용할 경우 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>메뉴 아이콘</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{!form.watch("iconNm") && "아이콘을 조회 후 선택하십시오."}
|
||||
{form.watch("iconNm")}
|
||||
</div>
|
||||
<IconFinder handleClick={handleIconClick} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>출력 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="otptYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="otptY" />
|
||||
<Label htmlFor="otptY">출력</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="otptN" />
|
||||
<Label htmlFor="otptN">출력안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/* 메뉴 유형 공통 폼 E */}
|
||||
|
||||
{(menuType === "CONTENTS" || menuType === "BOARD") && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead>QR코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="qrcd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="qrcdY" />
|
||||
<Label htmlFor="qrcdY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="qrcdN" />
|
||||
<Label htmlFor="qrcdN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>담당자</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{!form.watch("menuPicUserUnqIdNm") &&
|
||||
"메뉴 담당자를 조회 후 선택하십시오."}
|
||||
{form.watch("menuPicUserUnqIdNm")}
|
||||
</div>
|
||||
<OrganizationSearchDialog
|
||||
handleUserSelect={handleUserSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<b>지정일:</b>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuPicDsgnYmd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
mode="single"
|
||||
selected={
|
||||
field.value
|
||||
? dayjs(field.value).toDate()
|
||||
: undefined
|
||||
}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
handleDateSelect(date);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { TypeMenuForm };
|
||||
@@ -4,11 +4,11 @@ import type {
|
||||
Domain,
|
||||
DomainService,
|
||||
DomainList,
|
||||
IDomainParams,
|
||||
DomainParams,
|
||||
} from "../model/domain-interface";
|
||||
|
||||
export class DomainApiService implements DomainService {
|
||||
public async getDomainList(params: IDomainParams): Promise<DomainList> {
|
||||
public async getDomainList(params: DomainParams): Promise<DomainList> {
|
||||
const response = await client.get<DomainList>(
|
||||
`${ENDPOINT.CMS_SERVICE.DOMAINS}`,
|
||||
{ params },
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { IIpParams, IP, IpList, IpService } from "../model/ip-interface";
|
||||
import type { IpParams, IP, IpList, IpService } from "../model/ip-interface";
|
||||
|
||||
export class IpApiService implements IpService {
|
||||
public async getIpList(params: IIpParams): Promise<IpList> {
|
||||
public async getIpList(params: IpParams): Promise<IpList> {
|
||||
const response = await client.get<IpList>(`${ENDPOINT.CMS_SERVICE.IPS}`, {
|
||||
params,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import type { IPageableParams, PageableList } from "@/shared/model";
|
||||
import type { PageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface IDomainParams extends IPageableParams {
|
||||
export interface DomainParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
siteId?: string;
|
||||
@@ -23,9 +23,9 @@ export const domainFormSchema = z.object({
|
||||
|
||||
export type Domain = z.infer<typeof domainSchema>;
|
||||
export type DomainFormData = z.infer<typeof domainFormSchema>;
|
||||
export type DomainList = PageableList<Domain[]>;
|
||||
export type DomainList = PageableList<Domain>;
|
||||
|
||||
export interface DomainService {
|
||||
getDomainList(params: IDomainParams): Promise<DomainList>;
|
||||
getDomainList(params: DomainParams): Promise<DomainList>;
|
||||
saveDomainList(body: Domain[]): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import type { IPageableParams, PageableList } from "@/shared/model";
|
||||
import type { PageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface IIpParams extends IPageableParams {
|
||||
export interface IpParams extends PageableParams {
|
||||
siteId: string;
|
||||
ipAddr?: string;
|
||||
prmYn?: string;
|
||||
@@ -21,9 +21,9 @@ export const ipFormSchema = z.object({
|
||||
|
||||
export type IP = z.infer<typeof ipSchema>;
|
||||
export type IPFormData = z.infer<typeof ipFormSchema>;
|
||||
export type IpList = PageableList<IP[]>;
|
||||
export type IpList = PageableList<IP>;
|
||||
|
||||
export interface IpService {
|
||||
getIpList(params: IIpParams): Promise<IpList>;
|
||||
getIpList(params: IpParams): Promise<IpList>;
|
||||
saveIpList(body: IP[]): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import dayjs from "dayjs";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import type { ISite } from "@/entities/site";
|
||||
import type { Site } from "@/entities/site";
|
||||
|
||||
const columnHelper = createColumnHelper<ISite>();
|
||||
const columnHelper = createColumnHelper<Site>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor((_, index) => index + 1, {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input } from "@common/components";
|
||||
import { SiteApi, type ISite, type ISiteParams } from "@/entities/site";
|
||||
import { SiteApi, type Site, type SiteParams } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { columns } from "../model/table-columns";
|
||||
@@ -20,7 +20,7 @@ function SitePage() {
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const [siteQuery, setSiteQuery] = useState<ISiteParams>({
|
||||
const [siteQuery, setSiteQuery] = useState<SiteParams>({
|
||||
siteNm: "",
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ function SitePage() {
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: ISite) => {
|
||||
const handleRowClick = (row: Site) => {
|
||||
router.push(`/site/register/${row.siteId}`);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ export const ENDPOINT = {
|
||||
IPS: "/cms-service/api/v1/site-ips",
|
||||
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",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ interface PageableConfig {
|
||||
}
|
||||
|
||||
export interface PageableList<T> {
|
||||
content: T;
|
||||
content: T[];
|
||||
pageable: PageableConfig;
|
||||
sort: SortConfig;
|
||||
totalPages: number;
|
||||
@@ -27,7 +27,7 @@ export interface PageableList<T> {
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
export interface IPageableParams {
|
||||
export interface PageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
|
||||
@@ -10,9 +10,13 @@ import type { QueryClient } from "@tanstack/react-query";
|
||||
export function BaseLayout({
|
||||
children,
|
||||
queryClient,
|
||||
currentPath,
|
||||
routeHandler,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient?: QueryClient;
|
||||
currentPath?: string;
|
||||
routeHandler?: (path: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider client={queryClient}>
|
||||
@@ -20,7 +24,7 @@ export function BaseLayout({
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
<div className="flex h-[calc(100vh-50px)]">
|
||||
<LNB />
|
||||
<LNB currentPath={currentPath} routeHandler={routeHandler} />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="max-h-[calc(100%-50px)] flex-1 overflow-auto">
|
||||
{children}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { cn, setCookie } from "../../lib";
|
||||
import { ENDPOINT, SITE_ID } from "../../config";
|
||||
import { getMenuList, type MenuItem } from "../../api";
|
||||
import {
|
||||
@@ -19,7 +19,15 @@ import {
|
||||
} from "../collapsible";
|
||||
import { getParentIds } from "./lib";
|
||||
|
||||
export function LNB({ viewOnly }: { viewOnly?: boolean }) {
|
||||
export function LNB({
|
||||
viewOnly,
|
||||
currentPath,
|
||||
routeHandler,
|
||||
}: {
|
||||
viewOnly?: boolean;
|
||||
currentPath?: string;
|
||||
routeHandler?: (path: string) => void;
|
||||
}) {
|
||||
const { data: menuList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
queryFn: () => getMenuList(SITE_ID),
|
||||
@@ -28,14 +36,31 @@ export function LNB({ viewOnly }: { viewOnly?: boolean }) {
|
||||
|
||||
const [openMenuIds, setOpenMenuIds] = useState<number[]>([]);
|
||||
|
||||
function isPathMatched(path: string) {
|
||||
return currentPath === path || currentPath?.startsWith(`${path}/`);
|
||||
}
|
||||
|
||||
function MenuItem({ item }: { item: MenuItem }) {
|
||||
const isMatched = isPathMatched(item.urlPath);
|
||||
|
||||
const handleClick = () => {
|
||||
setCookie("menu-code-key", item.menuCd.toString());
|
||||
routeHandler?.(item.urlPath);
|
||||
};
|
||||
|
||||
if (item.children) {
|
||||
return <MenuGroup item={item} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="h-auto py-4 text-[1.4rem] text-label transition-colors hover:bg-accent">
|
||||
<SidebarMenuButton
|
||||
className={cn(
|
||||
"h-auto py-4 text-[1.4rem] text-label transition-colors hover:bg-accent",
|
||||
isMatched && "bg-accent text-primary",
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<i className={cn("diveicon ml-2", `di-${item.iconNm}`)} />
|
||||
{item.menuKornNm}
|
||||
</SidebarMenuButton>
|
||||
@@ -44,7 +69,11 @@ export function LNB({ viewOnly }: { viewOnly?: boolean }) {
|
||||
}
|
||||
|
||||
function MenuGroup({ item }: { item: MenuItem }) {
|
||||
const isOpen = openMenuIds.includes(item.menuCd);
|
||||
const hasMatchedChild = item.children?.some((child) =>
|
||||
isPathMatched(child.urlPath),
|
||||
);
|
||||
|
||||
const isOpen = openMenuIds.includes(item.menuCd) || hasMatchedChild;
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
|
||||
@@ -79,11 +79,7 @@ const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn("align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
<th ref={ref} className={cn("align-middle", className)} {...props} />
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
@@ -93,10 +89,7 @@ const TableCell = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"align-middle text-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
className={cn("align-middle text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user