Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
MenuItem,
|
||||
MenuFormData,
|
||||
MenuTemporarilyStoredData,
|
||||
SiteMapFormData,
|
||||
} from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
@@ -65,6 +66,14 @@ export class MenuApiService implements MenuService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async publishSiteMap(data: SiteMapFormData["siteMap"]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/sitemap/publish`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
|
||||
@@ -2,8 +2,11 @@ export { MenuApi } from "./api/menu-service";
|
||||
export {
|
||||
MENU_TYPE,
|
||||
menuFormSchema,
|
||||
siteMapFormSchema,
|
||||
type MenuType,
|
||||
type MenuItem,
|
||||
type MenuFormData,
|
||||
type SiteMapFormData,
|
||||
type SiteMapPayloadData,
|
||||
} from "./model/menu-interface";
|
||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||
|
||||
@@ -72,8 +72,19 @@ export const menuFormSchema = z.object({
|
||||
menuPicDsgnYmd: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||
export const siteMapPayloadSchema = z.object({
|
||||
menuCd: z.number(),
|
||||
menuSubKornNm: z.string(),
|
||||
otptYn: z.string(),
|
||||
});
|
||||
|
||||
export const siteMapFormSchema = z.object({
|
||||
siteMap: z.array(siteMapPayloadSchema),
|
||||
});
|
||||
|
||||
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||
export type SiteMapFormData = z.infer<typeof siteMapFormSchema>;
|
||||
export type SiteMapPayloadData = z.infer<typeof siteMapPayloadSchema>;
|
||||
export type Menu = Omit<MenuFormData, "iconNm"> & {
|
||||
menuCd: number;
|
||||
iconName?: string;
|
||||
@@ -92,4 +103,5 @@ export interface MenuService {
|
||||
getMenuTemporarilyStoredData: (
|
||||
menuCd: number,
|
||||
) => Promise<MenuTemporarilyStoredData[]>;
|
||||
publishSiteMap: (data: SiteMapFormData["siteMap"]) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { TreeMenuApi } from "./api/tree-menu-service";
|
||||
export type { TreeMenu, TreeNode } from "./model/tree-menu-interface";
|
||||
+1
@@ -19,6 +19,7 @@ export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
|
||||
qrcd?: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
menuSubKornNm?: string;
|
||||
requiredUrlPath?: boolean;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { ApiLogApi } from "@/entities/apilog";
|
||||
import { type ApiLogParams } from "@/entities/apilog/model/apilog-interface";
|
||||
import { ApiLogApi, type ApiLogParams } from "@/entities/apilog";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/apilog/model/table-columns";
|
||||
|
||||
@@ -16,8 +16,7 @@ import {
|
||||
SelectValue,
|
||||
LoadingWithImage,
|
||||
} from "@common/components";
|
||||
import { LoginlogApi } from "@/entities/loginlog";
|
||||
import { type LoginlogParams } from "@/entities/loginlog/model/loginlog-interface";
|
||||
import { LoginlogApi, type LoginlogParams } from "@/entities/loginlog";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/loginlog/model/table-columns";
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
TreeUserOrganization,
|
||||
TreeUser,
|
||||
} from "@/entities/user-organization";
|
||||
import type { TreeMenu, TreeNode } from "../model/tree-menu-interface";
|
||||
import type { TreeMenu, TreeNode } from "@/entities/tree-menu";
|
||||
|
||||
export const generateMenuTreeData = (
|
||||
data: TreeMenu[],
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useEffect, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
|
||||
import { Button, cn } from "@common/components";
|
||||
import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu";
|
||||
import { useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { TreeMenuApi } from "../api/tree-menu-service";
|
||||
import type { TreeMenu } from "../model/tree-menu-interface";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
type MenuNodeProps = {
|
||||
|
||||
@@ -21,11 +21,10 @@ import {
|
||||
type MenuFormData,
|
||||
} from "@/entities/menu";
|
||||
import { SiteApi } from "@/entities/site";
|
||||
import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { generateMenuTreeData } from "../lib/utils";
|
||||
import { TreeMenuApi } from "../api/tree-menu-service";
|
||||
import type { TreeMenu } from "../model/tree-menu-interface";
|
||||
import { MenuTree } from "./menu-tree";
|
||||
import { MenuForm } from "./menu-form";
|
||||
|
||||
|
||||
@@ -12,8 +12,11 @@ import { Button } from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useMutationWrapper } from "@/shared/lib";
|
||||
import type { MenuFormData } from "@/entities/menu";
|
||||
import { TreeMenuApi } from "../api/tree-menu-service";
|
||||
import type { TreeMenu, TreeNode } from "../model/tree-menu-interface";
|
||||
import {
|
||||
TreeMenuApi,
|
||||
type TreeMenu,
|
||||
type TreeNode,
|
||||
} from "@/entities/tree-menu";
|
||||
import { handleDropNode, handleSelectNode } from "../lib/utils";
|
||||
import { MenuNode } from "./menu-node";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { PrivacyLogPrivacy } from "@/entities/privacylog";
|
||||
import type { PrivacyLogParams } from "@/entities/privacylog/model/privacylog-interface";
|
||||
import {
|
||||
PrivacyLogPrivacy,
|
||||
type PrivacyLogParams,
|
||||
} from "@/entities/privacylog";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/privacylog/model/table-columns";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type { TreeMenu } from "@/entities/tree-menu";
|
||||
|
||||
export const generateMenuTreeData = (
|
||||
data: TreeMenu[],
|
||||
): NodeModel<TreeMenu>[] => {
|
||||
return data.reduce<NodeModel<TreeMenu>[]>((acc, menu) => {
|
||||
const node: NodeModel<TreeMenu> = {
|
||||
id: menu.menuCd ?? 0,
|
||||
parent: menu.parentId ?? 0,
|
||||
text: menu.menuSubKornNm ?? "",
|
||||
droppable: true,
|
||||
data: menu,
|
||||
};
|
||||
|
||||
acc.push(node);
|
||||
|
||||
if (menu.children?.length) {
|
||||
acc.push(...generateMenuTreeData(menu.children));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
@@ -1,6 +1,32 @@
|
||||
import dayjs from "dayjs";
|
||||
import { memo, useState } from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import type { Site } from "@/entities/site";
|
||||
import { TreeMenuApi } from "@/entities/tree-menu";
|
||||
import {
|
||||
MenuApi,
|
||||
siteMapFormSchema,
|
||||
type SiteMapFormData,
|
||||
} from "@/entities/menu";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { MenuTree } from "../ui/menu-tree";
|
||||
import { generateMenuTreeData } from "../lib/utils";
|
||||
|
||||
export const columns: ColumnDef<Site>[] = [
|
||||
{
|
||||
@@ -45,4 +71,81 @@ export const columns: ColumnDef<Site>[] = [
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
header: "관리",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<ActionCell data={row.original} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const ActionCell = memo(({ data }: { data: Site }) => {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: menuTreeData, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, data.siteId],
|
||||
queryFn: () => TreeMenuApi.getTreeMenuList(data.siteId),
|
||||
select: (data) => generateMenuTreeData(data),
|
||||
enabled: !!data.siteId && open,
|
||||
});
|
||||
|
||||
const { mutate: publishSiteMap } = useMutationWrapper({
|
||||
mutationFn: (data: SiteMapFormData["siteMap"]) =>
|
||||
MenuApi.publishSiteMap(data),
|
||||
onSuccess: () => {
|
||||
alert("사이트맵이 저장되었습니다.", () => {
|
||||
setOpen(false);
|
||||
refetch();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<SiteMapFormData>({
|
||||
resolver: zodResolver(siteMapFormSchema),
|
||||
});
|
||||
const fieldArray = useFieldArray({
|
||||
control: form.control,
|
||||
name: "siteMap",
|
||||
});
|
||||
|
||||
const handlePublishSiteMap = async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
publishSiteMap(form.getValues("siteMap"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">사이트맵</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{data.siteNm} - 사이트맵 관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<div className="flex items-center justify-end gap-2 p-2">
|
||||
<span className="text-xl">표시여부</span>
|
||||
<Checkbox checked />
|
||||
</div>
|
||||
<MenuTree tree={menuTreeData ?? []} fieldArray={fieldArray} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handlePublishSiteMap}>저장</Button>
|
||||
<DialogClose asChild>
|
||||
<Button color="white">취소</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
ActionCell.displayName = "ActionCell";
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button, Input } from "@common/components";
|
||||
|
||||
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 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<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 };
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useState } from "react";
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type { UseFieldArrayReturn } from "react-hook-form";
|
||||
import { Button, Checkbox, cn } from "@common/components";
|
||||
import { type TreeMenu } from "@/entities/tree-menu";
|
||||
import type { SiteMapFormData, SiteMapPayloadData } from "@/entities/menu";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
type MenuNodeProps = {
|
||||
node: NodeModel<TreeMenu>;
|
||||
depth: number;
|
||||
isOpen: boolean;
|
||||
fieldArray: UseFieldArrayReturn<SiteMapFormData>;
|
||||
onToggle: (id: NodeModel["id"]) => void;
|
||||
};
|
||||
|
||||
function MenuNode({
|
||||
node,
|
||||
depth,
|
||||
isOpen,
|
||||
fieldArray,
|
||||
onToggle,
|
||||
}: MenuNodeProps) {
|
||||
const { id, text, data } = node;
|
||||
const indent = depth * 10 + 5;
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(text);
|
||||
const [otptYn, setOtptYn] = useState(data?.otptYn ?? "N");
|
||||
|
||||
const updateField = (id: number, newField: SiteMapPayloadData) => {
|
||||
const existingFieldIndex = fieldArray.fields.findIndex(
|
||||
(field) => field.menuCd === id,
|
||||
);
|
||||
if (existingFieldIndex !== -1) {
|
||||
fieldArray.update(existingFieldIndex, newField);
|
||||
} else {
|
||||
fieldArray.append(newField);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = () => onToggle(id);
|
||||
|
||||
const handleSave = async () => {
|
||||
const newField = {
|
||||
menuCd: id as number,
|
||||
menuSubKornNm: nodeName,
|
||||
otptYn,
|
||||
};
|
||||
updateField(id as number, newField);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditMode(false);
|
||||
setNodeName(text);
|
||||
};
|
||||
|
||||
const handleOtptYnChange = (checked: boolean) => {
|
||||
const newField = {
|
||||
menuCd: id as number,
|
||||
menuSubKornNm: nodeName,
|
||||
otptYn: checked ? "Y" : "N",
|
||||
};
|
||||
updateField(id as number, newField);
|
||||
setOtptYn(checked ? "Y" : "N");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
className={cn(
|
||||
"group flex cursor-pointer items-center gap-2 rounded-sm p-2 hover:bg-accent",
|
||||
editMode && "p-0 pr-2 hover:bg-transparent",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
>
|
||||
{editMode ? (
|
||||
<EditableNode
|
||||
value={nodeName}
|
||||
onChange={(e) => setNodeName(e.target.value)}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{data?.children?.length && (
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward mr-2 text-base text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[1.4rem] font-medium text-label">
|
||||
{nodeName}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="invisible !bg-transparent group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditMode(true);
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Checkbox
|
||||
className="ml-auto"
|
||||
checked={otptYn === "Y"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onCheckedChange={handleOtptYnChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuNode };
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import type { UseFieldArrayReturn } from "react-hook-form";
|
||||
import {
|
||||
Tree,
|
||||
type NodeModel,
|
||||
type TreeMethods,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { Button } from "@common/components";
|
||||
import type { SiteMapFormData } from "@/entities/menu";
|
||||
import { type TreeMenu } from "@/entities/tree-menu";
|
||||
import { MenuNode } from "./menu-node";
|
||||
|
||||
type MenuTreeProps = {
|
||||
tree: NodeModel<TreeMenu>[];
|
||||
fieldArray: UseFieldArrayReturn<SiteMapFormData>;
|
||||
};
|
||||
|
||||
function MenuTree({ tree, fieldArray }: MenuTreeProps) {
|
||||
const treeMethods = useRef<TreeMethods>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-h-[calc(100vh-33.5rem)] min-h-[40vh] overflow-auto">
|
||||
<Tree
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
ref={treeMethods}
|
||||
render={(node: NodeModel<TreeMenu>, props) => {
|
||||
return <MenuNode node={node} fieldArray={fieldArray} {...props} />;
|
||||
}}
|
||||
onDrop={() => {}}
|
||||
canDrag={() => false}
|
||||
canDrop={() => false}
|
||||
classes={{
|
||||
container: "flex flex-col",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
placeholder: "relative",
|
||||
}}
|
||||
sort={false}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuTree };
|
||||
@@ -63,7 +63,7 @@ export function LNB({
|
||||
|
||||
function isPathMatched(path: string) {
|
||||
if (!path) return false;
|
||||
return currentPath?.startsWith(path);
|
||||
return currentPath === path;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user