Merge pull request 'iipbs_cs' (#15) from iipbs_cs into main
Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
NEXT_PUBLIC_PORT=3000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
|
||||
@@ -36,3 +36,5 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.env*
|
||||
@@ -0,0 +1,2 @@
|
||||
import { BannerPage } from "@/pages/banner";
|
||||
export default BannerPage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/banner";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { CreatePage } from "@/pages/banner";
|
||||
export default CreatePage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/image";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/image";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ serial: string }>;
|
||||
}) {
|
||||
const { serial } = await params;
|
||||
return <VModifyPage serial={serial} />;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { SkeletonPage } from "@/shared/ui/skeleton-page";
|
||||
export default SkeletonPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { TemplatePage } from "@/pages/template";
|
||||
export default TemplatePage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/template";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { CreatePage } from "@/pages/template";
|
||||
export default CreatePage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { TermsPage } from "@/pages/terms";
|
||||
export default TermsPage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/terms";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { CreatePage } from "@/pages/terms";
|
||||
export default CreatePage;
|
||||
+11
-1
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
|
||||
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
@@ -16,8 +17,17 @@ export function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/attach-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
const newUrl = `${serverApiUrl}${pathname}`;
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tinymce/tinymce-react": "^6.1.0",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"dayjs": "^1.11.13",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
|
||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
@@ -16,6 +17,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>{children}</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</StoreProvider>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
Attachment,
|
||||
AttachmentService,
|
||||
} from "../model/attachment-interface";
|
||||
|
||||
export class AttachmentApiService implements AttachmentService {
|
||||
public async getAttachments(atflCd: string): Promise<Attachment[]> {
|
||||
const response = await client.get<Attachment[]>(
|
||||
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteAttachment(atflId: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`);
|
||||
}
|
||||
|
||||
public async deleteAttachmentByAtflCd(atflCd: string): Promise<void> {
|
||||
await client.delete(
|
||||
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/atflCd/${atflCd}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async getImageUrl(atflId: string): Promise<string> {
|
||||
if (!atflId) return "";
|
||||
const response = await fetch(`${ENDPOINT.ATTACH_SERVICE.IMAGES}/${atflId}`);
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async getImageUrlByAtflCd(atflCd: string): Promise<string> {
|
||||
if (!atflCd) return "";
|
||||
const response = await fetch(
|
||||
`${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`,
|
||||
);
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
}
|
||||
|
||||
export const AttachmentApi = new AttachmentApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { AttachmentApi } from "./api/attachment-service";
|
||||
@@ -0,0 +1,22 @@
|
||||
export interface Attachment {
|
||||
atflCd: string;
|
||||
atflSn: number;
|
||||
atflId: string;
|
||||
physFileNm: string;
|
||||
orgnlFileNm: string;
|
||||
atflFlieSz: number;
|
||||
atflType: string;
|
||||
dwnldNocs: number;
|
||||
delYn: string;
|
||||
lnkgDmnId: string;
|
||||
lnkgDmnNm: string;
|
||||
regDt: string;
|
||||
}
|
||||
|
||||
export interface AttachmentService {
|
||||
getAttachments: (atflCd: string) => Promise<Attachment[]>;
|
||||
deleteAttachment: (atflId: string) => Promise<void>;
|
||||
deleteAttachmentByAtflCd: (atflCd: string) => Promise<void>;
|
||||
getImageUrl: (atflId: string) => Promise<string>;
|
||||
getImageUrlByAtflCd: (atflCd: string) => Promise<string>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CodeSelect } from "../ui/code-select";
|
||||
@@ -0,0 +1,19 @@
|
||||
import { client } from "@/shared/api";
|
||||
import type {
|
||||
EditorService,
|
||||
FileInfo,
|
||||
UploadFile,
|
||||
} from "../model/editor-interface";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export class EditorApiService implements EditorService {
|
||||
async uploadImage(data: UploadFile): Promise<FileInfo> {
|
||||
const response = await client.post<FileInfo>(
|
||||
ENDPOINT.ATTACH_SERVICE.EDITOR_UPLOAD,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const EditorApi = new EditorApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { Editor } from "./ui/editor";
|
||||
@@ -0,0 +1,10 @@
|
||||
export const blobToBase64 = (blob: Blob): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result as string;
|
||||
resolve(base64.split(",")[1] || "");
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { blobToBase64 } from "./blob-to-base64";
|
||||
|
||||
export const generateFileInfo = async (blobInfo: {
|
||||
blob: () => Blob;
|
||||
filename: () => string;
|
||||
}) => {
|
||||
const blob = blobInfo.blob();
|
||||
const base64 = await blobToBase64(blob);
|
||||
return {
|
||||
fieldName: `${blob.type.split("/")[1]}`,
|
||||
fileType: blob.type,
|
||||
fileBase64: base64,
|
||||
originalName: blobInfo.filename(),
|
||||
size: blob.size,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { SERVER_API_URL } from "@/shared/config";
|
||||
|
||||
export const editorBaseConfig = {
|
||||
apiKey: process.env.NEXT_PUBLIC_TINY_API_KEY,
|
||||
init: {
|
||||
language: "ko_KR",
|
||||
height: 500,
|
||||
plugins: [
|
||||
"advlist",
|
||||
"autolink",
|
||||
"lists",
|
||||
"link",
|
||||
"image",
|
||||
"charmap",
|
||||
"preview",
|
||||
"anchor",
|
||||
"searchreplace",
|
||||
"visualblocks",
|
||||
"code",
|
||||
"codesample",
|
||||
"fullscreen",
|
||||
"insertdatetime",
|
||||
"media",
|
||||
"table",
|
||||
"code",
|
||||
"help",
|
||||
"wordcount",
|
||||
"save",
|
||||
],
|
||||
toolbar: [
|
||||
"undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table",
|
||||
"link image",
|
||||
],
|
||||
images_upload_base_path: SERVER_API_URL,
|
||||
file_picker_types: "image",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface UploadFile {
|
||||
fieldName: string;
|
||||
fileType: string;
|
||||
fileBase64: string;
|
||||
originalName: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
originalFileName: string;
|
||||
message: string;
|
||||
fileType: string;
|
||||
size: number;
|
||||
uploaded: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface EditorService {
|
||||
uploadImage(data: UploadFile): Promise<FileInfo>;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
Editor as TinyEditor,
|
||||
type Editor as TinyEditorInstance,
|
||||
type IAllProps,
|
||||
} from "@tinymce/tinymce-react";
|
||||
import { editorBaseConfig as baseConfig } from "../model/editor-base-config";
|
||||
import { generateFileInfo } from "../lib/generate-file-info";
|
||||
import { EditorApi } from "../api/editor-service";
|
||||
|
||||
const Editor = ({ ...props }: IAllProps) => {
|
||||
const editorRef = useRef<TinyEditorInstance | null>(null);
|
||||
|
||||
const imagesUploadHandler = (blobInfo: {
|
||||
blob: () => Blob;
|
||||
filename: () => string;
|
||||
}) => {
|
||||
return new Promise(async (resolve) => {
|
||||
const fileInfo = await generateFileInfo(blobInfo);
|
||||
const data = await EditorApi.uploadImage(fileInfo);
|
||||
resolve(data.url);
|
||||
});
|
||||
};
|
||||
|
||||
const filePickerCallback = (
|
||||
callback: (base64: string, meta: { title: string }) => void,
|
||||
) => {
|
||||
const input = document.createElement("input");
|
||||
input.setAttribute("type", "file");
|
||||
input.setAttribute("accept", "image/*");
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const base64 = reader.result as string;
|
||||
callback(base64, { title: file.name });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<TinyEditor
|
||||
{...baseConfig}
|
||||
onInit={(_: unknown, editor: TinyEditorInstance) => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
init={{
|
||||
...baseConfig.init,
|
||||
images_upload_handler: imagesUploadHandler,
|
||||
file_picker_callback: filePickerCallback,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Editor };
|
||||
@@ -0,0 +1,32 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
MenuAuthorityList,
|
||||
MenuAuthorityParams,
|
||||
MenuAuthorityPayloadData,
|
||||
MenuAuthorityService,
|
||||
} from "../model/menu-authority-interface";
|
||||
|
||||
export class MenuAuthorityApiService implements MenuAuthorityService {
|
||||
public async getMenuAuthorityList(
|
||||
params: MenuAuthorityParams,
|
||||
): Promise<MenuAuthorityList> {
|
||||
const response = await client.get<MenuAuthorityList>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveMenuAuthority(
|
||||
data: MenuAuthorityPayloadData[],
|
||||
): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuAuthorityApi = new MenuAuthorityApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
|
||||
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface MenuAuthorityParams extends PageableParams {
|
||||
groupId?: string;
|
||||
menuCd?: number;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export const menuAuthoritySchema = z.object({
|
||||
groupId: z.string(),
|
||||
groupNm: z.string(),
|
||||
menuCd: z.number(),
|
||||
mngAuthrtYn: z.string(),
|
||||
inqAuthrtYn: z.string(),
|
||||
regAuthrtYn: z.string(),
|
||||
mdfcnAuthrtYn: z.string(),
|
||||
delAuthrtYn: z.string(),
|
||||
useYn: z.string(),
|
||||
mode: z.string(),
|
||||
});
|
||||
|
||||
export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({
|
||||
groupNm: true,
|
||||
});
|
||||
|
||||
export const menuAuthorityFormSchema = z.object({
|
||||
menuAuthority: z.array(menuAuthorityPayloadSchema),
|
||||
});
|
||||
|
||||
export type MenuAuthority = z.infer<typeof menuAuthoritySchema>;
|
||||
export type MenuAuthorityList = PageableList<MenuAuthority>;
|
||||
export type MenuAuthorityPayloadData = z.infer<
|
||||
typeof menuAuthorityPayloadSchema
|
||||
>;
|
||||
export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
|
||||
|
||||
export interface MenuAuthorityService {
|
||||
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
|
||||
saveMenuAuthority(data: MenuAuthorityPayloadData[]): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type {
|
||||
MenuAuthority,
|
||||
MenuAuthorityFormData,
|
||||
MenuAuthorityPayloadData,
|
||||
} from "./menu-authority-interface";
|
||||
|
||||
export const columns = (
|
||||
form: UseFormReturn<MenuAuthorityFormData>,
|
||||
): ColumnDef<MenuAuthority>[] => {
|
||||
const getCurrentRow = (i: number) => form.watch("menuAuthority")[i];
|
||||
|
||||
const handleCheckedChange =
|
||||
(i: number, key: keyof MenuAuthorityPayloadData) => (checked: boolean) => {
|
||||
if (getCurrentRow(i)?.mode === "R") {
|
||||
form.setValue(`menuAuthority.${i}.mode`, "U");
|
||||
}
|
||||
form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N");
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: "groupNm",
|
||||
header: "그룹명",
|
||||
},
|
||||
{
|
||||
accessorKey: "mngAuthrtYn",
|
||||
header: "관리권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mngAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"regAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"mdfcnAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"delAuthrtYn",
|
||||
)(checked as boolean);
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "regAuthrtYn",
|
||||
header: "등록권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.regAuthrtYn === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleCheckedChange(
|
||||
row.index,
|
||||
"inqAuthrtYn",
|
||||
)(checked as boolean);
|
||||
}
|
||||
handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnAuthrtYn",
|
||||
header: "수정권한 ",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.mdfcnAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "mdfcnAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "delAuthrtYn",
|
||||
header: "삭제권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.delAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "delAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "inqAuthrtYn",
|
||||
header: "조회권한",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={getCurrentRow(row.index)?.inqAuthrtYn === "Y"}
|
||||
onCheckedChange={handleCheckedChange(row.index, "inqAuthrtYn")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@common/components/ui";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthorityFormData,
|
||||
type MenuAuthorityPayloadData,
|
||||
} from "../model/menu-authority-interface";
|
||||
import { columns } from "../model/menu-authority-table-columns";
|
||||
import { MenuAuthorityApi } from "../api/menu-authority-service";
|
||||
|
||||
type ManagementAuthorityDialogProps = {
|
||||
menuCd?: number;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function ManagementAuthorityDialog({
|
||||
menuCd,
|
||||
...props
|
||||
}: ManagementAuthorityDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: menuAuthorityList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd],
|
||||
queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }),
|
||||
enabled: !!menuCd,
|
||||
});
|
||||
|
||||
const { mutateAsync: saveMenuAuthority } = useMutation({
|
||||
mutationFn: (data: MenuAuthorityPayloadData[]) =>
|
||||
MenuAuthorityApi.saveMenuAuthority(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<MenuAuthorityFormData>({
|
||||
resolver: zodResolver(menuAuthorityFormSchema),
|
||||
values: {
|
||||
menuAuthority: menuAuthorityList?.content ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: menuAuthorityList?.content ?? [],
|
||||
columns: columns(form),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
const data = form.getValues("menuAuthority");
|
||||
const filteredData = data.filter((item) => item.mode !== "R");
|
||||
await saveMenuAuthority(filteredData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3" disabled={!menuCd}>
|
||||
<i className="diveicon di-personal-information" />
|
||||
권한관리
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>그룹별 메뉴 권한</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DataTable table={table} />
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave}>
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ManagementAuthorityDialog };
|
||||
@@ -5,10 +5,16 @@ export const getCurrentMenu = (
|
||||
pathname: string,
|
||||
): MenuItem | null => {
|
||||
for (const menu of menus) {
|
||||
if (
|
||||
menu.urlPath !== "" &&
|
||||
(menu.urlPath === pathname || pathname.startsWith(menu.urlPath))
|
||||
)
|
||||
return menu;
|
||||
|
||||
if (menu.children) {
|
||||
return getCurrentMenu(menu.children, pathname);
|
||||
const children = getCurrentMenu(menu.children, pathname);
|
||||
if (children) return children;
|
||||
}
|
||||
if (menu.urlPath === pathname) return menu;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
import type {
|
||||
PostService,
|
||||
PostingParams,
|
||||
Post,
|
||||
PostPayloadData,
|
||||
} from "../model/posting-interface";
|
||||
|
||||
export class PostingApiService implements PostService {
|
||||
public async getPostingHistory(
|
||||
params: PostingParams,
|
||||
): Promise<PageableList<Post>> {
|
||||
const response = await client.get<PageableList<Post>>(
|
||||
`${ENDPOINT.CMS_SERVICE.POSTINGS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async savePosting(data: PostPayloadData[]): Promise<Post[]> {
|
||||
const response = await client.post<Post[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.POSTINGS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const PostingApi = new PostingApiService();
|
||||
@@ -0,0 +1,7 @@
|
||||
export { PostingApi } from "./api/posting-service";
|
||||
export {
|
||||
POSTING_TYPE,
|
||||
type Post,
|
||||
type PostPayloadData,
|
||||
} from "./model/posting-interface";
|
||||
export { PostingDialog } from "./ui/posting-dialog";
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
import { z } from "zod";
|
||||
|
||||
export const POSTING_TYPE = {
|
||||
POPUP: "POPUP",
|
||||
BANNER: "BANNER",
|
||||
IMAGE: "IMAGE",
|
||||
} as const;
|
||||
|
||||
export type PostingType = (typeof POSTING_TYPE)[keyof typeof POSTING_TYPE];
|
||||
|
||||
export interface PostingParams extends PageableParams {
|
||||
pstgTrgtNo: number;
|
||||
}
|
||||
|
||||
export const postSchema = z.object({
|
||||
siteId: z.string(),
|
||||
siteNm: z.string(),
|
||||
pstgTypeCd: z.string(),
|
||||
pstgTypeNm: z.string(),
|
||||
pstgTrgtNo: z.number(),
|
||||
pstgBgngDt: z.string().optional(),
|
||||
pstgEndDt: z.string().optional(),
|
||||
pstgPrdSeCd: z.string(),
|
||||
pstgPrdSeNm: z.string(),
|
||||
delYn: z.string(),
|
||||
});
|
||||
|
||||
export const postPayloadSchema = postSchema
|
||||
.extend({
|
||||
mode: z.string(),
|
||||
})
|
||||
.omit({
|
||||
siteNm: true,
|
||||
pstgTypeNm: true,
|
||||
pstgPrdSeNm: true,
|
||||
});
|
||||
|
||||
export const postingFormSchema = z.object({
|
||||
pstg: z.array(postPayloadSchema),
|
||||
});
|
||||
|
||||
export type Post = z.infer<typeof postSchema>;
|
||||
export type PostPayloadData = z.infer<typeof postPayloadSchema>;
|
||||
export type PostingFormData = z.infer<typeof postingFormSchema>;
|
||||
|
||||
export interface PostService {
|
||||
getPostingHistory: (params: PostingParams) => Promise<PageableList<Post>>;
|
||||
savePosting: (data: PostPayloadData[]) => Promise<Post[]>;
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Button,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Badge,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Checkbox,
|
||||
Select,
|
||||
DateRangePicker,
|
||||
DialogClose,
|
||||
DialogFooter,
|
||||
} from "@common/components/ui";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { CodeSelect } from "@/entities/code/@x/posting";
|
||||
import { SiteApi, type Site } from "@/entities/site/@x/posting";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PostingApi } from "../api/posting-service";
|
||||
import {
|
||||
postingFormSchema,
|
||||
type PostingFormData,
|
||||
type PostingType,
|
||||
type PostPayloadData,
|
||||
} from "../model/posting-interface";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
type DateRange = {
|
||||
from: string;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
type PostingDialogProps = {
|
||||
postingType: PostingType;
|
||||
postingTargets: PostingTarget[];
|
||||
};
|
||||
|
||||
const POSTING_TYPE_LABEL = {
|
||||
POPUP: "팝업",
|
||||
BANNER: "배너",
|
||||
IMAGE: "이미지",
|
||||
} as const;
|
||||
|
||||
function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
const [rememberPeriodCode, setRememberPeriodCode] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [rememberPeriodDate, setRememberPeriodDate] = useState<
|
||||
Record<string, DateRange | undefined>
|
||||
>({});
|
||||
|
||||
const { data: siteList } = useQuery<PageableList<Site>>({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.getSiteList({}),
|
||||
});
|
||||
|
||||
const { mutateAsync: savePosting } = useMutation({
|
||||
mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostingFormData>({
|
||||
resolver: zodResolver(postingFormSchema),
|
||||
});
|
||||
const { fields, append, replace } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "pstg",
|
||||
});
|
||||
|
||||
const isFormValid =
|
||||
fields.length && fields.every((field) => field.pstgPrdSeCd !== "");
|
||||
|
||||
const isAllSelected =
|
||||
siteList && selectedRows.length === siteList.content.length;
|
||||
|
||||
const postingPeriod = (siteId: string) => {
|
||||
const periodCode = Number(rememberPeriodCode[siteId]);
|
||||
|
||||
if (periodCode === 99) return "상시";
|
||||
|
||||
return `${dayjs().format("YYYY-MM-DD")} ~ ${dayjs()
|
||||
.add(periodCode, "day")
|
||||
.format("YYYY-MM-DD")}`;
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open) return true;
|
||||
replace([]);
|
||||
setSelectedRows([]);
|
||||
setRememberPeriodCode({});
|
||||
setRememberPeriodDate({});
|
||||
};
|
||||
|
||||
const handleAppendFields = (i: number) => {
|
||||
const siteId = siteList?.content[i]?.siteId ?? "";
|
||||
const newFields = postingTargets.map((target) => ({
|
||||
siteId,
|
||||
pstgTypeCd: postingType,
|
||||
pstgTrgtNo: target.pstgTrgtNo,
|
||||
pstgBgngDt: rememberPeriodDate[siteId]?.from ?? "",
|
||||
pstgEndDt: rememberPeriodDate[siteId]?.to ?? "",
|
||||
pstgPrdSeCd: rememberPeriodCode[siteId] ?? "",
|
||||
delYn: "N",
|
||||
mode: "C",
|
||||
}));
|
||||
append(newFields);
|
||||
};
|
||||
|
||||
const handleUpdateFields =
|
||||
(siteId: string) => (obj: Record<string, unknown>) => {
|
||||
const updatedFields = fields.map((field) =>
|
||||
"siteId" in field && field.siteId === siteId
|
||||
? { ...field, ...obj }
|
||||
: field,
|
||||
);
|
||||
replace(updatedFields);
|
||||
};
|
||||
|
||||
const handleRemoveFields = (siteId: string) => {
|
||||
const filteredFields = fields.filter(
|
||||
(field) => "siteId" in field && field.siteId !== siteId,
|
||||
);
|
||||
replace(filteredFields);
|
||||
};
|
||||
|
||||
const handleSelectRow = (i: number) => {
|
||||
const isSelected = selectedRows.includes(i);
|
||||
setSelectedRows((prev) =>
|
||||
isSelected ? prev.filter((j) => j !== i) : [...prev, i],
|
||||
);
|
||||
|
||||
if (isSelected) {
|
||||
const siteId = siteList?.content[i]?.siteId;
|
||||
if (!siteId) return;
|
||||
handleRemoveFields(siteId);
|
||||
return;
|
||||
}
|
||||
|
||||
handleAppendFields(i);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedRows.length === (siteList?.content.length ?? 0)) {
|
||||
replace([]);
|
||||
setSelectedRows([]);
|
||||
} else {
|
||||
replace([]);
|
||||
setSelectedRows(siteList?.content.map((_, i) => i) ?? []);
|
||||
siteList?.content.forEach((_, i) => handleAppendFields(i));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePeriodCodeChange = (siteId: string) => (value: string) => {
|
||||
const from = dayjs().format("YYYY-MM-DD 00:00:00");
|
||||
const to = dayjs().add(Number(value), "day").format("YYYY-MM-DD 23:59:59");
|
||||
|
||||
setRememberPeriodCode((prev) => ({
|
||||
...prev,
|
||||
[siteId]: value,
|
||||
}));
|
||||
setRememberPeriodDate((prev) => ({
|
||||
...prev,
|
||||
[siteId]: { from, to },
|
||||
}));
|
||||
handleUpdateFields(siteId)({
|
||||
pstgPrdSeCd: value,
|
||||
pstgBgngDt: from,
|
||||
pstgEndDt: to,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePeriodDateChange =
|
||||
(siteId: string) => (value: DateRange | undefined) => {
|
||||
const from = dayjs(value?.from).format("YYYY-MM-DD 00:00:00");
|
||||
const to = dayjs(value?.to).format("YYYY-MM-DD 23:59:59");
|
||||
setRememberPeriodDate((prev) => ({
|
||||
...prev,
|
||||
[siteId]: { from, to },
|
||||
}));
|
||||
handleUpdateFields(siteId)({
|
||||
pstgBgngDt: from,
|
||||
pstgEndDt: to,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
const data = form.getValues("pstg");
|
||||
await savePosting(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="emerald" disabled={!postingTargets.length}>
|
||||
<i className="diveicon di-layer" />
|
||||
사이트반영
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[68rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>사이트 반영</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="card bg-accent !p-4">
|
||||
<h3 className="font-semibold">
|
||||
선택된 {POSTING_TYPE_LABEL[postingType]}
|
||||
</h3>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{postingTargets.map((target) => (
|
||||
<Badge key={target.pstgTrgtNo}>
|
||||
<span className="text-xl">{target.pstgTrgtNm}</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="m-2 max-h-[30rem] overflow-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="25%" />
|
||||
<col width="65%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky top-0 z-10 bg-accent">
|
||||
<TableRow>
|
||||
<TableHead className="!px-0">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>사이트명</TableHead>
|
||||
<TableHead>게시 구분</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{siteList?.content.map((site, i) => (
|
||||
<TableRow key={site.siteId}>
|
||||
<TableCell align="center" className="!px-0">
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(i)}
|
||||
onCheckedChange={() => handleSelectRow(i)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">{site.siteNm}</TableCell>
|
||||
<TableCell align="left">
|
||||
<div className="flex items-center gap-4">
|
||||
<PostingPeriodSelect
|
||||
onValueChange={handlePeriodCodeChange(site.siteId)}
|
||||
disabled={!selectedRows.includes(i)}
|
||||
/>
|
||||
{!!Number(rememberPeriodCode[site.siteId]) && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-lg text-label",
|
||||
!selectedRows.includes(i) && "text-muted",
|
||||
)}
|
||||
>
|
||||
게시 기간: <u>{postingPeriod(site.siteId)}</u>
|
||||
</span>
|
||||
)}
|
||||
{Number(rememberPeriodCode[site.siteId]) === 0 && (
|
||||
<DateRangePicker
|
||||
handleDateChange={(date) =>
|
||||
handlePeriodDateChange(site.siteId)(
|
||||
date as DateRange | undefined,
|
||||
)
|
||||
}
|
||||
disabled={!selectedRows.includes(i)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave} disabled={!isFormValid}>
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function PostingPeriodSelect({
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof Select>) {
|
||||
return <CodeSelect upCd="PSTGPRDCD" {...props} />;
|
||||
}
|
||||
|
||||
export { PostingDialog };
|
||||
@@ -0,0 +1,2 @@
|
||||
export { SiteApi } from "../api/site-service";
|
||||
export type { Site } from "../model/site-interface";
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "../model/site-interface";
|
||||
|
||||
export class SiteApiService implements SiteService {
|
||||
public async getSiteList(params: SiteParams): Promise<SiteList> {
|
||||
public async getSiteList(params?: SiteParams): Promise<SiteList> {
|
||||
const response = await client.get<SiteList>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}`,
|
||||
{ params },
|
||||
@@ -44,10 +44,7 @@ export class SiteApiService implements SiteService {
|
||||
}
|
||||
|
||||
public async deleteSite(siteId: string): Promise<void> {
|
||||
const response = await client.delete<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
|
||||
);
|
||||
return response.data;
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,17 +35,17 @@ export interface SiteDetail extends SiteFormData {
|
||||
export const siteFormSchema = z.object({
|
||||
siteId: z.string().max(2).nonempty(),
|
||||
siteNm: z.string().max(100).nonempty(),
|
||||
siteExpln: z.string().max(1000).nullable(),
|
||||
siteExpln: z.string().max(1000).nullish(),
|
||||
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(),
|
||||
siteAddr: z.string().max(2000).nullish(),
|
||||
faxNo: z.string().max(100).nullish(),
|
||||
telNo: z.string().max(1000).nullish(),
|
||||
lwndCn: z.string().max(2000).nullish(),
|
||||
bscSiteYn: z.string(),
|
||||
bkmkIcon: z.string().max(100).nullable(),
|
||||
bkmkIcon: z.string().max(100).nullish(),
|
||||
useYn: z.string(),
|
||||
siteSkn: z.string().max(10).nullable(),
|
||||
dmnAddr: z.string().nullable(),
|
||||
siteSkn: z.string().max(10).nullish(),
|
||||
dmnAddr: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type SiteFormData = z.infer<typeof siteFormSchema>;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
BannerFormData,
|
||||
BannerInfo,
|
||||
BannerList,
|
||||
BannerParams,
|
||||
BannerService,
|
||||
} from "../model/banner-interface";
|
||||
|
||||
export class BannerApiService implements BannerService {
|
||||
public async getBannerList(params: BannerParams): Promise<BannerList> {
|
||||
const response = await client.get<BannerList>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getBannerInfo(bnrSn: string): Promise<BannerInfo> {
|
||||
const response = await client.get<BannerInfo>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createBanner(data: BannerFormData): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
ENDPOINT.CMS_SERVICE.BANNERS,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateBanner(
|
||||
bnrSn: string,
|
||||
data: BannerFormData,
|
||||
): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteBanner(bnrSn: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const BannerApi = new BannerApiService();
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { BannerPage } from "./ui/banner-page";
|
||||
@@ -0,0 +1,57 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const BANNER_TYPE = {
|
||||
BNRMAIN: "BNRMAIN",
|
||||
BNRFTER: "BNRFTER",
|
||||
} as const;
|
||||
|
||||
export type BannerType = (typeof BANNER_TYPE)[keyof typeof BANNER_TYPE];
|
||||
|
||||
export interface BannerParams extends PageableParams {
|
||||
bnrTypeCd?: BannerType;
|
||||
bnrTtl?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface Banner {
|
||||
bnrSn: number;
|
||||
bnrTypeCd: BannerType;
|
||||
bnrTypeNm: string;
|
||||
bnrTtl: string;
|
||||
lnkgUrlAddr: string;
|
||||
useYn: string;
|
||||
atflCd: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export interface BannerInfo extends Omit<Banner, "mdfr"> {
|
||||
bnrExpln: string;
|
||||
newWndwYn: string;
|
||||
atflCd: string;
|
||||
regDt: string;
|
||||
rgTrNm: string;
|
||||
}
|
||||
|
||||
export const bannerFormSchema = z.object({
|
||||
bnrTypeCd: z.string(),
|
||||
bnrTtl: z.string(),
|
||||
bnrExpln: z.string().nullish(),
|
||||
lnkgUrlAddr: z.string().nullish(),
|
||||
newWndwYn: z.string(),
|
||||
atflCd: z.string().nullish(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export type BannerFormData = z.infer<typeof bannerFormSchema>;
|
||||
export type BannerList = PageableList<Banner>;
|
||||
|
||||
export interface BannerService {
|
||||
getBannerList: (params: BannerParams) => Promise<BannerList>;
|
||||
getBannerInfo: (bnrSn: string) => Promise<BannerInfo>;
|
||||
createBanner: (data: BannerFormData) => Promise<void>;
|
||||
updateBanner: (bnrSn: string, data: BannerFormData) => Promise<void>;
|
||||
deleteBanner: (bnrSn: string) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import Image from "next/image";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox, Skeleton } from "@common/components/ui";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { Banner } from "./banner-interface";
|
||||
|
||||
export const columns = (
|
||||
imageRefs: React.MutableRefObject<Map<number, string>>,
|
||||
): ColumnDef<Banner>[] => [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "배너 이미지",
|
||||
cell: ({ row }) => (
|
||||
<BannerImage
|
||||
atflCd={row.original.atflCd}
|
||||
bnrSn={row.original.bnrSn}
|
||||
imageRefs={imageRefs}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "bnrTtl",
|
||||
header: "배너명",
|
||||
},
|
||||
{
|
||||
accessorKey: "bnrTypeNm",
|
||||
header: "배너구분",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("mdfcnDt") as string;
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type BannerImageProps = {
|
||||
atflCd: string;
|
||||
bnrSn: number;
|
||||
imageRefs: React.MutableRefObject<Map<number, string>>;
|
||||
};
|
||||
|
||||
const BannerImage = ({ atflCd, bnrSn, imageRefs }: BannerImageProps) => {
|
||||
const prevImage = imageRefs.current.get(bnrSn);
|
||||
|
||||
const { data: imageUrl, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
|
||||
queryFn: () => AttachmentApi.getImageUrlByAtflCd(atflCd),
|
||||
select: (url) => {
|
||||
if (url) imageRefs.current.set(bnrSn, url);
|
||||
return url;
|
||||
},
|
||||
enabled: !prevImage && !!atflCd,
|
||||
});
|
||||
|
||||
if (isLoading) return <Skeleton className="h-[4rem] w-[16rem]" />;
|
||||
|
||||
if (!imageUrl) return null;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-[4rem] w-[16rem] items-center justify-center">
|
||||
<Image src={imageUrl} className="object-contain" fill alt="배너 이미지" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Post } from "@/entities/posting";
|
||||
|
||||
export const columns: ColumnDef<Post>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트",
|
||||
},
|
||||
{
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,297 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { HttpResponse, Upload } from "tus-js-client";
|
||||
import {
|
||||
FileUploader,
|
||||
Input,
|
||||
Label,
|
||||
Progress,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { setCookie } from "@/shared/lib/cookie";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import type { BannerFormData } from "../model/banner-interface";
|
||||
import { handleUpload } from "../lib/utils";
|
||||
|
||||
type BannerFormProps = {
|
||||
form: UseFormReturn<BannerFormData>;
|
||||
bnrSn?: number;
|
||||
handleSave: (data: BannerFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
|
||||
const atflCd = form.watch("atflCd");
|
||||
|
||||
const [imageUrl, setImageUrl] = useState("");
|
||||
const [upload, setUpload] = useState<Upload | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const { data: attachmentData } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd],
|
||||
queryFn: () => AttachmentApi.getAttachments(atflCd!),
|
||||
select: (data) => data?.[data.length - 1],
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const handleProgress = (percentage: string) => {
|
||||
setProgress(Number(percentage));
|
||||
};
|
||||
|
||||
const handleUploadResponse = async (res: HttpResponse) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
|
||||
if (atflCd) form.setValue("atflCd", atflCd);
|
||||
if (atflId) setImageUrl(await AttachmentApi.getImageUrl(atflId));
|
||||
if (attachmentData)
|
||||
await AttachmentApi.deleteAttachmentByAtflCd(attachmentData.atflCd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!attachmentData) return;
|
||||
|
||||
(async () => {
|
||||
setImageUrl(await AttachmentApi.getImageUrl(attachmentData.atflId));
|
||||
})();
|
||||
}, [attachmentData]);
|
||||
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!bnrSn} {...props}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>배너명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bnrTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="배너명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>배너구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bnrTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="BNRTYPE"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>배너 이미지</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflCd"
|
||||
render={() => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
className="!h-auto !max-w-none flex-col !items-start"
|
||||
accept=".jpeg,.jpg,.png"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
setCookie("atfl-cd-key", "");
|
||||
handleUpload(files, {
|
||||
handleUploadStart: setUpload,
|
||||
handleProgress,
|
||||
handleSuccess: setUpload,
|
||||
handleResponse: handleUploadResponse,
|
||||
});
|
||||
}}
|
||||
customPreview={
|
||||
<>
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] overflow-hidden rounded-sm border border-border bg-accent">
|
||||
<div className="relative h-full w-full">
|
||||
{imageUrl && (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
className="object-contain"
|
||||
fill
|
||||
alt="업로드된 이미지"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="absolute bottom-3 left-0 z-10 p-2 text-base leading-none">
|
||||
{(upload?.file as File)?.name}
|
||||
</p>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0 z-10"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<div className="mb-2 text-lg font-medium text-label">
|
||||
<ul className="ml-10 list-disc">
|
||||
<li>
|
||||
이미지 파일(jpg, jpeg, png, gif)만 업로드
|
||||
가능합니다.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>링크 URL</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lnkgUrlAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="배너에 연결할 링크를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>링크 열기</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newWndwYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="newWndwYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="newWndwN" />
|
||||
<Label htmlFor="newWndwN">현재창</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="newWndwY" />
|
||||
<Label htmlFor="newWndwY">새창</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>배너 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bnrExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem] !leading-6"
|
||||
placeholder="배너에 대한 설명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<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>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { BannerForm };
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { columns } from "../model/banner-table-columns";
|
||||
import { type Banner, type BannerParams } from "../model/banner-interface";
|
||||
import { PreviewBannerDialog } from "./preview-banner-dialog";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
function BannerPage() {
|
||||
const router = useRouter();
|
||||
const imageRefs = useRef<Map<number, string>>(new Map());
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [bannerQuery, setBannerQuery] = useState<BannerParams>({ bnrTtl: "" });
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: bannerList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS],
|
||||
queryFn: () =>
|
||||
BannerApi.getBannerList({
|
||||
...bannerQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: bannerList?.content ?? [],
|
||||
columns: columns(imageRefs),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: bannerList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Banner) => {
|
||||
router.push(`/banner/register/${row.bnrSn}`);
|
||||
};
|
||||
|
||||
const getSelectedBannerImages = () => {
|
||||
const selectedImages: string[] = [];
|
||||
table.getFilteredSelectedRowModel().rows.forEach((row) => {
|
||||
const imageUrl = imageRefs.current.get(row.original.bnrSn);
|
||||
if (imageUrl) selectedImages.push(imageUrl);
|
||||
});
|
||||
return selectedImages;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
pstgTrgtNm: row.original.bnrTtl,
|
||||
pstgTrgtNo: row.original.bnrSn,
|
||||
}));
|
||||
|
||||
if (!postingTargets.length) {
|
||||
setPostingTargets([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setPostingTargets(postingTargets);
|
||||
}, [rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
배너명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={bannerQuery.bnrTtl}
|
||||
onChange={(e) =>
|
||||
setBannerQuery({ ...bannerQuery, bnrTtl: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">배너 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<PreviewBannerDialog images={getSelectedBannerImages()} />
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.BANNER}
|
||||
postingTargets={postingTargets}
|
||||
/>
|
||||
<Button onClick={() => router.push("/banner/register")}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BannerPage };
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
bannerFormSchema,
|
||||
type BannerFormData,
|
||||
} from "../model/banner-interface";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { BannerForm } from "./banner-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createBanner } = useMutation({
|
||||
mutationFn: (data: BannerFormData) => BannerApi.createBanner(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BannerFormData>({
|
||||
resolver: zodResolver(bannerFormSchema),
|
||||
defaultValues: {
|
||||
newWndwYn: "N",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(form.formState.errors);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">배너 등록</h3>
|
||||
</div>
|
||||
<BannerForm form={form} handleSave={createBanner} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components/ui";
|
||||
import { PostingApi, type PostPayloadData } from "@/entities/posting";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
bannerFormSchema,
|
||||
type BannerFormData,
|
||||
} from "../model/banner-interface";
|
||||
import { columns } from "../model/posting-table-columns";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { BannerForm } from "./banner-form";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: bannerDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, sequential],
|
||||
queryFn: () => BannerApi.getBannerInfo(sequential),
|
||||
});
|
||||
|
||||
const { data: postHistory } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
queryFn: () =>
|
||||
PostingApi.getPostingHistory({ pstgTrgtNo: Number(sequential) }),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateBanner } = useMutation({
|
||||
mutationFn: (data: BannerFormData) =>
|
||||
BannerApi.updateBanner(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteBanner } = useMutation({
|
||||
mutationFn: () => BannerApi.deleteBanner(sequential),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: savePosting } = useMutation({
|
||||
mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BannerFormData>({
|
||||
resolver: zodResolver(bannerFormSchema),
|
||||
values: bannerDetail,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: postHistory?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleDeletePostingHistory = async () => {
|
||||
const data = table.getFilteredSelectedRowModel().rows.map((row) => ({
|
||||
...row.original,
|
||||
mode: "D",
|
||||
}));
|
||||
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
await savePosting(data);
|
||||
alert("삭제되었습니다.");
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-6 flex flex-col">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">배너 수정</h3>
|
||||
</div>
|
||||
<BannerForm
|
||||
form={form}
|
||||
bnrSn={Number(sequential)}
|
||||
handleSave={updateBanner}
|
||||
handleDelete={deleteBanner}
|
||||
/>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
사이트 게시 이력
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
disabled={!table.getFilteredSelectedRowModel().rows.length}
|
||||
onClick={handleDeletePostingHistory}
|
||||
>
|
||||
이력삭제
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Button,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from "@common/components";
|
||||
|
||||
type PreviewBannerDialogProps = {
|
||||
images: string[];
|
||||
};
|
||||
|
||||
function PreviewBannerDialog({ images }: PreviewBannerDialogProps) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="white" disabled={!images.length}>
|
||||
<i className="diveicon di-display" />
|
||||
미리보기
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>배너 이미지 미리보기</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 px-12">
|
||||
<Carousel>
|
||||
<CarouselContent>
|
||||
{images.map((image, i) => (
|
||||
<CarouselItem key={`banner-${i}`} className="basis-1/3">
|
||||
<div className="relative flex aspect-video items-center justify-center">
|
||||
<Image
|
||||
src={image}
|
||||
className="object-contain"
|
||||
fill
|
||||
alt={`배너 이미지 ${i + 1}`}
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="!bg-accent text-accent-foreground" />
|
||||
<CarouselNext className="!bg-accent text-accent-foreground" />
|
||||
</Carousel>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviewBannerDialog };
|
||||
@@ -11,13 +11,13 @@ export type ImageType = (typeof IMAGE_TYPE)[keyof typeof IMAGE_TYPE];
|
||||
|
||||
export interface ImageParams extends PageableParams {
|
||||
keyword: string;
|
||||
imgTypeCd: string;
|
||||
useYn: string;
|
||||
imgTypeCd?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
imgSn: number;
|
||||
imgTypeCd: string;
|
||||
imgTypeCd: ImageType;
|
||||
imgTypeNm: string;
|
||||
imgTtl: string;
|
||||
lnkgUrlAddr: string;
|
||||
|
||||
+1
-2
@@ -37,7 +37,7 @@ export const columns: ColumnDef<Image>[] = [
|
||||
accessorKey: "imgTypeCd",
|
||||
header: "이미지구분",
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue("imgTypeCd") as string;
|
||||
const type = row.getValue("imgTypeCd");
|
||||
switch (type) {
|
||||
case IMAGE_TYPE.IMGSUB:
|
||||
return "서브이미지";
|
||||
@@ -57,7 +57,6 @@ export const columns: ColumnDef<Image>[] = [
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
cell: ({ row }) => (row.getValue("useYn") === "Y" ? "사용" : "미사용"),
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Post } from "@/entities/posting";
|
||||
|
||||
export const columns: ColumnDef<Post>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트",
|
||||
},
|
||||
{
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { HttpResponse, Upload } from "tus-js-client";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import type { HttpResponse } from "tus-js-client";
|
||||
import {
|
||||
Button,
|
||||
FileUploader,
|
||||
Input,
|
||||
Label,
|
||||
@@ -19,298 +18,290 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { handleUpload } from "../lib/utils";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useTus } from "@/shared/lib/use-tus";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { useAttachmentStore } from "@/shared/lib/use-attachment-store";
|
||||
import type { ImageFormData } from "../model/image-interface";
|
||||
|
||||
type ImageFormProps = {
|
||||
form: UseFormReturn<ImageFormData>;
|
||||
imgSn?: string;
|
||||
imgSn?: number;
|
||||
handleSave: (data: ImageFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
|
||||
const isModifyMode = !!imgSn;
|
||||
function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
|
||||
const atflYn = form.watch("atflYn");
|
||||
const atflCd = form.watch("atflCd");
|
||||
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [upload, setUpload] = useState<Upload | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const {
|
||||
file: uploadFile,
|
||||
response: uploadResponse,
|
||||
progress,
|
||||
setFile,
|
||||
} = useTus();
|
||||
const { setAttachmentFileCode } = useAttachmentStore((state) => state);
|
||||
const [imageUrl, setImageUrl] = useState("");
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
const { data: attachmentData } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd],
|
||||
queryFn: () => AttachmentApi.getAttachments(atflCd!),
|
||||
select: (data) => data?.[data.length - 1],
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const onSubmit = async (data: ImageFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
await handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
await handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleProgress = (percentage: string) => {
|
||||
setProgress(Number(percentage));
|
||||
};
|
||||
|
||||
const handleUploadResponse = (res: HttpResponse) => {
|
||||
const handleUploadResponse = async (res: HttpResponse) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
form.setValue("atflCd", JSON.parse(fileInfo)?.atflCd);
|
||||
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
|
||||
if (atflCd) form.setValue("atflCd", atflCd);
|
||||
if (atflId) setImageUrl(await AttachmentApi.getImageUrl(atflId));
|
||||
if (attachmentData)
|
||||
await AttachmentApi.deleteAttachmentByAtflCd(attachmentData.atflCd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!uploadResponse) return;
|
||||
handleUploadResponse(uploadResponse);
|
||||
}, [uploadResponse]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setImageUrl(
|
||||
await AttachmentApi.getImageUrlByAtflCd(attachmentData?.atflCd ?? ""),
|
||||
);
|
||||
})();
|
||||
}, [attachmentData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>이미지명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imgTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="이미지명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>정렬순서</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sortSeq"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (isNaN(Number(value))) return;
|
||||
field.onChange(Number(value));
|
||||
}}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
placeholder="정렬순서를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>이미지유형</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imgTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="IMGTYPE"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="atflYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="atflY" />
|
||||
<Label htmlFor="atflY">이미지파일</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="atflN" />
|
||||
<Label htmlFor="atflN">외부링크</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{atflYn === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead>이미지파일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflCd"
|
||||
render={() => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
className="!h-auto !max-w-none flex-col !items-start"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
handleUpload(files, {
|
||||
handleUploadStart: setUpload,
|
||||
handleProgress,
|
||||
handleSuccess: setUpload,
|
||||
handleResponse: handleUploadResponse,
|
||||
});
|
||||
}}
|
||||
customPreview={
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] rounded-sm border border-border">
|
||||
<p className="absolute left-0 top-0 p-2 text-base leading-none">
|
||||
{(upload?.file as File)?.name}
|
||||
</p>
|
||||
<div></div>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0"
|
||||
/>
|
||||
<FormWrapper form={form} isModifyMode={!!imgSn} {...props}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>이미지명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imgTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="이미지명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>정렬순서</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sortSeq"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (isNaN(Number(value))) return;
|
||||
field.onChange(Number(value));
|
||||
}}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
placeholder="정렬순서를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>이미지유형</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imgTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="IMGTYPE"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="atflYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="atflY" />
|
||||
<Label htmlFor="atflY">이미지파일</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="atflN" />
|
||||
<Label htmlFor="atflN">외부링크</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{atflYn === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead>이미지파일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflCd"
|
||||
render={() => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
className="!h-auto !max-w-none flex-col !items-start"
|
||||
accept=".jpeg,.jpg,.png"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
setAttachmentFileCode("");
|
||||
setFile(files[0] ?? null);
|
||||
}}
|
||||
customPreview={
|
||||
<>
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] overflow-hidden rounded-sm border border-border bg-accent">
|
||||
<div className="relative h-full w-full">
|
||||
{imageUrl && (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
className="object-contain"
|
||||
fill
|
||||
alt="업로드된 이미지"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{atflYn === "N" && (
|
||||
<TableRow>
|
||||
<TableHead>외부링크</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lnkgUrlAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="외부링크를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<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>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
<p className="absolute bottom-3 left-0 z-10 p-2 text-base leading-none">
|
||||
{uploadFile?.name}
|
||||
</p>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0 z-10"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<div className="mb-2 text-lg font-medium text-label">
|
||||
<ul className="ml-10 list-disc">
|
||||
<li>권장 사이즈: 1920x500</li>
|
||||
<li>파일 최대 크기: 2MB</li>
|
||||
<li>확장자: jpg, jpeg, png 가능</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
{atflYn === "N" && (
|
||||
<TableRow>
|
||||
<TableHead>외부링크</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lnkgUrlAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="외부링크를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<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>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,29 +6,40 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { columns } from "../model/image-table-columns";
|
||||
import { type Image, type ImageParams } from "../model/image-interface";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
function ImagePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({
|
||||
keyword: "",
|
||||
imgTypeCd: "",
|
||||
useYn: "",
|
||||
page: 0,
|
||||
size: 100,
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({ keyword: "" });
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: imageList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES],
|
||||
queryFn: () => ImageApi.getImageList(imageQuery),
|
||||
queryFn: () =>
|
||||
ImageApi.getImageList({
|
||||
...imageQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
@@ -36,10 +47,14 @@ function ImagePage() {
|
||||
data: imageList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: imageList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
@@ -52,9 +67,25 @@ function ImagePage() {
|
||||
router.push(`/image/register/${row.imgSn}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
pstgTrgtNm: row.original.imgTtl,
|
||||
pstgTrgtNo: row.original.imgSn,
|
||||
}));
|
||||
|
||||
if (!postingTargets.length) {
|
||||
setPostingTargets([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setPostingTargets(postingTargets);
|
||||
}, [rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
@@ -77,11 +108,21 @@ function ImagePage() {
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 목록</h3>
|
||||
<Button onClick={() => router.push("/image/register")}>등록</Button>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.IMAGE}
|
||||
postingTargets={postingTargets}
|
||||
/>
|
||||
<Button onClick={() => router.push("/image/register")}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,55 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components/ui";
|
||||
import { PostingApi, type PostPayloadData } from "@/entities/posting";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
|
||||
import { columns } from "../model/posting-table-columns";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { ImageForm } from "./image-form";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
function ModifyPage({ serial }: { serial: string }) {
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: imageDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, serial],
|
||||
queryFn: () => ImageApi.getImageInfo(serial),
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, sequential],
|
||||
queryFn: () => ImageApi.getImageInfo(sequential),
|
||||
});
|
||||
|
||||
const { data: postHistory } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
queryFn: () =>
|
||||
PostingApi.getPostingHistory({ pstgTrgtNo: Number(sequential) }),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateImage } = useMutation({
|
||||
mutationFn: (data: ImageFormData) => ImageApi.updateImage(serial, data),
|
||||
mutationFn: (data: ImageFormData) => ImageApi.updateImage(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteImage } = useMutation({
|
||||
mutationFn: () => ImageApi.deleteImage(serial),
|
||||
mutationFn: () => ImageApi.deleteImage(sequential),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: savePosting } = useMutation({
|
||||
mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ImageFormData>({
|
||||
resolver: zodResolver(imageFormSchema),
|
||||
values: imageDetail,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: postHistory?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleDeletePostingHistory = async () => {
|
||||
const data = table.getFilteredSelectedRowModel().rows.map((row) => ({
|
||||
...row.original,
|
||||
mode: "D",
|
||||
}));
|
||||
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
await savePosting(data);
|
||||
alert("삭제되었습니다.");
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 수정</h3>
|
||||
<>
|
||||
<div className="mt-6 flex flex-col">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 수정</h3>
|
||||
</div>
|
||||
<ImageForm
|
||||
form={form}
|
||||
imgSn={Number(sequential)}
|
||||
handleSave={updateImage}
|
||||
handleDelete={deleteImage}
|
||||
/>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
사이트 게시 이력
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
disabled={!table.getFilteredSelectedRowModel().rows.length}
|
||||
onClick={handleDeletePostingHistory}
|
||||
>
|
||||
이력삭제
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ImageForm
|
||||
form={form}
|
||||
imgSn={serial}
|
||||
handleSave={updateImage}
|
||||
handleDelete={deleteImage}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,7 @@ export class MenuApiService implements MenuService {
|
||||
}
|
||||
|
||||
public async deleteMenu(menuCd: number): Promise<void> {
|
||||
const response = await client.delete<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
|
||||
);
|
||||
return response.data;
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`);
|
||||
}
|
||||
|
||||
public async getTreeMenuList(siteId: string): Promise<TreeMenu[]> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
export class OrganizationApiService implements OrganizationService {
|
||||
public async getOrganizationUserTreeList(): Promise<Organization[]> {
|
||||
const response = await client.get<Organization[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_OGNZ}/treeuser`,
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { BoardPopup } from "@/entities/board";
|
||||
|
||||
const columnHelper = createColumnHelper<BoardPopup>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor((_, index) => index + 1, {
|
||||
export const columns: ColumnDef<BoardPopup>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
}),
|
||||
columnHelper.accessor("bbsNm", {
|
||||
id: "bbsNm",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsNm",
|
||||
header: "게시판명",
|
||||
}),
|
||||
columnHelper.accessor("bbsTypeNm", {
|
||||
id: "bbsTypeNm",
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsTypeNm",
|
||||
header: "게시판유형",
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
id: "regDt",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "최초생성일",
|
||||
}),
|
||||
columnHelper.accessor("pstCnt", {
|
||||
id: "pstCnt",
|
||||
},
|
||||
{
|
||||
accessorKey: "pstCnt",
|
||||
header: "게시물건수",
|
||||
}),
|
||||
columnHelper.display({
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
header: "선택",
|
||||
cell: ({ row }) => {
|
||||
@@ -37,7 +36,5 @@ const columns = [
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export { columns };
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Content } from "./content-interface";
|
||||
|
||||
const columnHelper = createColumnHelper<Content>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor((_, index) => index + 1, {
|
||||
export const columns: ColumnDef<Content>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
}),
|
||||
columnHelper.accessor("contsTtl", {
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
id: "contsTtl",
|
||||
header: "콘텐츠명",
|
||||
}),
|
||||
cell: ({ row }) => row.original.contsTtl,
|
||||
},
|
||||
// TODO: 최종수정자, 최종수정일, 승인일시 추가
|
||||
columnHelper.display({
|
||||
{
|
||||
id: "select",
|
||||
header: "선택",
|
||||
cell: ({ row }) => {
|
||||
@@ -26,7 +26,5 @@ const columns = [
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export { columns };
|
||||
|
||||
@@ -22,8 +22,8 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
{menuCd && (
|
||||
|
||||
@@ -33,8 +33,7 @@ function ContentSearchDialog({
|
||||
|
||||
const { data: contentList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS],
|
||||
queryFn: () =>
|
||||
ContentApi.getContentList({ contsTtl, useYn: "Y", page: 0, size: 100 }),
|
||||
queryFn: () => ContentApi.getContentList({ contsTtl }),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { Form } from "@/shared/ui/form";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { ManagementAuthorityDialog } from "@/entities/menu-authority";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
import { BaseMenuForm } from "./base-menu-form";
|
||||
import { TypeMenuForm } from "./type-menu-form";
|
||||
import { PermissionsManagementDialog } from "./permissions-management-dialog";
|
||||
|
||||
interface MenuFormProps {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
@@ -33,13 +31,6 @@ function MenuForm({
|
||||
const isModifyMode = !!menuCd;
|
||||
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: MenuFormData) => {
|
||||
if (isModifyMode) {
|
||||
@@ -50,62 +41,53 @@ function MenuForm({
|
||||
};
|
||||
|
||||
const onCreate = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
handleCreate(data);
|
||||
}
|
||||
});
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
handleCreate(data);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdate = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("수정하시겠습니까?")) {
|
||||
handleUpdate({ menuCd: menuCd!, data });
|
||||
}
|
||||
});
|
||||
if (await confirm("수정하시겠습니까?")) {
|
||||
handleUpdate({ menuCd: menuCd!, data });
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.(menuCd!);
|
||||
}
|
||||
});
|
||||
const onDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.(menuCd!);
|
||||
}
|
||||
};
|
||||
|
||||
const menuType = form.watch("menuLnkgTypeCd");
|
||||
const isSubmitDisabled = !form.watch("siteId");
|
||||
|
||||
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">
|
||||
{isModifyMode ? "메뉴 수정" : "메뉴 등록"}
|
||||
</h3>
|
||||
<PermissionsManagementDialog menuCd={menuCd} />
|
||||
</div>
|
||||
<div className="card">
|
||||
<BaseMenuForm form={form} menuCd={menuCd} />
|
||||
<TypeMenuForm form={form} menuType={menuType} />
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{isModifyMode ? "메뉴 수정" : "메뉴 등록"}
|
||||
</h3>
|
||||
<ManagementAuthorityDialog menuCd={menuCd} />
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button size="lg" type="submit" disabled={isSubmitDisabled}>
|
||||
저장
|
||||
<div className="card">
|
||||
<BaseMenuForm form={form} menuCd={menuCd} />
|
||||
<TypeMenuForm form={form} menuType={menuType} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button size="lg" type="submit" disabled={isSubmitDisabled}>
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ function MenuPage() {
|
||||
|
||||
const { data: options } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.getSiteList({}),
|
||||
queryFn: () => SiteApi.getSiteList(),
|
||||
select: (data) =>
|
||||
data.content.map((site) => ({
|
||||
label: site.siteNm,
|
||||
@@ -179,6 +179,7 @@ function MenuPage() {
|
||||
setOnConfirm(false);
|
||||
deleteMenu(selectedMenu!.menuCd!);
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const handleAddMenu = () => {
|
||||
|
||||
@@ -43,7 +43,7 @@ function OrganizationSearchDialog({
|
||||
);
|
||||
|
||||
const { data: treeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.USER_OGNZ],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION],
|
||||
queryFn: () => OrganizationApi.getOrganizationUserTreeList(),
|
||||
select: (data) => generateOrganizationTreeData(data),
|
||||
});
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Button, Dialog, DialogTrigger } from "@common/components/ui";
|
||||
|
||||
type PermissionsManagementDialogProps = {
|
||||
menuCd?: number;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function PermissionsManagementDialog({
|
||||
...props
|
||||
}: PermissionsManagementDialogProps) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3">
|
||||
<i className="diveicon di-personal-information" />
|
||||
권한관리
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PermissionsManagementDialog };
|
||||
@@ -55,8 +55,8 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
return (
|
||||
<Table variant="secondary" className="-mt-[0.1rem]">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
{menuType === "CONTENTS" && (
|
||||
|
||||
@@ -1,48 +1,43 @@
|
||||
import dayjs from "dayjs";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import type { Site } from "@/entities/site";
|
||||
|
||||
const columnHelper = createColumnHelper<Site>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor((_, index) => index + 1, {
|
||||
export const columns: ColumnDef<Site>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
}),
|
||||
columnHelper.accessor("siteId", {
|
||||
id: "siteId",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "siteId",
|
||||
header: "사이트코드",
|
||||
}),
|
||||
columnHelper.accessor("siteNm", {
|
||||
id: "siteNm",
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트명",
|
||||
}),
|
||||
columnHelper.accessor("dmnAddr", {
|
||||
id: "dmnAddr",
|
||||
},
|
||||
{
|
||||
accessorKey: "dmnAddr",
|
||||
header: "대표 URL",
|
||||
}),
|
||||
columnHelper.accessor("bscSiteYn", {
|
||||
id: "bscSiteYn",
|
||||
},
|
||||
{
|
||||
accessorKey: "bscSiteYn",
|
||||
header: "기본 사이트 여부",
|
||||
cell: (info) => (info.getValue() ? "Y" : "N"),
|
||||
}),
|
||||
columnHelper.accessor("useYn", {
|
||||
id: "useYn",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용 여부",
|
||||
cell: (info) => (info.getValue() ? "Y" : "N"),
|
||||
}),
|
||||
columnHelper.accessor("rgTrNm", {
|
||||
id: "rgTrNm",
|
||||
},
|
||||
{
|
||||
accessorKey: "rgTrNm",
|
||||
header: "등록자",
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
id: "regDt",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "등록일",
|
||||
cell: (info) => {
|
||||
const date = dayjs(info.getValue());
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.regDt);
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export { columns };
|
||||
|
||||
@@ -29,9 +29,9 @@ function CreatePage() {
|
||||
faxNo: "",
|
||||
telNo: "",
|
||||
lwndCn: "",
|
||||
bscSiteYn: "",
|
||||
bscSiteYn: "Y",
|
||||
bkmkIcon: "",
|
||||
useYn: "",
|
||||
useYn: "Y",
|
||||
siteSkn: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
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 = {
|
||||
@@ -79,7 +78,6 @@ function DomainManagementDialog({
|
||||
name: "domains",
|
||||
});
|
||||
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const initialForm = () => {
|
||||
@@ -88,14 +86,11 @@ function DomainManagementDialog({
|
||||
};
|
||||
|
||||
const onSubmit = async (data: DomainFormData) => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
await mutateAsync(data.domains);
|
||||
handleSaveDomain?.(data.domains);
|
||||
alert("저장되었습니다.");
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const handleAddDomain = () => {
|
||||
@@ -153,160 +148,157 @@ function DomainManagementDialog({
|
||||
fields.length > 0 && selectedRows.length === fields.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<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}>
|
||||
항목 추가
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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} />
|
||||
</>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import { Form, FormControl, FormField, FormItem } 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 = {
|
||||
@@ -75,7 +74,6 @@ function IPManagementDialog({
|
||||
name: "ips",
|
||||
});
|
||||
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const initialForm = () => {
|
||||
@@ -84,9 +82,7 @@ function IPManagementDialog({
|
||||
};
|
||||
|
||||
const onSubmit = async (data: IPFormData) => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
await mutateAsync(data.ips);
|
||||
alert("저장되었습니다.");
|
||||
}
|
||||
@@ -139,139 +135,136 @@ function IPManagementDialog({
|
||||
fields.length > 0 && selectedRows.length === fields.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<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}>
|
||||
항목 추가
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
</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} />
|
||||
</>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
@@ -14,10 +11,10 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { SiteFormData } from "@/entities/site";
|
||||
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import type { Domain } from "../model/domain-interface";
|
||||
import { IPManagementDialog } from "./ip-management-dialog";
|
||||
import { DomainManagementDialog } from "./domain-management-dialog";
|
||||
@@ -29,21 +26,12 @@ type SiteFormProps = {
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
function SiteForm({ form, siteId, ...props }: SiteFormProps) {
|
||||
const isModifyMode = !!siteId;
|
||||
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [isIpManagementOpen, setIsIpManagementOpen] = useState(false);
|
||||
const [isDomainManagementOpen, setIsDomainManagementOpen] = useState(false);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const handleSaveDomain = (rows: Domain[]) => {
|
||||
form.setValue(
|
||||
"dmnAddr",
|
||||
@@ -51,357 +39,314 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: SiteFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>사이트 코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled={isModifyMode}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteKndCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="SITEKNDCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isModifyMode && (
|
||||
<IPManagementDialog
|
||||
siteId={siteId}
|
||||
triggerDisabled={form.watch("siteKndCd") === "O"}
|
||||
open={isIpManagementOpen}
|
||||
onOpenChange={setIsIpManagementOpen}
|
||||
<FormWrapper form={form} isModifyMode={isModifyMode} {...props}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>사이트 코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled={isModifyMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 디자인</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteSkn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="SITESKINCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteKndCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="SITEKNDCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isModifyMode && (
|
||||
<IPManagementDialog
|
||||
siteId={siteId}
|
||||
triggerDisabled={form.watch("siteKndCd") === "O"}
|
||||
open={isIpManagementOpen}
|
||||
onOpenChange={setIsIpManagementOpen}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 설명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isModifyMode && (
|
||||
<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("dmnAddr")}
|
||||
</div>
|
||||
<DomainManagementDialog
|
||||
siteId={siteId}
|
||||
open={isDomainManagementOpen}
|
||||
onOpenChange={setIsDomainManagementOpen}
|
||||
handleSaveDomain={handleSaveDomain}
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 디자인</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteSkn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="SITESKINCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 주소</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 주소를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 전화번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="telNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 전화번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 팩스번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="faxNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 팩스번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lwndCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>북마크 아이콘</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bkmkIcon"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>기본 사이트 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bscSiteYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="bscSiteYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="bscSiteY" />
|
||||
<Label htmlFor="bscSiteY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="bscSiteN" />
|
||||
<Label htmlFor="bscSiteN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<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>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 설명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
<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("dmnAddr")}
|
||||
</div>
|
||||
<DomainManagementDialog
|
||||
siteId={siteId}
|
||||
open={isDomainManagementOpen}
|
||||
onOpenChange={setIsDomainManagementOpen}
|
||||
handleSaveDomain={handleSaveDomain}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 주소</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 주소를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 전화번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="telNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 전화번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 팩스번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="faxNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 팩스번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lwndCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>북마크 아이콘</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bkmkIcon"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>기본 사이트 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bscSiteYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="bscSiteYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="bscSiteY" />
|
||||
<Label htmlFor="bscSiteY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="bscSiteN" />
|
||||
<Label htmlFor="bscSiteN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<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>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,12 @@ import "@common/assets/styles/grid.css";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { SiteApi, type Site, type SiteParams } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
@@ -13,20 +17,25 @@ import { columns } from "../model/table-columns";
|
||||
|
||||
function SitePage() {
|
||||
const router = useRouter();
|
||||
const { data: siteList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.getSiteList(siteQuery),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const [siteQuery, setSiteQuery] = useState<SiteParams>({
|
||||
siteNm: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: siteList?.content ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
const { data: siteList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () =>
|
||||
SiteApi.getSiteList({
|
||||
...siteQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -38,9 +47,21 @@ function SitePage() {
|
||||
router.push(`/site/register/${row.siteId}`);
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: siteList?.content ?? [],
|
||||
columns,
|
||||
rowCount: siteList?.totalElements ?? 0,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
@@ -64,8 +85,8 @@ function SitePage() {
|
||||
<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">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
<div className="card !m-[1.2rem] h-[29.2rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
TemplateService,
|
||||
TemplateList,
|
||||
TemplateParams,
|
||||
TemplateFormData,
|
||||
Template,
|
||||
} from "../model/template-interface";
|
||||
|
||||
export class TemplateApiService implements TemplateService {
|
||||
public async getTemplateList(params?: TemplateParams): Promise<TemplateList> {
|
||||
const response = await client.get<TemplateList>(
|
||||
`${ENDPOINT.CMS_SERVICE.TEMPLATE}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getTemplateInfo(tmpltSn: string): Promise<Template> {
|
||||
const response = await client.get<Template>(
|
||||
`${ENDPOINT.CMS_SERVICE.TEMPLATE}/${tmpltSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createTemplate(data: TemplateFormData): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.TEMPLATE}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateTemplate(
|
||||
tmpltSn: string,
|
||||
data: TemplateFormData,
|
||||
): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.TEMPLATE}/${tmpltSn}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteTemplate(tmpltSn: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.TEMPLATE}/${tmpltSn}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const TemplateApi = new TemplateApiService();
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { TemplatePage } from "./ui/template-page";
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface TemplateParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
tmpltSeCd?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
tmpltSn: number;
|
||||
tmpltNm: string;
|
||||
tmpltCn: string;
|
||||
tmpltSeCd: string;
|
||||
tmpltSeNm: string;
|
||||
useYn: string;
|
||||
delYn: string;
|
||||
regDt: string;
|
||||
mdfcnDt: string;
|
||||
rgTrNm: string;
|
||||
mdfr: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export const templateFormSchema = z.object({
|
||||
tmpltNm: z.string(),
|
||||
tmpltCn: z.string(),
|
||||
tmpltSeCd: z.string(),
|
||||
useYn: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type TemplateList = PageableList<Template>;
|
||||
export type TemplateFormData = z.infer<typeof templateFormSchema>;
|
||||
|
||||
export interface TemplateService {
|
||||
getTemplateList(params: TemplateParams): Promise<TemplateList>;
|
||||
getTemplateInfo(tmpltSn: string): Promise<Template>;
|
||||
createTemplate(data: TemplateFormData): Promise<void>;
|
||||
updateTemplate(tmpltSn: string, data: TemplateFormData): Promise<void>;
|
||||
deleteTemplate(tmpltSn: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { Template } from "./template-interface";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const columns: ColumnDef<Template>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "tmpltNm",
|
||||
header: "템플릿명",
|
||||
},
|
||||
{
|
||||
accessorKey: "tmpltSeNm",
|
||||
header: "템플릿분류",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("mdfcnDt") as string;
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TemplateApi } from "../api/template-service";
|
||||
import {
|
||||
templateFormSchema,
|
||||
type TemplateFormData,
|
||||
} from "../model/template-interface";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { TemplateForm } from "./template-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { mutateAsync: createTemplate } = useMutation({
|
||||
mutationFn: (data: TemplateFormData) => TemplateApi.createTemplate(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<TemplateFormData>({
|
||||
resolver: zodResolver(templateFormSchema),
|
||||
defaultValues: {
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">템플릿 등록</h3>
|
||||
</div>
|
||||
<TemplateForm form={form} handleSave={createTemplate} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
templateFormSchema,
|
||||
type TemplateFormData,
|
||||
} from "../model/template-interface";
|
||||
import { TemplateApi } from "../api/template-service";
|
||||
import { TemplateForm } from "./template-form";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: templateDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, sequential],
|
||||
queryFn: () => TemplateApi.getTemplateInfo(sequential),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateTemplate } = useMutation({
|
||||
mutationFn: (data: TemplateFormData) =>
|
||||
TemplateApi.updateTemplate(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteTemplate } = useMutation({
|
||||
mutationFn: () => TemplateApi.deleteTemplate(sequential),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<TemplateFormData>({
|
||||
resolver: zodResolver(templateFormSchema),
|
||||
values: templateDetail,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">템플릿 수정</h3>
|
||||
</div>
|
||||
<TemplateForm
|
||||
form={form}
|
||||
tmpltSn={Number(sequential)}
|
||||
handleSave={updateTemplate}
|
||||
handleDelete={deleteTemplate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,138 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import type { TemplateFormData } from "../model/template-interface";
|
||||
|
||||
const Editor = dynamic(
|
||||
() => import("@/entities/editor").then((module) => module.Editor),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface TemplateFormProps {
|
||||
form: UseFormReturn<TemplateFormData>;
|
||||
tmpltSn?: number;
|
||||
handleSave: (data: TemplateFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function TemplateForm({ form, tmpltSn, ...props }: TemplateFormProps) {
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!tmpltSn} {...props}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>템플릿 구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tmpltSeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="TMPLTCLCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>템플릿명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tmpltNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="템플릿명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>템플릿 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tmpltCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Editor
|
||||
value={field.value ?? ""}
|
||||
onEditorChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<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>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { TemplateForm };
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { TemplateApi } from "../api/template-service";
|
||||
import { columns } from "../model/template-table-columns";
|
||||
import type { Template, TemplateParams } from "../model/template-interface";
|
||||
|
||||
function TemplatePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [templateQuery, setTemplateQuery] = useState<TemplateParams>({
|
||||
tmpltSeCd: "",
|
||||
keyword: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: templateList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE],
|
||||
queryFn: () =>
|
||||
TemplateApi.getTemplateList({
|
||||
...templateQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Template) => {
|
||||
router.push(`/template/register/${row.tmpltSn}`);
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: templateList?.content ?? [],
|
||||
columns,
|
||||
rowCount: templateList?.totalElements ?? 0,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex gap-2 p-2">
|
||||
<div className="flex space-x-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="" className="text-label">
|
||||
템플릿분류
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="TMPLTCLCD"
|
||||
value={templateQuery.tmpltSeCd}
|
||||
onValueChange={(value) =>
|
||||
setTemplateQuery({ ...templateQuery, tmpltSeCd: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="" className="text-label">
|
||||
템플릿명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={templateQuery.keyword}
|
||||
onChange={(e) =>
|
||||
setTemplateQuery({
|
||||
...templateQuery,
|
||||
keyword: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">템플릿 목록</h3>
|
||||
<Button onClick={() => router.push("/template/register")}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[29.2rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TemplatePage };
|
||||
@@ -0,0 +1,48 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
TermsService,
|
||||
TermsList,
|
||||
TermsParams,
|
||||
TermsFormData,
|
||||
Terms,
|
||||
} from "../model/terms-interface";
|
||||
|
||||
export class TermsApiService implements TermsService {
|
||||
public async getTermsList(params?: TermsParams): Promise<TermsList> {
|
||||
const response = await client.get<TermsList>(
|
||||
`${ENDPOINT.CMS_SERVICE.TERMS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getTermsInfo(trmsSn: string): Promise<Terms> {
|
||||
const response = await client.get<Terms>(
|
||||
`${ENDPOINT.CMS_SERVICE.TERMS}/${trmsSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createTerms(data: TermsFormData): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.TERMS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateTerms(trmsSn: string, data: TermsFormData): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.TERMS}/${trmsSn}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteTerms(trmsSn: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.TERMS}/${trmsSn}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const TermsApi = new TermsApiService();
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { TermsPage } from "./ui/terms-page";
|
||||
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableParams, PageableList } from "@/shared/model";
|
||||
|
||||
export interface TermsParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
trmsSeCd?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface Terms {
|
||||
trmsSn: number;
|
||||
trmsTtl: string;
|
||||
trmsCn: string;
|
||||
trmsSeCd: string;
|
||||
trmsSeNm: string;
|
||||
useYn: string;
|
||||
delYn: string;
|
||||
regDt: string;
|
||||
mdfcnDt: string;
|
||||
rgTrNm: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export const termsFormSchema = z.object({
|
||||
trmsTtl: z.string(),
|
||||
trmsCn: z.string(),
|
||||
trmsSeCd: z.string(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export type TermsList = PageableList<Terms>;
|
||||
export type TermsFormData = z.infer<typeof termsFormSchema>;
|
||||
|
||||
export interface TermsService {
|
||||
getTermsList(params: TermsParams): Promise<TermsList>;
|
||||
getTermsInfo(trmsSn: string): Promise<Terms>;
|
||||
createTerms(data: TermsFormData): Promise<void>;
|
||||
updateTerms(trmsSn: string, data: TermsFormData): Promise<void>;
|
||||
deleteTerms(trmsSn: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { Terms } from "./terms-interface";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const columns: ColumnDef<Terms>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "trmsTtl",
|
||||
header: "약관명",
|
||||
},
|
||||
{
|
||||
accessorKey: "trmsSeNm",
|
||||
header: "약관구분",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("mdfcnDt") as string;
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TermsApi } from "../api/terms-service";
|
||||
import { termsFormSchema, type TermsFormData } from "../model/terms-interface";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { TermsForm } from "./terms-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createTerms } = useMutation({
|
||||
mutationFn: (data: TermsFormData) => TermsApi.createTerms(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<TermsFormData>({
|
||||
resolver: zodResolver(termsFormSchema),
|
||||
defaultValues: {
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">약관 등록</h3>
|
||||
</div>
|
||||
<TermsForm form={form} handleSave={createTerms} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { termsFormSchema, type TermsFormData } from "../model/terms-interface";
|
||||
import { TermsApi } from "../api/terms-service";
|
||||
import { TermsForm } from "./terms-form";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: termsDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TERMS, sequential],
|
||||
queryFn: () => TermsApi.getTermsInfo(sequential),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateTerms } = useMutation({
|
||||
mutationFn: (data: TermsFormData) => TermsApi.updateTerms(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteTerms } = useMutation({
|
||||
mutationFn: () => TermsApi.deleteTerms(sequential),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<TermsFormData>({
|
||||
resolver: zodResolver(termsFormSchema),
|
||||
values: termsDetail,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">약관 수정</h3>
|
||||
</div>
|
||||
<TermsForm
|
||||
form={form}
|
||||
trmsSn={Number(sequential)}
|
||||
handleSave={updateTerms}
|
||||
handleDelete={deleteTerms}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,138 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import type { TermsFormData } from "../model/terms-interface";
|
||||
|
||||
const Editor = dynamic(
|
||||
() => import("@/entities/editor").then((module) => module.Editor),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface TermsFormProps {
|
||||
form: UseFormReturn<TermsFormData>;
|
||||
trmsSn?: number;
|
||||
handleSave: (data: TermsFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function TermsForm({ form, trmsSn, ...props }: TermsFormProps) {
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!trmsSn} {...props}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>약관 구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="trmsSeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="TRMSCLCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>약관명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="trmsTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="약관명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>약관 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="trmsCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Editor
|
||||
value={field.value ?? ""}
|
||||
onEditorChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<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>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { TermsForm };
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { TermsApi } from "../api/terms-service";
|
||||
import { columns } from "../model/terms-table-columns";
|
||||
import type { TermsParams, Terms } from "../model/terms-interface";
|
||||
|
||||
function TermsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [termsQuery, setTermsQuery] = useState<TermsParams>({
|
||||
trmsSeCd: "",
|
||||
keyword: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: termsList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TERMS],
|
||||
queryFn: () =>
|
||||
TermsApi.getTermsList({
|
||||
...termsQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Terms) => {
|
||||
router.push(`/terms/register/${row.trmsSn}`);
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: termsList?.content ?? [],
|
||||
columns,
|
||||
rowCount: termsList?.totalElements ?? 0,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex gap-2 p-2">
|
||||
<div className="flex space-x-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="" className="text-label">
|
||||
약관분류
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="TRMSCLCD"
|
||||
value={termsQuery.trmsSeCd}
|
||||
onValueChange={(value) =>
|
||||
setTermsQuery({ ...termsQuery, trmsSeCd: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="" className="text-label">
|
||||
약관명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={termsQuery.keyword}
|
||||
onChange={(e) =>
|
||||
setTermsQuery({
|
||||
...termsQuery,
|
||||
keyword: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">약관 목록</h3>
|
||||
<Button onClick={() => router.push("/terms/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[29.2rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TermsPage };
|
||||
@@ -5,25 +5,32 @@ export const BASE_URL = `${PROXY_HOST}/server`;
|
||||
export const ENDPOINT = {
|
||||
ATTACH_SERVICE: {
|
||||
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
|
||||
EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor",
|
||||
IMAGES: "/attach-service/api/v1/images",
|
||||
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
USERS: "/user-service/api/v1/users",
|
||||
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
|
||||
AUTHRTS: "/user-service/api/v1/authorizations",
|
||||
ROLES_AUTHRTS: "/user-service/api/v1/role-authorizations",
|
||||
ROLES_AUTHRTS: "/user-service/api/v1/role-authorizations",
|
||||
},
|
||||
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",
|
||||
POSTINGS: "/cms-service/api/v1/site-pstgs",
|
||||
CODE: "/cms-service/api/v1/code/cache",
|
||||
MENUS: "/cms-service/api/v1/menus",
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts",
|
||||
CONTENTS: "/cms-service/api/v1/contents",
|
||||
USER_OGNZ: "/cms-service/api/v1/user-ognz",
|
||||
USER_ORGANIZATION: "/cms-service/api/v1/user-ognz",
|
||||
IMAGES: "/cms-service/api/v1/images",
|
||||
GROUP: "/cms-service/api/v1/groups",
|
||||
BANNERS: "/cms-service/api/v1/bnrs",
|
||||
TEMPLATE: "/cms-service/api/v1/tmplt",
|
||||
TERMS: "/cms-service/api/v1/trms",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { AttachmentStore } from "../store/attachment-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useAttachmentStore = <T>(
|
||||
selector: (store: AttachmentStore) => T,
|
||||
): T => {
|
||||
const store = useContext(StoreContext);
|
||||
if (!store) {
|
||||
throw new Error("useAttachmentStore must be used within StoreProvider");
|
||||
}
|
||||
return useStore(store.attachmentStore, selector);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { type HttpResponse, Upload } from "tus-js-client";
|
||||
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "../config";
|
||||
import { useAttachmentStore } from "./use-attachment-store";
|
||||
import { getCookie } from "./cookie";
|
||||
|
||||
export const useTus = () => {
|
||||
const { attachmentFileCode, setAttachmentFileCode } = useAttachmentStore(
|
||||
(state) => state,
|
||||
);
|
||||
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [upload, setUpload] = useState<Upload | null>(null);
|
||||
const [response, setResponse] = useState<HttpResponse | null>(null);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
const metadata = {
|
||||
filename: file?.name ?? "",
|
||||
filetype: file?.type ?? "",
|
||||
atflCd: attachmentFileCode,
|
||||
};
|
||||
|
||||
const createUpload = () => {
|
||||
return new Upload(file!, {
|
||||
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
|
||||
chunkSize: 10 * 1024 * 1024,
|
||||
headers: {
|
||||
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
||||
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
||||
},
|
||||
metadata,
|
||||
onError(error) {
|
||||
throw new Error(`Failed because: ${error}`);
|
||||
},
|
||||
onProgress(bytesUploaded, bytesTotal) {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
setProgress(Number(percentage));
|
||||
},
|
||||
onAfterResponse(req, res) {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
setAttachmentFileCode(JSON.parse(fileInfo)?.atflCd ?? "");
|
||||
setResponse(res);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!file) return;
|
||||
|
||||
const upload = createUpload();
|
||||
upload.start();
|
||||
setUpload(upload);
|
||||
}, [file]);
|
||||
|
||||
return {
|
||||
file,
|
||||
upload,
|
||||
response,
|
||||
progress,
|
||||
setFile,
|
||||
};
|
||||
};
|
||||
@@ -1,10 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useRef } from "react";
|
||||
import { createAlertStore, createMenuStore, createUserStore } from "../store";
|
||||
import {
|
||||
createAlertStore,
|
||||
createAttachmentStore,
|
||||
createMenuStore,
|
||||
createUserStore,
|
||||
} from "../store";
|
||||
|
||||
type StoreContextType = {
|
||||
alertStore: ReturnType<typeof createAlertStore>;
|
||||
attachmentStore: ReturnType<typeof createAttachmentStore>;
|
||||
menuStore: ReturnType<typeof createMenuStore>;
|
||||
userStore: ReturnType<typeof createUserStore>;
|
||||
};
|
||||
@@ -18,6 +24,7 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
if (storeRef.current === null) {
|
||||
storeRef.current = {
|
||||
alertStore: createAlertStore(),
|
||||
attachmentStore: createAttachmentStore(),
|
||||
menuStore: createMenuStore(),
|
||||
userStore: createUserStore(),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export type AttachmentState = {
|
||||
attachmentFileCode: string;
|
||||
};
|
||||
|
||||
export type AttachmentActions = {
|
||||
setAttachmentFileCode: (attachmentFileCode: string) => void;
|
||||
};
|
||||
|
||||
export type AttachmentStore = AttachmentState & AttachmentActions;
|
||||
|
||||
export const defaultInitState: AttachmentState = {
|
||||
attachmentFileCode: "",
|
||||
};
|
||||
|
||||
export const createAttachmentStore = (
|
||||
initState: AttachmentState = defaultInitState,
|
||||
) => {
|
||||
return createStore<AttachmentStore>()((set) => ({
|
||||
...initState,
|
||||
setAttachmentFileCode: (attachmentFileCode: string) =>
|
||||
set({ attachmentFileCode }),
|
||||
}));
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export { createAlertStore } from "./alert-store";
|
||||
export { createAttachmentStore } from "./attachment-store";
|
||||
export { createMenuStore } from "./menu-store";
|
||||
export { createUserStore, type IUserInfo } from "./user-store";
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@common/components/ui";
|
||||
|
||||
type ConfirmDialogProps = {
|
||||
open: boolean;
|
||||
open?: boolean;
|
||||
confirmButtonProps?: ButtonProps & { label?: string };
|
||||
cancelButtonProps?: ButtonProps & { label?: string };
|
||||
};
|
||||
@@ -19,9 +19,9 @@ function ConfirmDialog({
|
||||
confirmButtonProps,
|
||||
cancelButtonProps,
|
||||
}: ConfirmDialogProps) {
|
||||
const { message, onConfirm, onCancel } = useConfirm();
|
||||
const { message, isOpen, onConfirm, onCancel } = useConfirm();
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onCancel}>
|
||||
<Dialog open={open ?? isOpen} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="text-center text-[1.4rem] text-foreground">{message}</p>
|
||||
|
||||
@@ -9,7 +9,7 @@ function PageLayout({ children }: { children: React.ReactNode }) {
|
||||
.map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container">
|
||||
<div className="h-auto min-h-full bg-container pb-2">
|
||||
<PageTitle
|
||||
title={currentMenuInfo?.menuKornNm ?? ""}
|
||||
menuPaths={menuPaths ?? []}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Skeleton } from "@common/components/ui/skeleton";
|
||||
|
||||
function SkeletonPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-6">
|
||||
<Skeleton className="h-[6rem] rounded-xl" />
|
||||
<Skeleton className="h-[60rem] rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SkeletonPage };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user