diff --git a/apps/cs/app/(with-base-layout)/popup/page.tsx b/apps/cs/app/(with-base-layout)/popup/page.tsx new file mode 100644 index 000000000..684e5bc58 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/popup/page.tsx @@ -0,0 +1,2 @@ +import { PopupPage } from "@/pages/popup"; +export default PopupPage; diff --git a/apps/cs/app/(with-base-layout)/popup/register/page.tsx b/apps/cs/app/(with-base-layout)/popup/register/page.tsx new file mode 100644 index 000000000..d330844d3 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/popup/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/popup"; +export default CreatePage; diff --git a/apps/cs/src/pages/popup/api/popup-service.ts b/apps/cs/src/pages/popup/api/popup-service.ts new file mode 100644 index 000000000..59b67ea9b --- /dev/null +++ b/apps/cs/src/pages/popup/api/popup-service.ts @@ -0,0 +1,25 @@ +import { ENDPOINT } from "@/shared/config"; +import { + popupFormSchema, + type PopupFormData, + type PopupList, + type PopupParams, + type PopupService, +} from "../model/popup-interface"; +import { client } from "@/shared/api"; + +export class PopupApiService implements PopupService { + async getPopupList(params: PopupParams): Promise { + const response = await client.get(ENDPOINT.CMS_SERVICE.POPUPS, { + params, + }); + return response.data; + } + + async createPopup(data: PopupFormData): Promise { + const validatedData = popupFormSchema.parse(data); + await client.post(ENDPOINT.CMS_SERVICE.POPUPS, validatedData); + } +} + +export const PopupApi = new PopupApiService(); diff --git a/apps/cs/src/pages/popup/index.ts b/apps/cs/src/pages/popup/index.ts new file mode 100644 index 000000000..dfc7388ff --- /dev/null +++ b/apps/cs/src/pages/popup/index.ts @@ -0,0 +1,2 @@ +export { CreatePage } from "./ui/create-page"; +export { PopupPage } from "./ui/popup-page"; diff --git a/apps/cs/src/pages/popup/model/popup-interface.ts b/apps/cs/src/pages/popup/model/popup-interface.ts new file mode 100644 index 000000000..51d78a0fb --- /dev/null +++ b/apps/cs/src/pages/popup/model/popup-interface.ts @@ -0,0 +1,49 @@ +import type { PageableList, PageableParams } from "@/shared/model"; +import { z } from "zod"; + +export interface PopupParams extends PageableParams { + keyword?: string; + popupTypeCd?: string; + contsTypeCd?: string; + useYn?: string; +} + +export interface Popup { + popupSn: number; + popupTypeCd: string; + popupTypeNm: string; + contsTypeCd: string; + contsTypeNm: string; + popupTtl: string; + popupWdth: number; + popupHgt: number; + popupEndUseYn: string; + useYn: string; + mdfcnDt: string; + mdfr: string; + mdfrNm: string; +} + +export const popupFormSchema = z.object({ + popupTypeCd: z.string(), + contsTypeCd: z.string(), + popupTtl: z.string(), + popupCn: z.string().nullish(), + popupWdth: z.number(), + popupHgt: z.number(), + popupVtclPstn: z.number(), + popupHrzntltyPstn: z.number(), + popupEndUseYn: z.string(), + popupLnkgUrl: z.string().nullish(), + atflCd: z.string().nullish(), + useYn: z.string(), + rgtr: z.string(), +}); + +export type PopupList = PageableList; +export type PopupFormData = z.infer; + +export interface PopupService { + getPopupList(params: PopupParams): Promise; + createPopup(data: PopupFormData): Promise; +} diff --git a/apps/cs/src/pages/popup/model/table-columns.tsx b/apps/cs/src/pages/popup/model/table-columns.tsx new file mode 100644 index 000000000..ad3938e2b --- /dev/null +++ b/apps/cs/src/pages/popup/model/table-columns.tsx @@ -0,0 +1,43 @@ +import dayjs from "dayjs"; +import { type ColumnDef } from "@tanstack/react-table"; +import type { Popup } from "./popup-interface"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const { pageIndex, pageSize } = table.getState().pagination; + const totalRows = table.getRowCount(); + return totalRows - (pageIndex * pageSize + row.index); + }, + }, + { + accessorKey: "popupTtl", + header: "제목", + }, + { + accessorKey: "popupTypeNm", + header: "팝업유형", + }, + { + accessorKey: "contsTypeNm", + header: "콘텐츠유형", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + accessorKey: "mdfcnDt", + header: "최종수정일", + cell: ({ row }) => { + const date = dayjs(row.original.mdfcnDt); + return date.format("YYYY-MM-DD"); + }, + }, + { + accessorKey: "mdfrNm", + header: "최종수정자", + }, +]; diff --git a/apps/cs/src/pages/popup/ui/create-page.tsx b/apps/cs/src/pages/popup/ui/create-page.tsx new file mode 100644 index 000000000..f157fb3c6 --- /dev/null +++ b/apps/cs/src/pages/popup/ui/create-page.tsx @@ -0,0 +1,49 @@ +"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 { useUserStore, useAlertStore } from "@/shared/lib"; +import { popupFormSchema, type PopupFormData } from "../model/popup-interface"; +import { PopupApi } from "../api/popup-service"; +import { PopupForm } from "./popup-form"; +import { useEffect } from "react"; + +function CreatePage() { + const router = useRouter(); + const { userInfo } = useUserStore((state) => state); + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: createPopup } = useMutation({ + mutationFn: (data: PopupFormData) => PopupApi.createPopup(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(popupFormSchema), + defaultValues: { + contsTypeCd: "39", + popupEndUseYn: "Y", + useYn: "Y", + }, + }); + + useEffect(() => { + if (!userInfo || !form) return; + form.setValue("rgtr", userInfo.userUnqId); + }, [userInfo, form]); + + return ( +
+
+

팝업 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/popup/ui/popup-form.tsx b/apps/cs/src/pages/popup/ui/popup-form.tsx new file mode 100644 index 000000000..3e0774fab --- /dev/null +++ b/apps/cs/src/pages/popup/ui/popup-form.tsx @@ -0,0 +1,333 @@ +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 { PopupFormData } from "../model/popup-interface"; +import { GeneralFileUploader } from "@/widgets/post/ui/general-file-uploader"; +import { useState } from "react"; + +const Editor = dynamic( + () => import("@/entities/editor").then((module) => module.Editor), + { ssr: false }, +); + +type PopupFormProps = { + form: UseFormReturn; + popupSn?: string; + handleSave: (data: PopupFormData) => Promise; + handleDelete?: () => Promise; +}; + +function PopupForm({ form, popupSn, ...props }: PopupFormProps) { + const [deleteAttachmentIds, setDeleteAttachmentIds] = useState([]); + + return ( + +
+ + + + + + + + 제목 + + ( + + + + + + )} + /> + + + + 팝업 크기 + +
+ ( + + + { + const value = e.target.value; + if (isNaN(parseInt(value))) return; + field.onChange(parseInt(value)); + }} + /> + + + )} + /> + × + ( + + + { + const value = e.target.value; + if (isNaN(parseInt(value))) return; + field.onChange(parseInt(value)); + }} + /> + + + )} + /> +
+
+
+ + 팝업 위치 + +
+ ( + + + { + const value = e.target.value; + if (isNaN(parseInt(value))) return; + field.onChange(parseInt(value)); + }} + /> + + + )} + /> + / + ( + + + { + const value = e.target.value; + if (isNaN(parseInt(value))) return; + field.onChange(parseInt(value)); + }} + /> + + + )} + /> +
+
+
+ + 팝업 유형 + + ( + + + + + + )} + /> + + + + 콘텐츠 유형 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 링크 주소 + + ( + + + + + + )} + /> + + + + 팝업 내용 + + ( + + + + + + )} + /> + + + + 첨부 파일 + + + + + + 팝업 그만 보기 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ ); +} + +export { PopupForm }; diff --git a/apps/cs/src/pages/popup/ui/popup-page.tsx b/apps/cs/src/pages/popup/ui/popup-page.tsx new file mode 100644 index 000000000..ced5bd80c --- /dev/null +++ b/apps/cs/src/pages/popup/ui/popup-page.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +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 { ENDPOINT } from "@/shared/config"; +import { PopupApi } from "../api/popup-service"; +import { columns } from "../model/table-columns"; +import type { Popup, PopupParams } from "../model/popup-interface"; + +function PopupPage() { + const router = useRouter(); + + const [popupParams, setPopupParams] = useState({ + keyword: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: popupList, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.POPUPS], + queryFn: () => + PopupApi.getPopupList({ + ...popupParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + const handleRowClick = (row: Popup) => { + router.push(`/popup/register/${row.popupSn}`); + }; + + const table = useReactTable({ + data: popupList?.content ?? [], + columns, + rowCount: popupList?.totalElements ?? 0, + state: { + pagination, + }, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }); + + return ( +
+
+
+ + + setPopupParams({ ...popupParams, keyword: e.target.value }) + } + /> + +
+
+
+
+

팝업 목록

+ +
+
+ +
+
+
+ ); +} + +export { PopupPage }; diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 7aef8df12..b8f822ab7 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -37,7 +37,8 @@ export const ENDPOINT = { TEMPLATE: "/cms-service/api/v1/tmplt", TERMS: "/cms-service/api/v1/trms", LWPO_CODE: "/cms-service/api/v1/lwpo-cds", - UP_CODE: "/cms-service/api/v1/up-codes", + UP_CODE: "/cms-service/api/v1/up-codes", + POPUPS: "/cms-service/api/v1/popups", }, BOARD_SERVICE: { BBS: "/board-service/api/v1/bbs", diff --git a/apps/cs/src/widgets/post/ui/general-file-uploader.tsx b/apps/cs/src/widgets/post/ui/general-file-uploader.tsx index 99088aa3e..e6806cae8 100644 --- a/apps/cs/src/widgets/post/ui/general-file-uploader.tsx +++ b/apps/cs/src/widgets/post/ui/general-file-uploader.tsx @@ -4,10 +4,9 @@ import type { HttpResponse, Upload } from "tus-js-client"; import { Button, FileUploader } from "@common/components"; import { handleTusUpload } from "@/entities/tus"; import { type Attachment } from "@/entities/attachment"; -import type { PostFormData, PostUpdateFormData } from "@/entities/post"; import { setCookie, useAlertStore, useMenuStore } from "@/shared/lib"; -function GeneralFileUploader({ +function GeneralFileUploader({ form, attachmentData, deleteAttachmentIds,