Merge commit 'c966581a993aa9547a67c65bd4064f7b9b92975f' into iipbs/wr/form_fuction
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
const App = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<html lang="ko" data-theme={theme?.value}>
|
||||
<body>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</body>
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { QueryProvider, StoreProvider } from "@/shared/providers";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
|
||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
return (
|
||||
<StoreProvider>
|
||||
<QueryProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>{children}</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</QueryProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
{children}
|
||||
<AlertDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</StoreProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,87 @@
|
||||
@tailwind components;
|
||||
.card {
|
||||
position: relative;
|
||||
margin: 0.6rem;
|
||||
box-sizing: border-box;
|
||||
min-height: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.6rem;
|
||||
font-size: 1.3rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card_width10 {width:calc(10% - 12px);}
|
||||
|
||||
.card_width20 {width:calc(20% - 12px);}
|
||||
|
||||
.card_width25 {width:calc(25% - 12px);}
|
||||
|
||||
.card_width30 {width:calc(30% - 12px);}
|
||||
|
||||
.card_width33 {width:calc(33.333% - 12px);}
|
||||
|
||||
.card_width40 {width:calc(40% - 12px);}
|
||||
|
||||
.card_width45 {width:calc(45% - 12px);}
|
||||
|
||||
.card_width50 {width:calc(50% - 12px);}
|
||||
|
||||
.card_width60 {width:calc(60% - 12px);}
|
||||
|
||||
.card_width66 {width:calc(66.666% - 12px);}
|
||||
|
||||
.card_width70 {width:calc(70% - 12px);}
|
||||
|
||||
.card_width75 {width:calc(75% - 12px);}
|
||||
|
||||
.card_width80 {width:calc(80% - 12px);}
|
||||
|
||||
.card_width90 {width:calc(90% - 12px);}
|
||||
|
||||
.card_width100 {width:calc(100% - 12px);}
|
||||
|
||||
.card_nopadding {padding:0 !important;}
|
||||
|
||||
.card_nomargin {width:100%;margin:0 !important;padding:0 !important;}
|
||||
|
||||
.topline {border-top: 1px solid #ccc;}
|
||||
|
||||
.bottomline {border-bottom: 1px solid #e6eef3;background: #fff;}
|
||||
|
||||
.card-border {
|
||||
border: 1px solid #dbe2e6;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-left: -0.6rem;
|
||||
margin-right: -0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e1e7eb;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-left: -0.6rem;
|
||||
margin-right: -0.6rem;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #e1e7eb;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 블랙 테마 */
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
border-color: #3f4042;
|
||||
background-color: rgba(255, 255, 255, 0.065);
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-mode="dark"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
border-bottom-color: #3f4042;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { setUserInfo } = useUserStore((state) => state);
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
||||
queryFn: async () => {
|
||||
async function authCheck() {
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
const response = await client.get<IUserInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
|
||||
);
|
||||
if (!userInfo) setUserInfo(response.data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
const response = await client.get<IUserInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
|
||||
);
|
||||
setUserInfo(response.data);
|
||||
return response;
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
router.push("/auth/login");
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
@@ -11,10 +11,7 @@ import { CodeApi } from "../api/code-service";
|
||||
|
||||
type CodeSelectProps = {
|
||||
upCd: string;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
};
|
||||
} & React.ComponentPropsWithoutRef<typeof Select>;
|
||||
|
||||
function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
|
||||
const { data: code } = useQuery({
|
||||
|
||||
+3
-3
@@ -2,16 +2,16 @@ import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
siteFormSchema,
|
||||
type ISiteInfo,
|
||||
type SiteService,
|
||||
type SiteList,
|
||||
type ISiteDetail,
|
||||
type ISiteParams,
|
||||
type SiteFormData,
|
||||
} from "../model/site-interface";
|
||||
|
||||
export class SiteApiService implements SiteService {
|
||||
public async siteInfo(params: ISiteParams): Promise<ISiteInfo> {
|
||||
const response = await client.get<ISiteInfo>(
|
||||
public async getSiteList(params: ISiteParams): Promise<SiteList> {
|
||||
const response = await client.get<SiteList>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}`,
|
||||
{ params },
|
||||
);
|
||||
@@ -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,59 @@
|
||||
import { z } from "zod";
|
||||
import type { IPageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface ISiteParams extends IPageableParams {
|
||||
siteNm?: string;
|
||||
siteKndCd?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface ISite {
|
||||
siteId: string;
|
||||
siteNm: string;
|
||||
siteKindCode: string;
|
||||
bscSiteYn: string;
|
||||
siteAddr: string;
|
||||
useYn: string;
|
||||
regDt: string;
|
||||
rgTrNm: string;
|
||||
dmnAddr: string;
|
||||
}
|
||||
|
||||
export interface ISiteDetail extends SiteFormData {
|
||||
siteId: string;
|
||||
siteKindNm: string;
|
||||
basicSiteYn: string;
|
||||
faxNumber: string;
|
||||
regDt: string;
|
||||
rgTr: string;
|
||||
mdfcnDt: string;
|
||||
mdFr: string;
|
||||
rgTrNm: string;
|
||||
mdFrNm: string;
|
||||
}
|
||||
|
||||
export const siteFormSchema = z.object({
|
||||
siteId: z.string().max(2).nonempty(),
|
||||
siteNm: z.string().max(100).nonempty(),
|
||||
siteExpln: z.string().max(1000).nullable(),
|
||||
siteKndCd: z.string().max(10).nonempty(),
|
||||
siteAddr: z.string().max(2000).nullable(),
|
||||
faxNo: z.string().max(100).nullable(),
|
||||
telNo: z.string().max(1000).nullable(),
|
||||
lwndCn: z.string().max(2000).nullable(),
|
||||
bscSiteYn: z.string(),
|
||||
bkmkIcon: z.string().max(100).nullable(),
|
||||
useYn: z.string(),
|
||||
siteSkn: z.string().max(10).nullable(),
|
||||
dmnAddr: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type SiteFormData = z.infer<typeof siteFormSchema>;
|
||||
export type SiteList = PageableList<ISite[]>;
|
||||
|
||||
export interface SiteService {
|
||||
getSiteList(params: ISiteParams): Promise<SiteList>;
|
||||
siteDetail(siteId: string): Promise<ISiteDetail>;
|
||||
modifySite(siteId: string, data: SiteFormData): Promise<void>;
|
||||
createSite(data: SiteFormData): Promise<void>;
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import Image from "next/image";
|
||||
import { images } from "@common/assets";
|
||||
import { LoginApi } from "@/entities/auth";
|
||||
import "./login.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
function LoginPage() {
|
||||
const router = useRouter();
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -13,7 +15,8 @@ function LoginPage() {
|
||||
const userId = formData.get("id") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
LoginApi.login({ userId, password });
|
||||
await LoginApi.login({ userId, password });
|
||||
router.push("/site");
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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 getTreeMenuList(siteId: string): Promise<Menu[]> {
|
||||
const response = await client.get<Menu[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async modifyTreeMenu(siteId: string, data: TreeMenu[]): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async modifyMenuName(menuCd: number, name: string): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { MenuPage } from "./ui/menu-page";
|
||||
@@ -0,0 +1,98 @@
|
||||
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 siblings = treeData.filter((node) => node.parent === dropTargetId);
|
||||
|
||||
const newSortSeq =
|
||||
siblings.findIndex((sibling) => sibling.id === dragSourceId) + 1;
|
||||
|
||||
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,
|
||||
icon: node.data?.iconNm,
|
||||
};
|
||||
};
|
||||
|
||||
const treeMenu: TreeMenu = {
|
||||
...baseNode(dragSource),
|
||||
parentId: dropTargetId === 0 ? null : (dropTargetId as number),
|
||||
sortSeq: newSortSeq,
|
||||
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),
|
||||
sortSeq: child.data?.sortSeq,
|
||||
levelNo: (treeMenu?.levelNo ?? 0) + 1,
|
||||
})),
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const handleSelectNode =
|
||||
(callback: (node: NodeModel<Menu>) => void) => (node: NodeModel<Menu>) => {
|
||||
callback(node);
|
||||
};
|
||||
|
||||
export const handleDropNode =
|
||||
(callback: (newTree: TreeMenu) => void) =>
|
||||
(newTree: NodeModel<Menu>[], options: DropOptions<Menu>) => {
|
||||
const newTreeData = generateNewTreeData(newTree, options);
|
||||
if (!newTreeData) return;
|
||||
callback(newTreeData);
|
||||
};
|
||||
|
||||
export const handleNodeTextChange =
|
||||
(treeData: NodeModel<Menu>[]) =>
|
||||
(callback: (newTree: NodeModel<Menu>[]) => void) =>
|
||||
(id: NodeModel["id"], value: string) => {
|
||||
const newTree = treeData.map((node) => {
|
||||
if (node.id === id) {
|
||||
return {
|
||||
...node,
|
||||
text: value,
|
||||
};
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
callback(newTree);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
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").nullish(),
|
||||
prntMenuCd: z.number().nullish(),
|
||||
sortSeq: z.number(),
|
||||
evlUseYn: z.enum(["Y", "N"]).nullish(),
|
||||
qrcd: z.string().nullish(),
|
||||
useYn: z.enum(["Y", "N"]).default("Y").nullish(),
|
||||
});
|
||||
|
||||
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||
|
||||
export interface MenuService {
|
||||
getTreeMenuList: (siteId: string) => Promise<Menu[]>;
|
||||
modifyTreeMenu: (siteId: string, data: TreeMenu[]) => Promise<void>;
|
||||
modifyMenuName: (menuCd: number, name: string) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,116 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { Button, Input } from "@common/components/ui";
|
||||
import type { Menu } from "../model/menu-interface";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
|
||||
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 { mutateAsync: modifyMenuName } = useMutation({
|
||||
mutationFn: (nodeName: string) => {
|
||||
const encodedName = encodeURIComponent(nodeName);
|
||||
return MenuApi.modifyMenuName(id as number, encodedName);
|
||||
},
|
||||
});
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(text);
|
||||
|
||||
const handleToggle = () => onToggle(id);
|
||||
const handleSelect = () => onSelect(node);
|
||||
|
||||
const handleSave = async () => {
|
||||
await modifyMenuName(nodeName);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
className={cn(
|
||||
"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"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{data?.children?.length && (
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward mr-2 text-base text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[1.4rem] font-medium text-label">{text}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{editMode ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={handleSave}
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuNode };
|
||||
@@ -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.getTreeMenuList(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 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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="card card-border flex-1">
|
||||
<MenuForm
|
||||
form={form}
|
||||
handleSave={handleSave}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuPage };
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Tree,
|
||||
MultiBackend,
|
||||
getBackendOptions,
|
||||
type NodeModel,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import type { Menu, TreeMenu } from "../model/menu-interface";
|
||||
import { handleDropNode, handleSelectNode } from "../lib/utils";
|
||||
import { MenuNode } from "./menu-node";
|
||||
|
||||
type MenuTreeProps = {
|
||||
treeData: NodeModel<Menu>[];
|
||||
siteId: string;
|
||||
};
|
||||
|
||||
function MenuTree({ treeData, siteId }: MenuTreeProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: modifyTreeMenu } = useMutation({
|
||||
mutationFn: (newTree: TreeMenu) =>
|
||||
MenuApi.modifyTreeMenu(siteId, [newTree]),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedNode, setSelectedNode] = useState<NodeModel<Menu> | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
Domain,
|
||||
DomainService,
|
||||
DomainList,
|
||||
IDomainParams,
|
||||
} from "../model/domain-interface";
|
||||
|
||||
export class DomainApiService implements DomainService {
|
||||
public async getDomainList(params: IDomainParams): Promise<DomainList> {
|
||||
const response = await client.get<DomainList>(
|
||||
`${ENDPOINT.CMS_SERVICE.DOMAINS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveDomainList(body: Domain[]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.DOMAINS}`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const DomainApi = new DomainApiService();
|
||||
@@ -0,0 +1,22 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { IIpParams, IP, IpList, IpService } from "../model/ip-interface";
|
||||
|
||||
export class IpApiService implements IpService {
|
||||
public async getIpList(params: IIpParams): Promise<IpList> {
|
||||
const response = await client.get<IpList>(`${ENDPOINT.CMS_SERVICE.IPS}`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveIpList(body: IP[]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.IPS}`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const IpApi = new IpApiService();
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from "zod";
|
||||
import type { IPageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface IDomainParams extends IPageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
siteId?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export const domainSchema = z.object({
|
||||
siteDmnNo: z.number().nullish(),
|
||||
siteId: z.string(),
|
||||
dmnAddr: z.string().nonempty(),
|
||||
useYn: z.string(),
|
||||
rprsDmnYn: z.string(),
|
||||
mode: z.string(),
|
||||
});
|
||||
|
||||
export const domainFormSchema = z.object({
|
||||
domains: z.array(domainSchema),
|
||||
});
|
||||
|
||||
export type Domain = z.infer<typeof domainSchema>;
|
||||
export type DomainFormData = z.infer<typeof domainFormSchema>;
|
||||
export type DomainList = PageableList<Domain[]>;
|
||||
|
||||
export interface DomainService {
|
||||
getDomainList(params: IDomainParams): Promise<DomainList>;
|
||||
saveDomainList(body: Domain[]): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { z } from "zod";
|
||||
import type { IPageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface IIpParams extends IPageableParams {
|
||||
siteId: string;
|
||||
ipAddr?: string;
|
||||
prmYn?: string;
|
||||
}
|
||||
|
||||
export const ipSchema = z.object({
|
||||
mngNo: z.number().nullish(),
|
||||
siteId: z.string(),
|
||||
ipAddr: z.string().nonempty(),
|
||||
prmYn: z.string(),
|
||||
mode: z.string(),
|
||||
});
|
||||
|
||||
export const ipFormSchema = z.object({
|
||||
ips: z.array(ipSchema),
|
||||
});
|
||||
|
||||
export type IP = z.infer<typeof ipSchema>;
|
||||
export type IPFormData = z.infer<typeof ipFormSchema>;
|
||||
export type IpList = PageableList<IP[]>;
|
||||
|
||||
export interface IpService {
|
||||
getIpList(params: IIpParams): Promise<IpList>;
|
||||
saveIpList(body: IP[]): Promise<void>;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export interface ISiteParams {
|
||||
siteNm: string;
|
||||
siteKndCd: string;
|
||||
useYn: string;
|
||||
page: number;
|
||||
size: number;
|
||||
sort: string[];
|
||||
}
|
||||
|
||||
export interface ISite {
|
||||
siteId: string;
|
||||
siteNm: string;
|
||||
siteKindCode: string;
|
||||
bscSiteYn: string;
|
||||
siteAddr: string;
|
||||
useYn: string;
|
||||
regDt: string;
|
||||
rgTrNm: string;
|
||||
}
|
||||
|
||||
export interface ISiteInfo {
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
number: number;
|
||||
sort: {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
};
|
||||
size: number;
|
||||
content: ISite[];
|
||||
pageable: {
|
||||
sort: {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
};
|
||||
offset: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
};
|
||||
numberOfElements: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
export interface ISiteDetail extends SiteFormData {
|
||||
siteId: string;
|
||||
siteKindNm: string;
|
||||
basicSiteYn: string;
|
||||
faxNumber: string;
|
||||
regDt: string;
|
||||
rgTr: string;
|
||||
mdfcnDt: string;
|
||||
mdFr: string;
|
||||
rgTrNm: string;
|
||||
mdFrNm: string;
|
||||
}
|
||||
|
||||
export const siteFormSchema = z.object({
|
||||
siteId: z.string().max(2).nonempty(),
|
||||
siteNm: z.string().max(100).nonempty(),
|
||||
siteExpln: z.string().max(1000).optional(),
|
||||
siteKndCd: z.string().max(10).nonempty(),
|
||||
siteAddr: z.string().max(2000).optional(),
|
||||
faxNo: z.string().max(100).optional(),
|
||||
telNo: z.string().max(1000).optional(),
|
||||
lwndCn: z.string().max(2000).optional(),
|
||||
bscSiteYn: z.string(),
|
||||
bkmkIcon: z.string().max(100).optional(),
|
||||
useYn: z.string(),
|
||||
siteSkn: z.string().max(10).optional(),
|
||||
});
|
||||
|
||||
export type SiteFormData = z.infer<typeof siteFormSchema>;
|
||||
|
||||
export interface SiteService {
|
||||
siteInfo(query: ISiteParams): Promise<ISiteInfo>;
|
||||
siteDetail(siteId: string): Promise<ISiteDetail>;
|
||||
modifySite(siteId: string, data: SiteFormData): Promise<void>;
|
||||
createSite(data: SiteFormData): Promise<void>;
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
@@ -17,8 +17,8 @@ const columns = [
|
||||
id: "siteNm",
|
||||
header: "사이트명",
|
||||
}),
|
||||
columnHelper.accessor("siteAddr", {
|
||||
id: "siteAddr",
|
||||
columnHelper.accessor("dmnAddr", {
|
||||
id: "dmnAddr",
|
||||
header: "대표 URL",
|
||||
}),
|
||||
columnHelper.accessor("bscSiteYn", {
|
||||
|
||||
@@ -1,53 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
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 { 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";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
} from "@common/components/ui";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { mutateAsync: createSite } = useMutation({
|
||||
mutationFn: (data: SiteFormData) => SiteApi.createSite(data),
|
||||
onSuccess: () => {
|
||||
setIsCreateSuccess(true);
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const [isCreateSuccess, setIsCreateSuccess] = useState(false);
|
||||
|
||||
const form = useForm<SiteFormData>({
|
||||
resolver: zodResolver(siteFormSchema),
|
||||
defaultValues: {
|
||||
siteId: "",
|
||||
siteNm: "",
|
||||
siteExpln: "",
|
||||
siteKndCd: "",
|
||||
siteAddr: "",
|
||||
faxNo: "",
|
||||
telNo: "",
|
||||
lwndCn: "",
|
||||
bscSiteYn: "",
|
||||
bkmkIcon: "",
|
||||
useYn: "",
|
||||
siteSkn: "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SitePageLayout>
|
||||
<PageLayout pageTitle="사이트 관리">
|
||||
<SiteForm form={form} handleSave={createSite} />
|
||||
<Dialog open={isCreateSuccess} onOpenChange={() => router.back()}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="text-center text-[1.4rem]">등록되었습니다.</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button>확인</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</SitePageLayout>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -13,197 +17,304 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Input,
|
||||
Checkbox,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
|
||||
export type Domain = {
|
||||
id: number;
|
||||
domainUrl: string;
|
||||
isRepresentative: boolean;
|
||||
isActive: "true" | "false";
|
||||
};
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { DomainApi } from "../api/domain-service";
|
||||
import {
|
||||
domainFormSchema,
|
||||
type Domain,
|
||||
type DomainFormData,
|
||||
} from "../model/domain-interface";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
type DomainManagementDialogProps = {
|
||||
triggerDisabled?: boolean;
|
||||
handleSave: (rows: Domain[]) => void;
|
||||
siteId: string;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
handleSaveDomain?: (rows: Domain[]) => void;
|
||||
};
|
||||
|
||||
function DomainManagementDialog({
|
||||
triggerDisabled,
|
||||
handleSave,
|
||||
siteId,
|
||||
handleSaveDomain,
|
||||
...props
|
||||
}: DomainManagementDialogProps) {
|
||||
const [rows, setRows] = useState<Domain[]>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Domain["id"][]>([]);
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const onSave = () => {
|
||||
handleSave(rows);
|
||||
const { data: domainList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.DOMAINS, props.open],
|
||||
queryFn: () => DomainApi.getDomainList({ siteId }),
|
||||
select: (data) => {
|
||||
const domains = data.content.map((item) => ({
|
||||
...item,
|
||||
mode: "U",
|
||||
}));
|
||||
|
||||
initialForm();
|
||||
return domains;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync } = useMutation({
|
||||
mutationFn: (body: Domain[]) => DomainApi.saveDomainList(body),
|
||||
});
|
||||
|
||||
const form = useForm<DomainFormData>({
|
||||
resolver: zodResolver(domainFormSchema),
|
||||
values: {
|
||||
domains: domainList ?? [],
|
||||
},
|
||||
});
|
||||
const { fields, append, remove, update } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "domains",
|
||||
});
|
||||
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const initialForm = () => {
|
||||
form.reset();
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: DomainFormData) => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
await mutateAsync(data.domains);
|
||||
handleSaveDomain?.(data.domains);
|
||||
alert("저장되었습니다.");
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const handleAddDomain = () => {
|
||||
const newDomain: Domain = {
|
||||
id: Date.now(),
|
||||
domainUrl: "",
|
||||
isRepresentative: rows.length === 0,
|
||||
isActive: "true",
|
||||
};
|
||||
setRows([...rows, newDomain]);
|
||||
};
|
||||
|
||||
const handleDeleteDomain = () => {
|
||||
const filteredRows = rows.filter((row) => !rowSelection.includes(row.id));
|
||||
const hasRepresentative = filteredRows.some((row) => row.isRepresentative);
|
||||
|
||||
let updatedRows = filteredRows;
|
||||
if (!hasRepresentative && filteredRows.length > 0) {
|
||||
updatedRows = filteredRows.map((row, index) =>
|
||||
index === 0 ? { ...row, isRepresentative: true } : row,
|
||||
);
|
||||
}
|
||||
|
||||
setRows(updatedRows);
|
||||
setRowSelection([]);
|
||||
};
|
||||
|
||||
const handleDomainUrlChange = (id: number, value: string) => {
|
||||
setRows(
|
||||
rows.map((row) => (row.id === id ? { ...row, domainUrl: value } : row)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRepresentativeChange = (id: number) => {
|
||||
setRows(
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
isRepresentative: row.id === id,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const handleActiveChange = (id: number, value: "true" | "false") => {
|
||||
setRows(
|
||||
rows.map((row) => (row.id === id ? { ...row, isActive: value } : row)),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleRowSelection = (id: number) => {
|
||||
setRowSelection((prev) => {
|
||||
if (prev.includes(id)) {
|
||||
return prev.filter((item) => item !== id);
|
||||
} else {
|
||||
return [...prev, id];
|
||||
}
|
||||
append({
|
||||
siteDmnNo: null,
|
||||
siteId,
|
||||
dmnAddr: "",
|
||||
useYn: "Y",
|
||||
rprsDmnYn: "N",
|
||||
mode: "C",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedRows.length === fields.length) {
|
||||
setSelectedRows([]);
|
||||
} else {
|
||||
setSelectedRows(fields.map((_, index) => index));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRow = (i: number) => {
|
||||
setSelectedRows((prev) =>
|
||||
prev.includes(i) ? prev.filter((j) => j !== i) : [...prev, i],
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteDomain = async () => {
|
||||
if (selectedRows.length === 0) return;
|
||||
|
||||
const sortedIndices = [...selectedRows].sort((a, b) => b - a);
|
||||
sortedIndices.forEach((i) => {
|
||||
const field = fields[i];
|
||||
if (field?.siteDmnNo) {
|
||||
update(i, {
|
||||
...field,
|
||||
mode: "D",
|
||||
});
|
||||
} else {
|
||||
remove(i);
|
||||
}
|
||||
});
|
||||
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const handleChangeRprsDmnYn = (i: number) => {
|
||||
fields.forEach((_, j) => {
|
||||
if (j === i) return;
|
||||
form.setValue(`domains.${j}.rprsDmnYn`, "N");
|
||||
});
|
||||
};
|
||||
|
||||
const isAllSelected =
|
||||
fields.length > 0 && selectedRows.length === fields.length;
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button disabled={triggerDisabled}>도메인 관리</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[60rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>도메인 관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
<Button size="sm" color="white" onClick={handleAddDomain}>
|
||||
항목 추가
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteDomain}
|
||||
disabled={rowSelection.length === 0}
|
||||
>
|
||||
항목 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="12%" />
|
||||
<col width="40%" />
|
||||
<col width="20%" />
|
||||
<col width="20%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>NO</TableHead>
|
||||
<TableHead>선택</TableHead>
|
||||
<TableHead>도메인URL</TableHead>
|
||||
<TableHead>대표URL</TableHead>
|
||||
<TableHead>사용여부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row, idx) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell align="center">{idx + 1}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Checkbox
|
||||
checked={rowSelection.includes(row.id)}
|
||||
onCheckedChange={() => toggleRowSelection(row.id)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Input
|
||||
value={row.domainUrl}
|
||||
onChange={(e) =>
|
||||
handleDomainUrlChange(row.id, e.target.value)
|
||||
}
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Checkbox
|
||||
checked={row.isRepresentative}
|
||||
onCheckedChange={() => handleRepresentativeChange(row.id)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Select
|
||||
value={row.isActive}
|
||||
onValueChange={(value: "true" | "false") =>
|
||||
handleActiveChange(row.id, value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mx-auto w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">예</SelectItem>
|
||||
<SelectItem value="false">아니오</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" onClick={onSave}>
|
||||
저장
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<>
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>도메인 관리</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>도메인 관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
<Button type="button" size="sm" onClick={handleAddDomain}>
|
||||
항목 추가
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteDomain}
|
||||
disabled={selectedRows.length === 0}
|
||||
>
|
||||
항목 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border max-h-[28.5rem] overflow-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="auto" />
|
||||
<col width="15%" />
|
||||
<col width="25%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky -top-[0.6rem]">
|
||||
<TableRow>
|
||||
<TableHead className="!px-0">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>도메인 주소</TableHead>
|
||||
<TableHead>대표 URL</TableHead>
|
||||
<TableHead>사용여부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{fields.map((field, i) => {
|
||||
if (field.mode === "D") return null;
|
||||
return (
|
||||
<TableRow key={field.id}>
|
||||
<TableCell align="center" className="!px-0">
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(i)}
|
||||
onCheckedChange={() => handleSelectRow(i)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`domains.${i}.dmnAddr`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-left" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`domains.${i}.rprsDmnYn`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked ? "Y" : "N");
|
||||
handleChangeRprsDmnYn(i);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`domains.${i}.useYn`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="Y"
|
||||
id={`useY-${i}`}
|
||||
/>
|
||||
<label htmlFor={`useY-${i}`}>예</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="N"
|
||||
id={`useN-${i}`}
|
||||
/>
|
||||
<label htmlFor={`useN-${i}`}>
|
||||
아니오
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button type="submit" size="lg">
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -15,162 +19,266 @@ import {
|
||||
Input,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Checkbox,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
|
||||
type IP = {
|
||||
id: number;
|
||||
ipAddress: string;
|
||||
allowed: boolean;
|
||||
};
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { IpApi } from "../api/ip-service";
|
||||
import { ipFormSchema, type IP, type IPFormData } from "../model/ip-interface";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
type IPManagementDialogProps = {
|
||||
siteId: string;
|
||||
triggerDisabled?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) {
|
||||
const [rows, setRows] = useState<IP[]>([]);
|
||||
const [rowSelection, setRowSelection] = useState<IP["id"][]>([]);
|
||||
function IPManagementDialog({
|
||||
siteId,
|
||||
triggerDisabled,
|
||||
...props
|
||||
}: IPManagementDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const onSave = () => {};
|
||||
const { data: ipList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IPS, props.open],
|
||||
queryFn: () => IpApi.getIpList({ siteId, sort: [] }),
|
||||
select: (data) => {
|
||||
const ips = data.content.map((item) => ({
|
||||
...item,
|
||||
mode: "U",
|
||||
}));
|
||||
|
||||
initialForm();
|
||||
return ips;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync } = useMutation({
|
||||
mutationFn: (body: IP[]) => IpApi.saveIpList(body),
|
||||
});
|
||||
|
||||
const form = useForm<IPFormData>({
|
||||
resolver: zodResolver(ipFormSchema),
|
||||
values: {
|
||||
ips: ipList ?? [],
|
||||
},
|
||||
});
|
||||
const { fields, append, remove, update } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "ips",
|
||||
});
|
||||
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const initialForm = () => {
|
||||
form.reset();
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: IPFormData) => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
await mutateAsync(data.ips);
|
||||
alert("저장되었습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddIP = () => {
|
||||
const newIP: IP = {
|
||||
id: Date.now(),
|
||||
ipAddress: "",
|
||||
allowed: true,
|
||||
};
|
||||
setRows([...rows, newIP]);
|
||||
};
|
||||
|
||||
const handleDeleteIP = () => {
|
||||
const filteredRows = rows.filter((row) => !rowSelection.includes(row.id));
|
||||
setRows(filteredRows);
|
||||
setRowSelection([]);
|
||||
};
|
||||
|
||||
const handleIPAddressChange = (id: number, value: string) => {
|
||||
setRows(
|
||||
rows.map((row) => (row.id === id ? { ...row, ipAddress: value } : row)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAllowedChange = (id: number, value: boolean) => {
|
||||
setRows(
|
||||
rows.map((row) => (row.id === id ? { ...row, allowed: value } : row)),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleRowSelection = (id: number) => {
|
||||
setRowSelection((prev) => {
|
||||
if (prev.includes(id)) {
|
||||
return prev.filter((item) => item !== id);
|
||||
} else {
|
||||
return [...prev, id];
|
||||
}
|
||||
append({
|
||||
mngNo: null,
|
||||
siteId,
|
||||
ipAddr: "",
|
||||
prmYn: "Y",
|
||||
mode: "C",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedRows.length === fields.length) {
|
||||
setSelectedRows([]);
|
||||
} else {
|
||||
setSelectedRows(fields.map((_, index) => index));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRow = (i: number) => {
|
||||
setSelectedRows((prev) =>
|
||||
prev.includes(i) ? prev.filter((j) => j !== i) : [...prev, i],
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteIP = async () => {
|
||||
if (selectedRows.length === 0) return;
|
||||
|
||||
const sortedIndices = [...selectedRows].sort((a, b) => b - a);
|
||||
sortedIndices.forEach((i) => {
|
||||
const field = fields[i];
|
||||
if (field?.mngNo) {
|
||||
update(i, {
|
||||
...field,
|
||||
mode: "D",
|
||||
});
|
||||
} else {
|
||||
remove(i);
|
||||
}
|
||||
});
|
||||
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const isAllSelected =
|
||||
fields.length > 0 && selectedRows.length === fields.length;
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button disabled={triggerDisabled}>IP 관리</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[60rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>IP 관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
<Button size="sm" color="green">
|
||||
엑셀 업로드
|
||||
</Button>
|
||||
<Button size="sm" color="white" onClick={handleAddIP}>
|
||||
항목 추가
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteIP}
|
||||
disabled={rowSelection.length === 0}
|
||||
>
|
||||
항목 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
<col width="25%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>NO</TableHead>
|
||||
<TableHead>선택</TableHead>
|
||||
<TableHead>IP 주소</TableHead>
|
||||
<TableHead>허용여부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row, idx) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell align="center">{idx + 1}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Checkbox
|
||||
checked={rowSelection.includes(row.id)}
|
||||
onCheckedChange={() => toggleRowSelection(row.id)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Input
|
||||
value={row.ipAddress}
|
||||
onChange={(e) =>
|
||||
handleIPAddressChange(row.id, e.target.value)
|
||||
}
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<RadioGroup
|
||||
value={row.allowed ? "true" : "false"}
|
||||
onValueChange={(value) =>
|
||||
handleAllowedChange(row.id, value === "true")
|
||||
}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="true" id={`allow-${row.id}`} />
|
||||
<label htmlFor={`allow-${row.id}`}>예</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="false" id={`deny-${row.id}`} />
|
||||
<label htmlFor={`deny-${row.id}`}>아니오</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" onClick={onSave}>
|
||||
저장
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<>
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button disabled={triggerDisabled}>IP 관리</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>IP 관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
<Button type="button" size="sm" onClick={handleAddIP}>
|
||||
항목 추가
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteIP}
|
||||
disabled={selectedRows.length === 0}
|
||||
>
|
||||
항목 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border max-h-[28.5rem] overflow-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="65%" />
|
||||
<col width="25%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky -top-[0.6rem]">
|
||||
<TableRow>
|
||||
<TableHead className="!px-0">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>IP 주소</TableHead>
|
||||
<TableHead>허용여부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{fields.map((field, i) => {
|
||||
if (field.mode === "D") return null;
|
||||
return (
|
||||
<TableRow key={field.id}>
|
||||
<TableCell align="center" className="!px-0">
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(i)}
|
||||
onCheckedChange={() => handleSelectRow(i)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ips.${i}.ipAddr`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-left" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ips.${i}.prmYn`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="Y"
|
||||
id={`prmY-${i}`}
|
||||
/>
|
||||
<label htmlFor={`prmY-${i}`}>예</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="N"
|
||||
id={`prmN-${i}`}
|
||||
/>
|
||||
<label htmlFor={`prmN-${i}`}>
|
||||
아니오
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button type="submit" size="lg">
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation, useQuery } 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 { SiteApi } from "../api/site-service";
|
||||
import {
|
||||
siteFormSchema,
|
||||
type ISiteDetail,
|
||||
type SiteFormData,
|
||||
} from "../model/site-interface";
|
||||
import { SitePageLayout } from "./layout";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { SiteForm } from "./site-form";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
} from "@common/components/ui";
|
||||
|
||||
function ModifyPage({ siteId }: { siteId: string }) {
|
||||
const isModifyMode = !!siteId;
|
||||
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: siteDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
|
||||
queryFn: () => SiteApi.siteDetail(siteId),
|
||||
select: (data) => ({
|
||||
...data,
|
||||
faxNo: data.faxNumber,
|
||||
bscSiteYn: data.basicSiteYn,
|
||||
}),
|
||||
select: (data) => {
|
||||
const keys = Object.keys(siteFormSchema.shape);
|
||||
const filteredData = Object.fromEntries(
|
||||
Object.entries(data).filter(([key]) => keys.includes(key)),
|
||||
);
|
||||
return {
|
||||
...filteredData,
|
||||
faxNo: data.faxNumber,
|
||||
bscSiteYn: data.basicSiteYn,
|
||||
};
|
||||
},
|
||||
enabled: isModifyMode,
|
||||
});
|
||||
|
||||
const { mutateAsync: modifySite } = useMutation({
|
||||
mutationFn: (data: SiteFormData) => SiteApi.modifySite(siteId, data),
|
||||
onSuccess: () => {
|
||||
setMessage("수정되었습니다.");
|
||||
alert("수정되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteSite } = useMutation({
|
||||
mutationFn: () => SiteApi.deleteSite(siteId),
|
||||
onSuccess: () => {
|
||||
setMessage("삭제되었습니다.");
|
||||
alert("삭제되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
});
|
||||
router.back();
|
||||
});
|
||||
},
|
||||
});
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const form = useForm<SiteFormData>({
|
||||
resolver: zodResolver(siteFormSchema),
|
||||
values: siteDetail as SiteFormData,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!siteDetail) return;
|
||||
|
||||
const keys = Object.keys(siteFormSchema.shape);
|
||||
for (const key of keys) {
|
||||
form.setValue(
|
||||
key as keyof SiteFormData,
|
||||
siteDetail[key as keyof ISiteDetail] ?? "",
|
||||
);
|
||||
}
|
||||
}, [siteDetail]);
|
||||
|
||||
return (
|
||||
<SitePageLayout>
|
||||
<PageLayout pageTitle="사이트 관리">
|
||||
<SiteForm
|
||||
form={form}
|
||||
siteId={siteId}
|
||||
handleSave={modifySite}
|
||||
handleDelete={deleteSite}
|
||||
/>
|
||||
<Dialog open={!!message} onOpenChange={() => router.back()}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="text-center text-[1.4rem]">{message}</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button>확인</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</SitePageLayout>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,6 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
@@ -19,6 +15,7 @@ import {
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { SiteFormData } from "@/entities/site";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
@@ -26,47 +23,54 @@ import {
|
||||
Form,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { type ISiteDetail, type SiteFormData } from "../model/site-interface";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import type { Domain } from "../model/domain-interface";
|
||||
import { IPManagementDialog } from "./ip-management-dialog";
|
||||
import {
|
||||
DomainManagementDialog,
|
||||
type Domain,
|
||||
} from "./domain-management-dialog";
|
||||
import { DomainManagementDialog } from "./domain-management-dialog";
|
||||
|
||||
type SiteFormProps = {
|
||||
form: UseFormReturn<SiteFormData>;
|
||||
siteId?: string;
|
||||
handleSave: (data: SiteFormData) => void;
|
||||
handleDelete?: () => void;
|
||||
handleSave: (data: SiteFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
const router = useRouter();
|
||||
const { message, confirm, onConfirm, onCancel } = useConfirm();
|
||||
|
||||
const [viewState, setViewState] = useState<Partial<ISiteDetail>>({});
|
||||
const [representativeDomain, setRepresentativeDomain] = useState<string>("");
|
||||
|
||||
const isModifyMode = !!siteId;
|
||||
|
||||
const handleViewStateChange = (key: keyof ISiteDetail) => (value: string) => {
|
||||
setViewState({ ...viewState, [key]: value });
|
||||
};
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [isIpManagementOpen, setIsIpManagementOpen] = useState(false);
|
||||
const [isDomainManagementOpen, setIsDomainManagementOpen] = useState(false);
|
||||
|
||||
const handleDomainSave = (rows: Domain[]) => {
|
||||
setRepresentativeDomain(
|
||||
rows.find((row) => row.isRepresentative)?.domainUrl || "",
|
||||
const handleSaveDomain = (rows: Domain[]) => {
|
||||
form.setValue(
|
||||
"dmnAddr",
|
||||
rows.find((row) => row.rprsDmnYn === "Y")?.dmnAddr || "",
|
||||
);
|
||||
};
|
||||
|
||||
const onSubmit = (data: SiteFormData) => {
|
||||
handleSave(data);
|
||||
const onSubmit = async (data: SiteFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
setOnConfirm(true);
|
||||
if (await confirm(confirmMessage)) {
|
||||
setOnConfirm(false);
|
||||
handleSave(data);
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm("정말 삭제하시겠습니까?")) {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleDelete?.();
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -97,6 +101,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled={isModifyMode}
|
||||
/>
|
||||
@@ -120,10 +125,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="SITEKNDCD"
|
||||
onValueChange={(value) => {
|
||||
handleViewStateChange("siteKndCd")(value);
|
||||
field.onChange(value);
|
||||
}}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -132,7 +134,10 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
/>
|
||||
{isModifyMode && (
|
||||
<IPManagementDialog
|
||||
triggerDisabled={viewState.siteKndCd === "O"}
|
||||
siteId={siteId}
|
||||
triggerDisabled={form.watch("siteKndCd") === "O"}
|
||||
open={isIpManagementOpen}
|
||||
onOpenChange={setIsIpManagementOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -149,6 +154,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="SITESKINCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
@@ -170,6 +176,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트명을 입력하십시오."
|
||||
/>
|
||||
@@ -191,6 +198,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 설명을 입력하십시오."
|
||||
/>
|
||||
@@ -207,10 +215,13 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<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">
|
||||
{representativeDomain}
|
||||
{form.watch("dmnAddr")}
|
||||
</div>
|
||||
<DomainManagementDialog
|
||||
handleSave={handleDomainSave}
|
||||
siteId={siteId}
|
||||
open={isDomainManagementOpen}
|
||||
onOpenChange={setIsDomainManagementOpen}
|
||||
handleSaveDomain={handleSaveDomain}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -227,6 +238,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 주소를 입력하십시오."
|
||||
/>
|
||||
@@ -248,6 +260,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 전화번호를 입력하십시오."
|
||||
/>
|
||||
@@ -269,6 +282,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 팩스번호를 입력하십시오."
|
||||
/>
|
||||
@@ -290,6 +304,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
|
||||
/>
|
||||
@@ -311,6 +326,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
|
||||
/>
|
||||
@@ -405,25 +421,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<Dialog open={!!message} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="text-center text-[1.4rem]">{message}</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button color="red" onClick={onConfirm}>
|
||||
삭제
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button color="white" onClick={onCancel}>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,32 +6,26 @@ import { ENDPOINT } from "@/shared/config/api";
|
||||
import { Button, DataTable, Input } from "@common/components";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SiteApi } from "../api/site-service";
|
||||
import { type ISite, type ISiteParams } from "../model/site-interface";
|
||||
import { Button, DataTable, Input } from "@common/components";
|
||||
import { SiteApi, type ISite, type ISiteParams } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { SitePageLayout } from "./layout";
|
||||
|
||||
function SitePage() {
|
||||
const router = useRouter();
|
||||
const { data: siteInfo, refetch } = useQuery({
|
||||
const { data: siteList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.siteInfo(siteQuery),
|
||||
queryFn: () => SiteApi.getSiteList(siteQuery),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const [siteQuery, setSiteQuery] = useState<ISiteParams>({
|
||||
siteNm: "",
|
||||
siteKndCd: "",
|
||||
useYn: "",
|
||||
page: 0,
|
||||
size: 0,
|
||||
sort: [],
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: siteInfo?.content ?? [],
|
||||
data: siteList?.content ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
@@ -47,23 +41,20 @@ function SitePage() {
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SitePageLayout>
|
||||
<PageLayout pageTitle="사이트 관리">
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border !border-[#dbe2e6] !bg-[#eef7ff]">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<label htmlFor="" className="mr-2 text-[#657481]">
|
||||
<label htmlFor="" className="mr-2 text-label">
|
||||
사이트명
|
||||
</label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={siteQuery.siteNm}
|
||||
onChange={(e) =>
|
||||
setSiteQuery({ ...siteQuery, siteNm: e.target.value })
|
||||
}
|
||||
onChange={(e) => setSiteQuery({ siteNm: e.target.value })}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
@@ -72,7 +63,7 @@ function SitePage() {
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-[#666]">사이트 목록</h3>
|
||||
<h3 className="text-2xl font-medium text-label">사이트 목록</h3>
|
||||
<Button onClick={() => router.push("/site/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
@@ -80,7 +71,7 @@ function SitePage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SitePageLayout>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ export const ENDPOINT = {
|
||||
CMS_SERVICE: {
|
||||
SITES: "/cms-service/api/v1/sites",
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { AlertStore } from "../store/alert-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useAlertStore = <T>(selector: (store: AlertStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useAlertStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.alertStore, selector);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import { type UserStore } from "@/shared/store/user-store";
|
||||
import type { UserStore } from "../store/user-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useUserStore = <T>(selector: (store: UserStore) => T): T => {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
interface SortConfig {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
}
|
||||
|
||||
interface PageableConfig {
|
||||
sort: SortConfig;
|
||||
offset: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
}
|
||||
|
||||
export interface PageableList<T> {
|
||||
content: T;
|
||||
pageable: PageableConfig;
|
||||
sort: SortConfig;
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
number: number;
|
||||
numberOfElements: number;
|
||||
size: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
export interface IPageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./api-interface";
|
||||
@@ -1,2 +1 @@
|
||||
export { QueryProvider } from "./query-provider";
|
||||
export { StoreProvider } from "./store-provider";
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useRef } from "react";
|
||||
import { createUserStore } from "@/shared/store/user-store";
|
||||
import { createAlertStore } from "../store/alert-store";
|
||||
import { createUserStore } from "../store/user-store";
|
||||
|
||||
type StoreContextType = {
|
||||
alertStore: ReturnType<typeof createAlertStore>;
|
||||
userStore: ReturnType<typeof createUserStore>;
|
||||
};
|
||||
|
||||
@@ -15,6 +17,7 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const storeRef = useRef<StoreContextType | null>(null);
|
||||
if (storeRef.current === null) {
|
||||
storeRef.current = {
|
||||
alertStore: createAlertStore(),
|
||||
userStore: createUserStore(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export type AlertState = {
|
||||
message: string;
|
||||
callback?: () => void;
|
||||
};
|
||||
|
||||
export type AlertActions = {
|
||||
setMessage: (message: string, callback?: () => void) => void;
|
||||
};
|
||||
|
||||
export type AlertStore = AlertState & AlertActions;
|
||||
|
||||
export const defaultInitState: AlertState = {
|
||||
message: "",
|
||||
callback: undefined,
|
||||
};
|
||||
|
||||
export const createAlertStore = (initState: AlertState = defaultInitState) => {
|
||||
return createStore<AlertStore>()((set) => ({
|
||||
...initState,
|
||||
setMessage: (message: string, callback?: () => void) =>
|
||||
set(() => ({ message, callback })),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
} from "@common/components/ui";
|
||||
import { useAlertStore } from "../lib/use-alert-store";
|
||||
|
||||
function AlertDialog() {
|
||||
const { message, callback, setMessage } = useAlertStore((state) => state);
|
||||
const handleOpenChange = () => {
|
||||
setMessage("");
|
||||
callback?.();
|
||||
};
|
||||
return (
|
||||
<Dialog open={!!message} onOpenChange={handleOpenChange}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="text-center text-[1.4rem] text-foreground">{message}</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button>확인</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AlertDialog };
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
type ButtonProps,
|
||||
} from "@common/components/ui";
|
||||
|
||||
type ConfirmDialogProps = {
|
||||
open: boolean;
|
||||
confirmButtonProps?: ButtonProps & { label?: string };
|
||||
cancelButtonProps?: ButtonProps & { label?: string };
|
||||
};
|
||||
|
||||
function ConfirmDialog({
|
||||
open,
|
||||
confirmButtonProps,
|
||||
cancelButtonProps,
|
||||
}: ConfirmDialogProps) {
|
||||
const { message, onConfirm, onCancel } = useConfirm();
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="text-center text-[1.4rem] text-foreground">{message}</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onConfirm} {...confirmButtonProps}>
|
||||
{confirmButtonProps?.label ?? "확인"}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onCancel} color="white" {...cancelButtonProps}>
|
||||
{cancelButtonProps?.label ?? "취소"}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ConfirmDialog };
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user