-
-
- {isModifyMode ? "메뉴 수정" : "메뉴 등록"}
-
-
-
-
-
-
-
+
+
);
}
diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx
index 71ad0042b..8e474b0b6 100644
--- a/apps/cs/src/pages/menu/ui/menu-page.tsx
+++ b/apps/cs/src/pages/menu/ui/menu-page.tsx
@@ -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 = () => {
diff --git a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx
index 9e0637082..ed825508c 100644
--- a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx
+++ b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx
@@ -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),
});
diff --git a/apps/cs/src/pages/menu/ui/permissions-management-dialog.tsx b/apps/cs/src/pages/menu/ui/permissions-management-dialog.tsx
deleted file mode 100644
index c5f782cbb..000000000
--- a/apps/cs/src/pages/menu/ui/permissions-management-dialog.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Button, Dialog, DialogTrigger } from "@common/components/ui";
-
-type PermissionsManagementDialogProps = {
- menuCd?: number;
-} & React.ComponentProps
;
-
-function PermissionsManagementDialog({
- ...props
-}: PermissionsManagementDialogProps) {
- return (
-
- );
-}
-
-export { PermissionsManagementDialog };
diff --git a/apps/cs/src/pages/menu/ui/type-menu-form.tsx b/apps/cs/src/pages/menu/ui/type-menu-form.tsx
index b2e6d98e2..210c36f17 100644
--- a/apps/cs/src/pages/menu/ui/type-menu-form.tsx
+++ b/apps/cs/src/pages/menu/ui/type-menu-form.tsx
@@ -55,8 +55,8 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
return (
-
-
+
+
{menuType === "CONTENTS" && (
diff --git a/apps/cs/src/pages/site/model/table-columns.tsx b/apps/cs/src/pages/site/model/table-columns.tsx
index 8a2d852cd..c5da01880 100644
--- a/apps/cs/src/pages/site/model/table-columns.tsx
+++ b/apps/cs/src/pages/site/model/table-columns.tsx
@@ -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();
-
-const columns = [
- columnHelper.accessor((_, index) => index + 1, {
+export const columns: ColumnDef[] = [
+ {
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 };
diff --git a/apps/cs/src/pages/site/ui/create-page.tsx b/apps/cs/src/pages/site/ui/create-page.tsx
index e8f8c700d..730a6f412 100644
--- a/apps/cs/src/pages/site/ui/create-page.tsx
+++ b/apps/cs/src/pages/site/ui/create-page.tsx
@@ -29,9 +29,9 @@ function CreatePage() {
faxNo: "",
telNo: "",
lwndCn: "",
- bscSiteYn: "",
+ bscSiteYn: "Y",
bkmkIcon: "",
- useYn: "",
+ useYn: "Y",
siteSkn: "",
},
});
diff --git a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx
index fa754d36b..12b9aee6e 100644
--- a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx
+++ b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx
@@ -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([]);
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 (
- <>
-
-
- >
+
+
+
+
+
+
);
}
diff --git a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx
index dd9ee3748..e17577d16 100644
--- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx
+++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx
@@ -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([]);
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 (
- <>
-
-
- >
+
+
+
+
+
+
);
}
diff --git a/apps/cs/src/pages/site/ui/site-form.tsx b/apps/cs/src/pages/site/ui/site-form.tsx
index cbf90c95b..f624a81b7 100644
--- a/apps/cs/src/pages/site/ui/site-form.tsx
+++ b/apps/cs/src/pages/site/ui/site-form.tsx
@@ -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;
};
-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) => {
- 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 (
- <>
-
-
-
+
);
}
diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx
index e89ddafa0..9ffd1294e 100644
--- a/apps/cs/src/pages/site/ui/site-page.tsx
+++ b/apps/cs/src/pages/site/ui/site-page.tsx
@@ -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({
siteNm: "",
});
+ const [pagination, setPagination] = useState({
+ 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) => {
@@ -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 (
@@ -64,8 +85,8 @@ function SitePage() {
사이트 목록
-
diff --git a/apps/cs/src/pages/template/api/template-service.ts b/apps/cs/src/pages/template/api/template-service.ts
new file mode 100644
index 000000000..27a8dfaa3
--- /dev/null
+++ b/apps/cs/src/pages/template/api/template-service.ts
@@ -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 {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.TEMPLATE}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async getTemplateInfo(tmpltSn: string): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.TEMPLATE}/${tmpltSn}`,
+ );
+ return response.data;
+ }
+
+ public async createTemplate(data: TemplateFormData): Promise {
+ const response = await client.post(
+ `${ENDPOINT.CMS_SERVICE.TEMPLATE}`,
+ data,
+ );
+ return response.data;
+ }
+
+ public async updateTemplate(
+ tmpltSn: string,
+ data: TemplateFormData,
+ ): Promise {
+ const response = await client.put(
+ `${ENDPOINT.CMS_SERVICE.TEMPLATE}/${tmpltSn}`,
+ data,
+ );
+ return response.data;
+ }
+
+ public async deleteTemplate(tmpltSn: string): Promise {
+ await client.delete(`${ENDPOINT.CMS_SERVICE.TEMPLATE}/${tmpltSn}`);
+ }
+}
+
+export const TemplateApi = new TemplateApiService();
diff --git a/apps/cs/src/pages/template/index.ts b/apps/cs/src/pages/template/index.ts
new file mode 100644
index 000000000..9db5d7fc0
--- /dev/null
+++ b/apps/cs/src/pages/template/index.ts
@@ -0,0 +1,3 @@
+export { CreatePage } from "./ui/create-page";
+export { ModifyPage } from "./ui/modify-page";
+export { TemplatePage } from "./ui/template-page";
diff --git a/apps/cs/src/pages/template/model/template-interface.ts b/apps/cs/src/pages/template/model/template-interface.ts
new file mode 100644
index 000000000..1a7bc170b
--- /dev/null
+++ b/apps/cs/src/pages/template/model/template-interface.ts
@@ -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;
+export type TemplateFormData = z.infer;
+
+export interface TemplateService {
+ getTemplateList(params: TemplateParams): Promise;
+ getTemplateInfo(tmpltSn: string): Promise;
+ createTemplate(data: TemplateFormData): Promise;
+ updateTemplate(tmpltSn: string, data: TemplateFormData): Promise;
+ deleteTemplate(tmpltSn: string): Promise;
+}
diff --git a/apps/cs/src/pages/template/model/template-table-columns.tsx b/apps/cs/src/pages/template/model/template-table-columns.tsx
new file mode 100644
index 000000000..4407469d7
--- /dev/null
+++ b/apps/cs/src/pages/template/model/template-table-columns.tsx
@@ -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[] = [
+ {
+ 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") : "";
+ },
+ },
+];
diff --git a/apps/cs/src/pages/template/ui/create-page.tsx b/apps/cs/src/pages/template/ui/create-page.tsx
new file mode 100644
index 000000000..8057ddd00
--- /dev/null
+++ b/apps/cs/src/pages/template/ui/create-page.tsx
@@ -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({
+ resolver: zodResolver(templateFormSchema),
+ defaultValues: {
+ useYn: "Y",
+ },
+ });
+
+ return (
+
+ );
+}
+
+export { CreatePage };
diff --git a/apps/cs/src/pages/template/ui/modify-page.tsx b/apps/cs/src/pages/template/ui/modify-page.tsx
new file mode 100644
index 000000000..987725c7a
--- /dev/null
+++ b/apps/cs/src/pages/template/ui/modify-page.tsx
@@ -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({
+ resolver: zodResolver(templateFormSchema),
+ values: templateDetail,
+ });
+
+ return (
+
+ );
+}
+
+export { ModifyPage };
diff --git a/apps/cs/src/pages/template/ui/template-form.tsx b/apps/cs/src/pages/template/ui/template-form.tsx
new file mode 100644
index 000000000..8716afe00
--- /dev/null
+++ b/apps/cs/src/pages/template/ui/template-form.tsx
@@ -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;
+ tmpltSn?: number;
+ handleSave: (data: TemplateFormData) => Promise;
+ handleDelete?: () => Promise;
+}
+
+function TemplateForm({ form, tmpltSn, ...props }: TemplateFormProps) {
+ return (
+
+
+
+ );
+}
+
+export { TemplateForm };
diff --git a/apps/cs/src/pages/template/ui/template-page.tsx b/apps/cs/src/pages/template/ui/template-page.tsx
new file mode 100644
index 000000000..cb6c52433
--- /dev/null
+++ b/apps/cs/src/pages/template/ui/template-page.tsx
@@ -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({
+ tmpltSeCd: "",
+ keyword: "",
+ });
+ const [pagination, setPagination] = useState({
+ 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) => {
+ 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 (
+
+
+
+
+
+
+
+ setTemplateQuery({ ...templateQuery, tmpltSeCd: value })
+ }
+ />
+
+
+
+
+ setTemplateQuery({
+ ...templateQuery,
+ keyword: e.target.value,
+ })
+ }
+ />
+
+
+
+
+
+
+
+
템플릿 목록
+
+
+
+
+
+
+
+ );
+}
+
+export { TemplatePage };
diff --git a/apps/cs/src/pages/terms/api/terms-service.ts b/apps/cs/src/pages/terms/api/terms-service.ts
new file mode 100644
index 000000000..2f06ada66
--- /dev/null
+++ b/apps/cs/src/pages/terms/api/terms-service.ts
@@ -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 {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.TERMS}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async getTermsInfo(trmsSn: string): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.TERMS}/${trmsSn}`,
+ );
+ return response.data;
+ }
+
+ public async createTerms(data: TermsFormData): Promise {
+ const response = await client.post(
+ `${ENDPOINT.CMS_SERVICE.TERMS}`,
+ data,
+ );
+ return response.data;
+ }
+
+ public async updateTerms(trmsSn: string, data: TermsFormData): Promise {
+ const response = await client.put(
+ `${ENDPOINT.CMS_SERVICE.TERMS}/${trmsSn}`,
+ data,
+ );
+ return response.data;
+ }
+
+ public async deleteTerms(trmsSn: string): Promise {
+ await client.delete(`${ENDPOINT.CMS_SERVICE.TERMS}/${trmsSn}`);
+ }
+}
+
+export const TermsApi = new TermsApiService();
diff --git a/apps/cs/src/pages/terms/index.ts b/apps/cs/src/pages/terms/index.ts
new file mode 100644
index 000000000..fc5d97a3a
--- /dev/null
+++ b/apps/cs/src/pages/terms/index.ts
@@ -0,0 +1,3 @@
+export { CreatePage } from "./ui/create-page";
+export { ModifyPage } from "./ui/modify-page";
+export { TermsPage } from "./ui/terms-page";
diff --git a/apps/cs/src/pages/terms/model/terms-interface.ts b/apps/cs/src/pages/terms/model/terms-interface.ts
new file mode 100644
index 000000000..14a0bb397
--- /dev/null
+++ b/apps/cs/src/pages/terms/model/terms-interface.ts
@@ -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;
+export type TermsFormData = z.infer;
+
+export interface TermsService {
+ getTermsList(params: TermsParams): Promise;
+ getTermsInfo(trmsSn: string): Promise;
+ createTerms(data: TermsFormData): Promise;
+ updateTerms(trmsSn: string, data: TermsFormData): Promise;
+ deleteTerms(trmsSn: string): Promise;
+}
diff --git a/apps/cs/src/pages/terms/model/terms-table-columns.tsx b/apps/cs/src/pages/terms/model/terms-table-columns.tsx
new file mode 100644
index 000000000..b70f47d42
--- /dev/null
+++ b/apps/cs/src/pages/terms/model/terms-table-columns.tsx
@@ -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[] = [
+ {
+ 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") : "";
+ },
+ },
+];
diff --git a/apps/cs/src/pages/terms/ui/create-page.tsx b/apps/cs/src/pages/terms/ui/create-page.tsx
new file mode 100644
index 000000000..509fc91b2
--- /dev/null
+++ b/apps/cs/src/pages/terms/ui/create-page.tsx
@@ -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({
+ resolver: zodResolver(termsFormSchema),
+ defaultValues: {
+ useYn: "Y",
+ },
+ });
+
+ return (
+
+ );
+}
+
+export { CreatePage };
diff --git a/apps/cs/src/pages/terms/ui/modify-page.tsx b/apps/cs/src/pages/terms/ui/modify-page.tsx
new file mode 100644
index 000000000..61522c2eb
--- /dev/null
+++ b/apps/cs/src/pages/terms/ui/modify-page.tsx
@@ -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({
+ resolver: zodResolver(termsFormSchema),
+ values: termsDetail,
+ });
+
+ return (
+
+ );
+}
+
+export { ModifyPage };
diff --git a/apps/cs/src/pages/terms/ui/terms-form.tsx b/apps/cs/src/pages/terms/ui/terms-form.tsx
new file mode 100644
index 000000000..df436e63a
--- /dev/null
+++ b/apps/cs/src/pages/terms/ui/terms-form.tsx
@@ -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;
+ trmsSn?: number;
+ handleSave: (data: TermsFormData) => Promise;
+ handleDelete?: () => Promise;
+}
+
+function TermsForm({ form, trmsSn, ...props }: TermsFormProps) {
+ return (
+
+
+
+ );
+}
+
+export { TermsForm };
diff --git a/apps/cs/src/pages/terms/ui/terms-page.tsx b/apps/cs/src/pages/terms/ui/terms-page.tsx
new file mode 100644
index 000000000..5921e2fdd
--- /dev/null
+++ b/apps/cs/src/pages/terms/ui/terms-page.tsx
@@ -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({
+ trmsSeCd: "",
+ keyword: "",
+ });
+ const [pagination, setPagination] = useState({
+ 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) => {
+ 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 (
+
+
+
+
+
+
+
+ setTermsQuery({ ...termsQuery, trmsSeCd: value })
+ }
+ />
+
+
+
+
+ setTermsQuery({
+ ...termsQuery,
+ keyword: e.target.value,
+ })
+ }
+ />
+
+
+
+
+
+
+
+
약관 목록
+
+
+
+
+
+
+
+ );
+}
+
+export { TermsPage };
diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts
index a9ea4c325..94d2e728d 100644
--- a/apps/cs/src/shared/config/api.ts
+++ b/apps/cs/src/shared/config/api.ts
@@ -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",
diff --git a/apps/cs/src/shared/lib/use-attachment-store.ts b/apps/cs/src/shared/lib/use-attachment-store.ts
new file mode 100644
index 000000000..285699cc6
--- /dev/null
+++ b/apps/cs/src/shared/lib/use-attachment-store.ts
@@ -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 = (
+ 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);
+};
diff --git a/apps/cs/src/shared/lib/use-tus.ts b/apps/cs/src/shared/lib/use-tus.ts
new file mode 100644
index 000000000..fdaeb1f65
--- /dev/null
+++ b/apps/cs/src/shared/lib/use-tus.ts
@@ -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(null);
+ const [upload, setUpload] = useState(null);
+ const [response, setResponse] = useState(null);
+ const [progress, setProgress] = useState(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,
+ };
+};
diff --git a/apps/cs/src/shared/providers/store-provider.tsx b/apps/cs/src/shared/providers/store-provider.tsx
index 82783bdac..a0ec17b61 100644
--- a/apps/cs/src/shared/providers/store-provider.tsx
+++ b/apps/cs/src/shared/providers/store-provider.tsx
@@ -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;
+ attachmentStore: ReturnType;
menuStore: ReturnType;
userStore: ReturnType;
};
@@ -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(),
};
diff --git a/apps/cs/src/shared/store/attachment-store.ts b/apps/cs/src/shared/store/attachment-store.ts
new file mode 100644
index 000000000..faa700949
--- /dev/null
+++ b/apps/cs/src/shared/store/attachment-store.ts
@@ -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()((set) => ({
+ ...initState,
+ setAttachmentFileCode: (attachmentFileCode: string) =>
+ set({ attachmentFileCode }),
+ }));
+};
diff --git a/apps/cs/src/shared/store/index.ts b/apps/cs/src/shared/store/index.ts
index 73c608f05..e26f6781e 100644
--- a/apps/cs/src/shared/store/index.ts
+++ b/apps/cs/src/shared/store/index.ts
@@ -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";
diff --git a/apps/cs/src/shared/ui/confirm-dialog.tsx b/apps/cs/src/shared/ui/confirm-dialog.tsx
index 8f9ce2264..4223d197e 100644
--- a/apps/cs/src/shared/ui/confirm-dialog.tsx
+++ b/apps/cs/src/shared/ui/confirm-dialog.tsx
@@ -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 (
-