iipbs_cs #12
@@ -0,0 +1,2 @@
|
||||
import { MenuPage } from "@/pages/menu";
|
||||
export default MenuPage;
|
||||
@@ -13,6 +13,7 @@
|
||||
"@common/components": "workspace:*",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@minoru/react-dnd-treeview": "^3.5.0",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
@@ -23,6 +24,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"next": "15.1.7",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.24.2",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export { SiteApi } from "./api/site-service";
|
||||
export {
|
||||
siteFormSchema,
|
||||
type ISiteParams,
|
||||
type ISite,
|
||||
type SiteFormData,
|
||||
} from "./model/site-interface";
|
||||
@@ -0,0 +1,22 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { Menu, MenuService, TreeMenu } from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
public async getMenuList(siteId: string): Promise<Menu[]> {
|
||||
const response = await client.get<Menu[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async modifyMenu(siteId: string, data: TreeMenu[]): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { MenuPage } from "./ui/menu-page";
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type { Menu, TreeMenu } from "../model/menu-interface";
|
||||
|
||||
export const generateTreeData = (data: Menu[]): NodeModel<Menu>[] => {
|
||||
return data.reduce<NodeModel<Menu>[]>((acc, menu) => {
|
||||
const node: NodeModel<Menu> = {
|
||||
id: menu.menuCd ?? 0,
|
||||
parent: menu.parentId ?? 0,
|
||||
text: menu.menuKornNm,
|
||||
droppable: true,
|
||||
data: menu,
|
||||
};
|
||||
|
||||
acc.push(node);
|
||||
|
||||
if (menu.children?.length) {
|
||||
acc.push(...generateTreeData(menu.children));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const generateNewTreeData = (
|
||||
treeData: NodeModel<Menu>[],
|
||||
options: DropOptions<Menu>,
|
||||
): TreeMenu | null => {
|
||||
const { dragSourceId, dragSource, dropTargetId, dropTarget } = options;
|
||||
if (!dragSource?.data) return null;
|
||||
|
||||
const childNodes = treeData.filter(
|
||||
(node) => node.parent === dragSourceId && node.data,
|
||||
);
|
||||
|
||||
const baseNode = (node: NodeModel<Menu>): TreeMenu => {
|
||||
if (!node.data) return {};
|
||||
return {
|
||||
menuCd: node.data?.menuCd,
|
||||
name: node.data?.menuKornNm,
|
||||
sortSeq: node.data?.sortSeq,
|
||||
icon: node.data?.iconNm,
|
||||
};
|
||||
};
|
||||
|
||||
const treeMenu: TreeMenu = {
|
||||
...baseNode(dragSource),
|
||||
parentId: dropTargetId === 0 ? null : (dropTargetId as number),
|
||||
levelNo: (dropTarget?.data?.levelNo ?? 0) + 1,
|
||||
};
|
||||
|
||||
const result: TreeMenu = {
|
||||
...treeMenu,
|
||||
children: childNodes.map((child) => ({
|
||||
...baseNode(child),
|
||||
parentId: child.data?.parentId === 0 ? null : (dropTargetId as number),
|
||||
levelNo: (treeMenu?.levelNo ?? 0) + 1,
|
||||
})),
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const MENU_TYPE = {
|
||||
EMPTY: "EMPTY",
|
||||
CONTENTS: "CONTENTS",
|
||||
BOARD: "BOARD",
|
||||
INSIDE: "INSIDE",
|
||||
OUTSIDE: "OUTSIDE",
|
||||
BOTTOM: "BOTTOM",
|
||||
} as const;
|
||||
|
||||
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
|
||||
|
||||
export interface TreeMenu {
|
||||
menuCd?: number;
|
||||
name?: string;
|
||||
sortSeq?: number;
|
||||
parentId?: number | null;
|
||||
levelNo?: number;
|
||||
icon?: string;
|
||||
children?: TreeMenu[];
|
||||
}
|
||||
|
||||
export interface Menu extends Omit<TreeMenu, "name" | "icon" | "children"> {
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
iconNm: string;
|
||||
children: Menu[];
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
export const menuFormSchema = z.object({
|
||||
siteId: z.string(),
|
||||
menuKornNm: z.string(),
|
||||
menuSubKornNm: z.string().nullish(),
|
||||
menuEngNm: z.string().nullish(),
|
||||
menuSubEngNm: z.string().nullish(),
|
||||
levelNo: z.number(),
|
||||
urlPath: z.string().nullish(),
|
||||
iconNm: z.string().nullish(),
|
||||
menuLnkgTypeCd: z.enum([
|
||||
MENU_TYPE.EMPTY,
|
||||
MENU_TYPE.CONTENTS,
|
||||
MENU_TYPE.BOARD,
|
||||
MENU_TYPE.INSIDE,
|
||||
MENU_TYPE.OUTSIDE,
|
||||
MENU_TYPE.BOTTOM,
|
||||
]),
|
||||
menuLnkgSn: z.number().nullish(),
|
||||
otptYn: z.enum(["Y", "N"]).default("Y"),
|
||||
prntMenuCd: z.number().nullish(),
|
||||
sortSeq: z.number(),
|
||||
evlUseYn: z.enum(["Y", "N"]).nullish(),
|
||||
qrcd: z.string().nullish(),
|
||||
useYn: z.enum(["Y", "N"]).default("Y"),
|
||||
});
|
||||
|
||||
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||
|
||||
export interface MenuService {
|
||||
getMenuList: (siteId: string) => Promise<Menu[]>;
|
||||
modifyMenu: (siteId: string, data: TreeMenu[]) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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} 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} 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
|
||||
|
||||
interface BaseMenuFormProps {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuId?: number;
|
||||
}
|
||||
|
||||
export function BaseMenuForm({ form, menuId }: BaseMenuFormProps) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<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>
|
||||
<TableRow>
|
||||
<TableHead>메뉴명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuKornNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="메뉴명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>메뉴 영문명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuEngNm"
|
||||
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">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuLnkgTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
{Object.entries(MENU_TYPE).map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={value}
|
||||
id={`menuType${key}`}
|
||||
/>
|
||||
<Label htmlFor={`menuType${key}`}>
|
||||
{key === "EMPTY" && "빈메뉴"}
|
||||
{key === "CONTENTS" && "콘텐츠"}
|
||||
{key === "BOARD" && "게시판"}
|
||||
{key === "INSIDE" && "내부링크"}
|
||||
{key === "OUTSIDE" && "외부링크"}
|
||||
{key === "BOTTOM" && "하단메뉴"}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
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,22 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
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,18 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useState } from "react";
|
||||
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 { 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";
|
||||
|
||||
interface MenuFormProps {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuId?: number;
|
||||
handleSave: (data: MenuFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function MenuForm({
|
||||
form,
|
||||
menuId,
|
||||
handleSave,
|
||||
handleDelete,
|
||||
}: MenuFormProps) {
|
||||
const isModifyMode = !!menuId;
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
|
||||
const onSubmit = async (data: MenuFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
setOnConfirm(true);
|
||||
if (await confirm(confirmMessage)) {
|
||||
setOnConfirm(false);
|
||||
handleSave(data);
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleDelete?.();
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const menuType = form.watch("menuLnkgTypeCd");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
메뉴 등록/수정
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<BaseMenuForm form={form} menuId={menuId} />
|
||||
<div className="-mt-[0.1rem]">
|
||||
{renderMenuTypeComponent(menuType, form)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components/ui";
|
||||
import { SiteApi } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import {
|
||||
MENU_TYPE,
|
||||
menuFormSchema,
|
||||
type MenuFormData,
|
||||
} from "../model/menu-interface";
|
||||
import { MenuTree } from "./menu-tree";
|
||||
import { MenuForm } from "./menu-form";
|
||||
import { generateTreeData } from "../lib/utils";
|
||||
|
||||
function MenuPage() {
|
||||
const [siteId, setSiteId] = useState("");
|
||||
|
||||
const { data: treeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
queryFn: () => MenuApi.getMenuList(siteId),
|
||||
select: (data) => generateTreeData(data),
|
||||
enabled: !!siteId,
|
||||
});
|
||||
|
||||
const { data: options } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.getSiteList({}),
|
||||
select: (data) =>
|
||||
data.content.map((site) => ({
|
||||
label: site.siteNm,
|
||||
value: site.siteId,
|
||||
})),
|
||||
});
|
||||
|
||||
const form = useForm<MenuFormData>({
|
||||
resolver: zodResolver(menuFormSchema),
|
||||
defaultValues: {
|
||||
menuLnkgTypeCd: MENU_TYPE.EMPTY,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSave = async (data: MenuFormData) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {};
|
||||
|
||||
return (
|
||||
<PageLayout pageTitle="메뉴 관리">
|
||||
<div className="card flex gap-2">
|
||||
<div className="card card-border">
|
||||
<Select value={siteId} onValueChange={setSiteId}>
|
||||
<SelectTrigger className="w-[18rem]">
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="card card-border flex-1">
|
||||
<MenuForm
|
||||
form={form}
|
||||
handleSave={handleSave}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuPage };
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Tree,
|
||||
MultiBackend,
|
||||
getBackendOptions,
|
||||
type NodeModel,
|
||||
type DropOptions,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import type { Menu, TreeMenu } from "../model/menu-interface";
|
||||
import { generateNewTreeData } from "../lib/utils";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
type MenuTreeProps = {
|
||||
treeData: NodeModel<Menu>[];
|
||||
siteId: string;
|
||||
};
|
||||
|
||||
function MenuTree({ treeData, siteId }: MenuTreeProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: ({ data }: { data: TreeMenu[] }) =>
|
||||
MenuApi.modifyMenu(siteId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedNode, setSelectedNode] = useState<NodeModel<Menu> | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleSelect = (node: NodeModel<Menu>) => setSelectedNode(node);
|
||||
|
||||
const handleDrop = (
|
||||
newTree: NodeModel<Menu>[],
|
||||
options: DropOptions<Menu>,
|
||||
) => {
|
||||
const newTreeData = generateNewTreeData(newTree, options);
|
||||
if (!newTreeData) return;
|
||||
mutate({ data: [newTreeData] });
|
||||
};
|
||||
|
||||
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={handleSelect}
|
||||
/>
|
||||
)}
|
||||
onDrop={handleDrop}
|
||||
classes={{
|
||||
root: "p-4",
|
||||
container: "flex flex-col",
|
||||
listItem: "",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
draggingSource: "",
|
||||
placeholder: "",
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type MenuNodeProps = {
|
||||
node: NodeModel<Menu>;
|
||||
depth: number;
|
||||
isOpen: boolean;
|
||||
isSelected: boolean;
|
||||
onToggle: (id: NodeModel["id"]) => void;
|
||||
onSelect: (node: NodeModel<Menu>) => void;
|
||||
};
|
||||
|
||||
function MenuNode({
|
||||
node,
|
||||
depth,
|
||||
isOpen,
|
||||
isSelected,
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: MenuNodeProps) {
|
||||
const { id, text, data } = node;
|
||||
const indent = depth * 10 + 5;
|
||||
|
||||
const handleToggle = () => onToggle(id);
|
||||
const handleSelect = () => onSelect(node);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-2 rounded-sm p-2 hover:bg-accent",
|
||||
isSelected && "!bg-[rgba(32,148,250,0.2)]",
|
||||
)}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{data?.children?.length && (
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward mr-2 text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[1.4rem] font-medium text-label">{text}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuTree };
|
||||
@@ -0,0 +1,22 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import type { ISite } from "./site-interface";
|
||||
import type { ISite } from "@/entities/site";
|
||||
|
||||
const columnHelper = createColumnHelper<ISite>();
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { SiteApi } from "../api/site-service";
|
||||
import { siteFormSchema, type SiteFormData } from "../model/site-interface";
|
||||
import { SitePageLayout } from "./layout";
|
||||
import { SiteForm } from "./site-form";
|
||||
import { SiteApi, siteFormSchema, type SiteFormData } from "@/entities/site";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { SiteForm } from "./site-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
@@ -39,9 +38,9 @@ function CreatePage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<SitePageLayout>
|
||||
<PageLayout pageTitle="사이트 관리">
|
||||
<SiteForm form={form} handleSave={createSite} />
|
||||
</SitePageLayout>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { SiteApi, siteFormSchema, type SiteFormData } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { SiteApi } from "../api/site-service";
|
||||
import { siteFormSchema, type SiteFormData } from "../model/site-interface";
|
||||
import { SitePageLayout } from "./layout";
|
||||
import { SiteForm } from "./site-form";
|
||||
|
||||
function ModifyPage({ siteId }: { siteId: string }) {
|
||||
@@ -63,14 +62,14 @@ function ModifyPage({ siteId }: { siteId: string }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<SitePageLayout>
|
||||
<PageLayout pageTitle="사이트 관리">
|
||||
<SiteForm
|
||||
form={form}
|
||||
siteId={siteId}
|
||||
handleSave={modifySite}
|
||||
handleDelete={deleteSite}
|
||||
/>
|
||||
</SitePageLayout>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { SiteFormData } from "@/entities/site";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import type { SiteFormData } from "../model/site-interface";
|
||||
import type { Domain } from "../model/domain-interface";
|
||||
import { IPManagementDialog } from "./ip-management-dialog";
|
||||
import { DomainManagementDialog } from "./domain-management-dialog";
|
||||
|
||||
@@ -7,11 +7,10 @@ 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 { ENDPOINT } from "@/shared/config";
|
||||
import { SiteApi } from "../api/site-service";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { type ISite, type ISiteParams } from "../model/site-interface";
|
||||
import { SitePageLayout } from "./layout";
|
||||
|
||||
function SitePage() {
|
||||
const router = useRouter();
|
||||
@@ -46,7 +45,7 @@ function SitePage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SitePageLayout>
|
||||
<PageLayout pageTitle="사이트 관리">
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
@@ -73,7 +72,7 @@ function SitePage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SitePageLayout>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export const ENDPOINT = {
|
||||
CODE: "/cms-service/api/v1/code/cache",
|
||||
IPS: "/cms-service/api/v1/site-ips",
|
||||
DOMAINS: "/cms-service/api/v1/site-dmns",
|
||||
MENUS: "/cms-service/api/v1/menus",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
function SitePageLayout({ children }: { children: React.ReactNode }) {
|
||||
function PageLayout({
|
||||
children,
|
||||
pageTitle,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
pageTitle: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container">
|
||||
<h2 className="px-[1.8rem] pt-[2rem] text-[2.4rem] font-semibold text-title">
|
||||
사이트 관리
|
||||
{pageTitle}
|
||||
</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SitePageLayout };
|
||||
export { PageLayout };
|
||||
@@ -0,0 +1,592 @@
|
||||
export default [
|
||||
"account-box",
|
||||
"account-circle",
|
||||
"add-image",
|
||||
"add-Note",
|
||||
"alarm-add",
|
||||
"alarm-clock",
|
||||
"alarm-off",
|
||||
"alarm-on",
|
||||
"alarm",
|
||||
"alternate",
|
||||
"analytics",
|
||||
"android",
|
||||
"announcement",
|
||||
"app-indicator",
|
||||
"app-list",
|
||||
"app-square",
|
||||
"apple",
|
||||
"apps-square",
|
||||
"apps",
|
||||
"archive-fill",
|
||||
"archive",
|
||||
"arrow-90left-down",
|
||||
"arrow-90left-up",
|
||||
"arrow-90left",
|
||||
"arrow-90right-down",
|
||||
"arrow-90right-up",
|
||||
"arrow-90right",
|
||||
"arrow-back-square-fill",
|
||||
"arrow-back",
|
||||
"arrow-down-square-fill",
|
||||
"arrow-down-square",
|
||||
"arrow-down",
|
||||
"arrow-forward-square-fill",
|
||||
"arrow-forward",
|
||||
"arrow-left-right",
|
||||
"arrow-left-square-fill",
|
||||
"arrow-left-square",
|
||||
"arrow-left",
|
||||
"arrow-lr-up-down",
|
||||
"arrow-reflash",
|
||||
"arrow-reset",
|
||||
"arrow-right-square-fill",
|
||||
"arrow-right-square",
|
||||
"arrow-right",
|
||||
"arrow-up-down",
|
||||
"arrow-up-square-fill",
|
||||
"arrow-up-square",
|
||||
"arrow-up",
|
||||
"arrows-angle-contract",
|
||||
"arrows-angle-expand",
|
||||
"arrows-fullscreen",
|
||||
"aspect-ratio-fill",
|
||||
"aspect-ratio",
|
||||
"assignment-ind",
|
||||
"assignment-turned-in",
|
||||
"assignment",
|
||||
"asterisk",
|
||||
"atm",
|
||||
"badge-4k-fill",
|
||||
"badge-4k",
|
||||
"badge-8k-fill",
|
||||
"badge-8k",
|
||||
"badge-ad-fill",
|
||||
"badge-ad",
|
||||
"badge-cc-fill",
|
||||
"badge-cc",
|
||||
"badge-hd-fill",
|
||||
"badge-hd",
|
||||
"badge-tm-fill",
|
||||
"badge-tm",
|
||||
"bag-check-fill",
|
||||
"bag-check",
|
||||
"bag-fill",
|
||||
"bag",
|
||||
"bags",
|
||||
"bar-chart-fill",
|
||||
"bar-chart",
|
||||
"battery-charging",
|
||||
"battery-emty",
|
||||
"battery-full",
|
||||
"battery-harf",
|
||||
"bell-alert-box-fill",
|
||||
"bell-alert-box",
|
||||
"bell-alert-fill",
|
||||
"bell-alert",
|
||||
"bell-notification-fill",
|
||||
"bell-notification-on-fill",
|
||||
"bell-notification-on",
|
||||
"bell-notification",
|
||||
"bike",
|
||||
"bluetooth",
|
||||
"boat",
|
||||
"book",
|
||||
"bookmark-check-fill",
|
||||
"bookmark-check",
|
||||
"bookmark-fill",
|
||||
"bookmark-new-fill",
|
||||
"bookmark-new",
|
||||
"bookmark-star-fill",
|
||||
"bookmark-star",
|
||||
"bookmark",
|
||||
"boostzone",
|
||||
"box-arrow-backup",
|
||||
"box-arrow-download",
|
||||
"box-arrow-link-blank",
|
||||
"box-arrow-upload",
|
||||
"box-seam",
|
||||
"box",
|
||||
"braces",
|
||||
"briefcase-fill",
|
||||
"briefcase",
|
||||
"brightness-fill",
|
||||
"brightness",
|
||||
"broadcast-pin",
|
||||
"broadcast",
|
||||
"bug-fill",
|
||||
"bug",
|
||||
"building",
|
||||
"bus",
|
||||
"calculator-fill",
|
||||
"calculator",
|
||||
"calendar-check-fill",
|
||||
"calendar-check",
|
||||
"calendar-day-fill",
|
||||
"calendar-day",
|
||||
"calendar-event-fill",
|
||||
"calendar-event",
|
||||
"calendar-fill",
|
||||
"calendar-week-fill",
|
||||
"calendar-week",
|
||||
"calendar",
|
||||
"call-end",
|
||||
"call",
|
||||
"camera-fill",
|
||||
"camera-video-fill",
|
||||
"camera-video",
|
||||
"camera",
|
||||
"car",
|
||||
"card-fill",
|
||||
"card",
|
||||
"caret-down-fill",
|
||||
"caret-down-square-fill",
|
||||
"caret-down-square",
|
||||
"caret-left-fill",
|
||||
"caret-left-square-fill",
|
||||
"caret-left-square",
|
||||
"caret-right-fill",
|
||||
"caret-right-square-fill",
|
||||
"caret-right-square",
|
||||
"caret-up-fill",
|
||||
"caret-up-square-fill",
|
||||
"caret-up-square",
|
||||
"cart-fill",
|
||||
"cart",
|
||||
"cash-stack",
|
||||
"cash",
|
||||
"cast",
|
||||
"cctv",
|
||||
"chat-box-left-dots-fill",
|
||||
"chat-box-left-dots",
|
||||
"chat-box-left-fill",
|
||||
"chat-box-left-text-fill",
|
||||
"chat-box-left-text",
|
||||
"chat-box-left",
|
||||
"chat-box-right-dots-fill",
|
||||
"chat-box-right-dots",
|
||||
"chat-box-right-fill",
|
||||
"chat-box-right-text-fill",
|
||||
"chat-box-right-text",
|
||||
"chat-box-right",
|
||||
"chat-circle-left-dot-fill",
|
||||
"chat-circle-left-dot",
|
||||
"chat-circle-left-fill",
|
||||
"chat-circle-left-text-fill",
|
||||
"chat-circle-left-text",
|
||||
"chat-circle-left",
|
||||
"chat-circle-right-dot-fill",
|
||||
"chat-circle-right-dot",
|
||||
"chat-circle-right-fill",
|
||||
"chat-circle-right-text-fill",
|
||||
"chat-circle-right-text",
|
||||
"chat-circle-right",
|
||||
"check-box",
|
||||
"check-circle-fill",
|
||||
"check-circle-line",
|
||||
"check-circle",
|
||||
"check-square-fill",
|
||||
"check-square-line",
|
||||
"check-square",
|
||||
"check",
|
||||
"chevron-bar-contract",
|
||||
"chevron-bar-down",
|
||||
"chevron-bar-expand",
|
||||
"chevron-bar-left",
|
||||
"chevron-bar-right",
|
||||
"chevron-bar-up",
|
||||
"chevron-contract",
|
||||
"chevron-double-down",
|
||||
"chevron-double-left",
|
||||
"chevron-double-right",
|
||||
"chevron-double-up",
|
||||
"chevron-down",
|
||||
"chevron-expand",
|
||||
"chevron-left",
|
||||
"chevron-right",
|
||||
"chevron-up",
|
||||
"clipboard-check",
|
||||
"clipboard-data",
|
||||
"clipboard-text",
|
||||
"clipboard",
|
||||
"clock-fill",
|
||||
"clock-history",
|
||||
"clock",
|
||||
"cloud-arrow-down-fill",
|
||||
"cloud-arrow-down",
|
||||
"cloud-arrow-up-fill",
|
||||
"cloud-arrow-up",
|
||||
"cloud-check-fill",
|
||||
"cloud-check",
|
||||
"cloud-fill",
|
||||
"cloud",
|
||||
"code-square",
|
||||
"code",
|
||||
"collection-fill",
|
||||
"collection",
|
||||
"compass-fill",
|
||||
"compass",
|
||||
"cone",
|
||||
"controller-fill",
|
||||
"controller",
|
||||
"copyright-sign",
|
||||
"copyright-symbol",
|
||||
"COVID-19-fill",
|
||||
"COVID-19",
|
||||
"cpu-fill",
|
||||
"cpu",
|
||||
"crop",
|
||||
"cup-fill",
|
||||
"cup-straw",
|
||||
"cup",
|
||||
"cursor-fill",
|
||||
"cursor-text",
|
||||
"cursor",
|
||||
"dash-circle-fill",
|
||||
"dash-circle",
|
||||
"dash-square-fill",
|
||||
"dash-square",
|
||||
"dash",
|
||||
"dashboard",
|
||||
"diagram",
|
||||
"diamond-fill",
|
||||
"diamond",
|
||||
"diamond2",
|
||||
"display-fill",
|
||||
"display",
|
||||
"dns",
|
||||
"door-closed-fill",
|
||||
"door-closed",
|
||||
"download_app",
|
||||
"download",
|
||||
"email-fill-notification",
|
||||
"email-fill-open",
|
||||
"email-fill",
|
||||
"email-notification",
|
||||
"email-open",
|
||||
"email",
|
||||
"emergenc_call",
|
||||
"emergenc",
|
||||
"exclamation-chat-fill",
|
||||
"exclamation-chat",
|
||||
"exclamation-circle-fill",
|
||||
"exclamation-circle",
|
||||
"exclamation-diamond-fill",
|
||||
"exclamation-diamond",
|
||||
"exclamation-square-circle",
|
||||
"exclamation-square-fill",
|
||||
"exclamation-square",
|
||||
"exclamation-triangle-fill",
|
||||
"exclamation-triangle",
|
||||
"exclamation",
|
||||
"exclude",
|
||||
"face-id",
|
||||
"facebook-circle",
|
||||
"facebook-square",
|
||||
"favorite-hart",
|
||||
"favorite-star",
|
||||
"file-copy",
|
||||
"file-excel",
|
||||
"file-image",
|
||||
"file-keynote",
|
||||
"file-numbers",
|
||||
"file-powerpoint",
|
||||
"file-text",
|
||||
"file-word",
|
||||
"file",
|
||||
"filter-circle-fill",
|
||||
"filter-circle",
|
||||
"filter-left",
|
||||
"filter-right",
|
||||
"filter-square-fill",
|
||||
"filter-square",
|
||||
"filter",
|
||||
"finder-folder",
|
||||
"finder",
|
||||
"fingerprint",
|
||||
"flag-fill",
|
||||
"flag",
|
||||
"flashlight",
|
||||
"folder-check",
|
||||
"folder-delet",
|
||||
"folder-fill",
|
||||
"folder-open-fill",
|
||||
"folder-open",
|
||||
"folder-plus",
|
||||
"folder-share",
|
||||
"folder-x",
|
||||
"folder",
|
||||
"font",
|
||||
"fullscreen-exit",
|
||||
"fullscreen",
|
||||
"gear-fill",
|
||||
"gear-square-fill",
|
||||
"gear-system-update",
|
||||
"gear",
|
||||
"gem",
|
||||
"geo-alt",
|
||||
"geo-location",
|
||||
"geo-my-location",
|
||||
"gift-fill",
|
||||
"gift",
|
||||
"graph-down",
|
||||
"graph-up",
|
||||
"grid-2-fill",
|
||||
"grid-3-fill",
|
||||
"grid",
|
||||
"grip-horizontal",
|
||||
"grip-vertical",
|
||||
"group",
|
||||
"group2",
|
||||
"hand-index",
|
||||
"harf",
|
||||
"hart-fill",
|
||||
"hart-half",
|
||||
"hash",
|
||||
"hdd-network",
|
||||
"hdd",
|
||||
"hexagon-fill",
|
||||
"hexagon-harf",
|
||||
"hexagon",
|
||||
"history",
|
||||
"home-fill",
|
||||
"home",
|
||||
"hotel_bed",
|
||||
"hotel",
|
||||
"image-fill",
|
||||
"image",
|
||||
"images",
|
||||
"info-box",
|
||||
"info-circle",
|
||||
"info-fill",
|
||||
"info",
|
||||
"instagram-circle",
|
||||
"instagram-fill",
|
||||
"instagram",
|
||||
"intersect",
|
||||
"justify-left",
|
||||
"justify-right",
|
||||
"justify",
|
||||
"kakao-alim-talk",
|
||||
"kakao-circle",
|
||||
"kakao-firend-talk",
|
||||
"kakao-story-circle",
|
||||
"kakao-story",
|
||||
"kakao-talk",
|
||||
"key",
|
||||
"keyboard",
|
||||
"label",
|
||||
"language",
|
||||
"laptop-fill",
|
||||
"laptop",
|
||||
"layer-back",
|
||||
"layer-front",
|
||||
"layer",
|
||||
"lightning-fill",
|
||||
"lightning",
|
||||
"list-check",
|
||||
"list",
|
||||
"local-airport-land",
|
||||
"local-airport",
|
||||
"local-atm",
|
||||
"local-bar",
|
||||
"local-cafe",
|
||||
"local-dining",
|
||||
"local-ev-station",
|
||||
"local-fastfood",
|
||||
"local-gas-station",
|
||||
"local-hospital",
|
||||
"local-laundry",
|
||||
"local-library",
|
||||
"local-museum",
|
||||
"local-restaurant",
|
||||
"local-store",
|
||||
"lock-open",
|
||||
"lock",
|
||||
"map-fill",
|
||||
"map",
|
||||
"menu-close-fill",
|
||||
"menu-close",
|
||||
"menu-open-fill",
|
||||
"menu-open",
|
||||
"mic-fill",
|
||||
"mic-mute-fill",
|
||||
"mic-mute",
|
||||
"mic",
|
||||
"minus",
|
||||
"mobile-android",
|
||||
"mobile-flip",
|
||||
"mobile-fold",
|
||||
"mobile-ios",
|
||||
"mobile-pad",
|
||||
"mobile-store",
|
||||
"mobile",
|
||||
"moon-crescent",
|
||||
"moon-half",
|
||||
"moon",
|
||||
"more-horiz",
|
||||
"more-vert",
|
||||
"movie-fill",
|
||||
"movie",
|
||||
"music-backward-fill",
|
||||
"music-backward",
|
||||
"music-forward-fill",
|
||||
"music-forward",
|
||||
"music-list",
|
||||
"music-pause-fill",
|
||||
"music-pause",
|
||||
"music-play-fill",
|
||||
"music-play",
|
||||
"music-stop-fill",
|
||||
"music-stop",
|
||||
"music",
|
||||
"naver-blog",
|
||||
"naver-circle",
|
||||
"naver",
|
||||
"news",
|
||||
"option",
|
||||
"paperclip",
|
||||
"payment",
|
||||
"pen-fill",
|
||||
"pen",
|
||||
"pencil-fill",
|
||||
"pencil-square",
|
||||
"pencil",
|
||||
"person-fill",
|
||||
"person",
|
||||
"personal-information",
|
||||
"pet",
|
||||
"power",
|
||||
"print",
|
||||
"problem",
|
||||
"profile-smile",
|
||||
"profile",
|
||||
"push-assemble",
|
||||
"push-fill",
|
||||
"push-notification",
|
||||
"push",
|
||||
"question-answer",
|
||||
"question-circle-fill",
|
||||
"question-circle",
|
||||
"question-diamond-fill",
|
||||
"question-diamond",
|
||||
"question-square-fill",
|
||||
"question-square",
|
||||
"question",
|
||||
"radio",
|
||||
"receipt",
|
||||
"refresh-fill",
|
||||
"refresh",
|
||||
"reset-fill",
|
||||
"reset",
|
||||
"rotation-lock",
|
||||
"rotation",
|
||||
"rwis",
|
||||
"search",
|
||||
"server",
|
||||
"share",
|
||||
"shield",
|
||||
"shift-fill",
|
||||
"shift",
|
||||
"shuffle",
|
||||
"silent",
|
||||
"sim",
|
||||
"slash",
|
||||
"sliders",
|
||||
"smart-watch",
|
||||
"sort-alpha-down-alt",
|
||||
"sort-alpha-down",
|
||||
"sort-alpha-up-alt",
|
||||
"sort-alpha-up",
|
||||
"sort-down-alt",
|
||||
"sort-down",
|
||||
"sort-numeric-down-alt",
|
||||
"sort-numeric-down",
|
||||
"sort-numeric-up-alt",
|
||||
"sort-numeric-up",
|
||||
"sort-up-alt",
|
||||
"sort-up",
|
||||
"sound",
|
||||
"soundwave",
|
||||
"square-fill",
|
||||
"square",
|
||||
"star-circle",
|
||||
"star-fill",
|
||||
"star",
|
||||
"statistics-circle",
|
||||
"statistics",
|
||||
"sticky-fill",
|
||||
"sticky",
|
||||
"stoplights-fill",
|
||||
"stoplights",
|
||||
"sunny",
|
||||
"tab",
|
||||
"table",
|
||||
"terminal-fill",
|
||||
"terminal",
|
||||
"ternnel",
|
||||
"textarea-t",
|
||||
"textarea",
|
||||
"thermometer-full",
|
||||
"thermometer-half",
|
||||
"thermometer",
|
||||
"time-end",
|
||||
"time-full",
|
||||
"time-start",
|
||||
"toggle-off",
|
||||
"toggle-on",
|
||||
"toggle",
|
||||
"train",
|
||||
"trash-fill",
|
||||
"trash",
|
||||
"truck-fill",
|
||||
"truck",
|
||||
"ui-checks-grid",
|
||||
"ui-radios-grid",
|
||||
"upload",
|
||||
"vibration",
|
||||
"vms",
|
||||
"volume-down-fill",
|
||||
"volume-down",
|
||||
"volume-mute-fill",
|
||||
"volume-mute",
|
||||
"volume-off-fill",
|
||||
"volume-off",
|
||||
"volume-up-fill",
|
||||
"volume-up",
|
||||
"weather-cloud-fill",
|
||||
"weather-cloud",
|
||||
"weather-cloudy-gusts",
|
||||
"weather-cloudy-windy",
|
||||
"weather-cloudy",
|
||||
"weather-fahrenheit",
|
||||
"weather-fog",
|
||||
"weather-hail",
|
||||
"weather-humidity",
|
||||
"weather-hurricane",
|
||||
"weather-lightning",
|
||||
"weather-rain",
|
||||
"weather-rainny",
|
||||
"weather-snow",
|
||||
"weather-snowflake",
|
||||
"weather-sunny-overcast",
|
||||
"weather-sunny",
|
||||
"weather-thunderstorm",
|
||||
"weather-umbrella",
|
||||
"weather-windy",
|
||||
"whater",
|
||||
"whather-celsius",
|
||||
"wifi-1",
|
||||
"wifi-2",
|
||||
"wifi-off",
|
||||
"wifi",
|
||||
"window",
|
||||
"x-circle-fill",
|
||||
"x-circle",
|
||||
"x-square-fill",
|
||||
"x-square",
|
||||
"x",
|
||||
"zoom-in",
|
||||
"zoom-out",
|
||||
"server-line",
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { IconFinder } from "./index";
|
||||
|
||||
const meta: Meta<typeof IconFinder> = {
|
||||
title: "UI/IconFinder",
|
||||
component: IconFinder,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof IconFinder>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <IconFinder />,
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "../button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "../dialog";
|
||||
import { cn } from "../../lib";
|
||||
import iconNames from "./data";
|
||||
|
||||
type IconFinderProps = {
|
||||
handleClick: (icon: string) => void;
|
||||
};
|
||||
|
||||
const IconFinder = ({ handleClick }: IconFinderProps) => {
|
||||
const [selectedIcon, setSelectedIcon] = useState("");
|
||||
|
||||
const handleIconClick = (icon: string) => {
|
||||
setSelectedIcon(icon);
|
||||
handleClick(icon);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="leading-none">
|
||||
<i className="diveicon di-search" />
|
||||
아이콘 찾기
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[65rem] max-w-[65rem] overflow-y-auto p-12">
|
||||
<div className="py-4">
|
||||
<div className="grid grid-cols-5 gap-6 text-[2.4rem]">
|
||||
{iconNames.map((icon) => (
|
||||
<IconItem
|
||||
key={icon}
|
||||
icon={icon}
|
||||
selectedIcon={selectedIcon}
|
||||
handleClick={handleIconClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const IconItem = ({
|
||||
icon,
|
||||
selectedIcon,
|
||||
handleClick,
|
||||
}: {
|
||||
icon: string;
|
||||
selectedIcon: string;
|
||||
handleClick: (icon: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex cursor-pointer flex-col items-center gap-2 rounded-md p-4",
|
||||
selectedIcon === icon && "bg-accent",
|
||||
)}
|
||||
onClick={() => handleClick(icon)}
|
||||
>
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-check absolute left-4 top-4 text-[1rem] text-primary",
|
||||
selectedIcon !== icon && "hidden",
|
||||
)}
|
||||
/>
|
||||
<i className={`diveicon di-${icon}`} />
|
||||
<span className="text-center text-[1.2rem]">{icon}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { IconFinder };
|
||||
@@ -7,8 +7,8 @@ export * from "./data-table";
|
||||
export * from "./date-picker";
|
||||
export * from "./dialog";
|
||||
export * from "./dropdown-menu";
|
||||
export * from "./input";
|
||||
export * from "./file-search";
|
||||
export * from "./icon-finder";
|
||||
export * from "./input";
|
||||
export * from "./label";
|
||||
export * from "./layout";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Footer } from "./footer";
|
||||
import "./common.css";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
export function BaseLayout({
|
||||
export function VBaseLayout({
|
||||
children,
|
||||
queryClient,
|
||||
}: {
|
||||
|
||||
Generated
+6
@@ -51,6 +51,9 @@ importers:
|
||||
'@hookform/resolvers':
|
||||
specifier: ^4.1.3
|
||||
version: 4.1.3(react-hook-form@7.54.2)
|
||||
'@minoru/react-dnd-treeview':
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
@@ -81,6 +84,9 @@ importers:
|
||||
react-chartjs-2:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0(chart.js@4.4.8)(react@18.3.1)
|
||||
react-dnd:
|
||||
specifier: ^16.0.1
|
||||
version: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1)
|
||||
react-hook-form:
|
||||
specifier: ^7.54.2
|
||||
version: 7.54.2(react@18.3.1)
|
||||
|
||||
Reference in New Issue
Block a user