feat: 약관관리 페이지 작업

This commit is contained in:
2025-04-11 15:15:12 +09:00
parent 0ef244a5b2
commit b9a5938761
21 changed files with 514 additions and 26 deletions
@@ -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;
@@ -44,10 +44,7 @@ export class SiteApiService implements SiteService {
} }
public async deleteSite(siteId: string): Promise<void> { public async deleteSite(siteId: string): Promise<void> {
const response = await client.delete<void>( await client.delete(`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`);
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
);
return response.data;
} }
} }
@@ -94,10 +94,6 @@ function BannerPage() {
setPostingTargets(postingTargets); setPostingTargets(postingTargets);
}, [rowSelection]); }, [rowSelection]);
useEffect(() => {
refetch();
}, []);
useEffect(() => { useEffect(() => {
refetch(); refetch();
}, [pagination]); }, [pagination]);
@@ -83,10 +83,6 @@ function ImagePage() {
setPostingTargets(postingTargets); setPostingTargets(postingTargets);
}, [rowSelection]); }, [rowSelection]);
useEffect(() => {
refetch();
}, []);
useEffect(() => { useEffect(() => {
refetch(); refetch();
}, [pagination]); }, [pagination]);
+1 -4
View File
@@ -36,10 +36,7 @@ export class MenuApiService implements MenuService {
} }
public async deleteMenu(menuCd: number): Promise<void> { public async deleteMenu(menuCd: number): Promise<void> {
const response = await client.delete<void>( await client.delete(`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`);
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
);
return response.data;
} }
public async getTreeMenuList(siteId: string): Promise<TreeMenu[]> { public async getTreeMenuList(siteId: string): Promise<TreeMenu[]> {
-4
View File
@@ -59,10 +59,6 @@ function SitePage() {
manualPagination: true, manualPagination: true,
}); });
useEffect(() => {
refetch();
}, []);
useEffect(() => { useEffect(() => {
refetch(); refetch();
}, [pagination]); }, [pagination]);
@@ -42,6 +42,10 @@ export class TemplateApiService implements TemplateService {
); );
return response.data; return response.data;
} }
public async deleteTemplate(tmpltSn: string): Promise<void> {
await client.delete(`${ENDPOINT.CMS_SERVICE.TEMPLATE}/${tmpltSn}`);
}
} }
export const TemplateApi = new TemplateApiService(); export const TemplateApi = new TemplateApiService();
@@ -32,9 +32,11 @@ export const templateFormSchema = z.object({
export type TemplateList = PageableList<Template>; export type TemplateList = PageableList<Template>;
export type TemplateFormData = z.infer<typeof templateFormSchema>; export type TemplateFormData = z.infer<typeof templateFormSchema>;
export interface TemplateService { export interface TemplateService {
getTemplateList(params: TemplateParams): Promise<TemplateList>; getTemplateList(params: TemplateParams): Promise<TemplateList>;
getTemplateInfo(tmpltSn: string): Promise<Template>; getTemplateInfo(tmpltSn: string): Promise<Template>;
createTemplate(data: TemplateFormData): Promise<void>; createTemplate(data: TemplateFormData): Promise<void>;
updateTemplate(tmpltSn: string, data: TemplateFormData): Promise<void>; updateTemplate(tmpltSn: string, data: TemplateFormData): Promise<void>;
deleteTemplate(tmpltSn: string): Promise<void>;
} }
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
@@ -13,6 +14,7 @@ import { TemplateApi } from "../api/template-service";
import { TemplateForm } from "./template-form"; import { TemplateForm } from "./template-form";
function ModifyPage({ sequential }: { sequential: string }) { function ModifyPage({ sequential }: { sequential: string }) {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { data: templateDetail } = useQuery({ const { data: templateDetail } = useQuery({
@@ -28,6 +30,13 @@ function ModifyPage({ sequential }: { sequential: string }) {
}, },
}); });
const { mutateAsync: deleteTemplate } = useMutation({
mutationFn: () => TemplateApi.deleteTemplate(sequential),
onSuccess: () => {
alert("삭제되었습니다.", router.back);
},
});
const form = useForm<TemplateFormData>({ const form = useForm<TemplateFormData>({
resolver: zodResolver(templateFormSchema), resolver: zodResolver(templateFormSchema),
values: templateDetail, values: templateDetail,
@@ -42,6 +51,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
form={form} form={form}
tmpltSn={Number(sequential)} tmpltSn={Number(sequential)}
handleSave={updateTemplate} handleSave={updateTemplate}
handleDelete={deleteTemplate}
/> />
</div> </div>
); );
@@ -13,12 +13,12 @@ import { CodeSelect } from "@/entities/code";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import { TemplateApi } from "../api/template-service"; import { TemplateApi } from "../api/template-service";
import { columns } from "../model/template-table-columns"; import { columns } from "../model/template-table-columns";
import type { Template } from "../model/template-interface"; import type { Template, TemplateParams } from "../model/template-interface";
function TemplatePage() { function TemplatePage() {
const router = useRouter(); const router = useRouter();
const [templateQuery, setTemplateQuery] = useState({ const [templateQuery, setTemplateQuery] = useState<TemplateParams>({
tmpltSeCd: "", tmpltSeCd: "",
keyword: "", keyword: "",
}); });
@@ -60,10 +60,6 @@ function TemplatePage() {
manualPagination: true, manualPagination: true,
}); });
useEffect(() => {
refetch();
}, []);
useEffect(() => { useEffect(() => {
refetch(); refetch();
}, [pagination]); }, [pagination]);
@@ -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();
+3
View File
@@ -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 };
+138
View File
@@ -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 };
+118
View File
@@ -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 };
+1
View File
@@ -28,6 +28,7 @@ export const ENDPOINT = {
IMAGES: "/cms-service/api/v1/images", IMAGES: "/cms-service/api/v1/images",
BANNERS: "/cms-service/api/v1/bnrs", BANNERS: "/cms-service/api/v1/bnrs",
TEMPLATE: "/cms-service/api/v1/tmplt", TEMPLATE: "/cms-service/api/v1/tmplt",
TERMS: "/cms-service/api/v1/trms",
}, },
BOARD_SERVICE: { BOARD_SERVICE: {
BBS: "/board-service/api/v1/bbs", BBS: "/board-service/api/v1/bbs",