From 9a2bdfc0ca4c03eacb55bdaa13aae6b37d152959 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 7 Apr 2025 17:05:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=B0=EB=84=88=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs/app/(with-base-layout)/banner/page.tsx | 2 + .../banner/register/[serial]/page.tsx | 10 + .../banner/register/page.tsx | 2 + .../cs/src/pages/banner/api/banner-service.ts | 51 +++ apps/cs/src/pages/banner/index.ts | 3 + apps/cs/src/pages/banner/lib/utils.ts | 61 ++++ .../pages/banner/model/banner-interface.ts | 56 +++ .../banner/model/banner-table-columns.tsx | 55 +++ .../banner/model/posting-table-columns.tsx | 44 +++ apps/cs/src/pages/banner/ui/banner-form.tsx | 337 ++++++++++++++++++ apps/cs/src/pages/banner/ui/banner-page.tsx | 136 +++++++ apps/cs/src/pages/banner/ui/create-page.tsx | 46 +++ apps/cs/src/pages/banner/ui/modify-page.tsx | 140 ++++++++ .../pages/image/model/image-table-columns.tsx | 1 - apps/cs/src/pages/image/ui/image-page.tsx | 7 +- apps/cs/src/shared/config/api.ts | 1 + 16 files changed, 950 insertions(+), 2 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/banner/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/banner/register/[serial]/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/banner/register/page.tsx create mode 100644 apps/cs/src/pages/banner/api/banner-service.ts create mode 100644 apps/cs/src/pages/banner/index.ts create mode 100644 apps/cs/src/pages/banner/lib/utils.ts create mode 100644 apps/cs/src/pages/banner/model/banner-interface.ts create mode 100644 apps/cs/src/pages/banner/model/banner-table-columns.tsx create mode 100644 apps/cs/src/pages/banner/model/posting-table-columns.tsx create mode 100644 apps/cs/src/pages/banner/ui/banner-form.tsx create mode 100644 apps/cs/src/pages/banner/ui/banner-page.tsx create mode 100644 apps/cs/src/pages/banner/ui/create-page.tsx create mode 100644 apps/cs/src/pages/banner/ui/modify-page.tsx diff --git a/apps/cs/app/(with-base-layout)/banner/page.tsx b/apps/cs/app/(with-base-layout)/banner/page.tsx new file mode 100644 index 000000000..23f63defe --- /dev/null +++ b/apps/cs/app/(with-base-layout)/banner/page.tsx @@ -0,0 +1,2 @@ +import { BannerPage } from "@/pages/banner"; +export default BannerPage; diff --git a/apps/cs/app/(with-base-layout)/banner/register/[serial]/page.tsx b/apps/cs/app/(with-base-layout)/banner/register/[serial]/page.tsx new file mode 100644 index 000000000..b8ecbbcc8 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/banner/register/[serial]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/banner"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ serial: string }>; +}) { + const { serial } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/banner/register/page.tsx b/apps/cs/app/(with-base-layout)/banner/register/page.tsx new file mode 100644 index 000000000..46eb12c1c --- /dev/null +++ b/apps/cs/app/(with-base-layout)/banner/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/banner"; +export default CreatePage; diff --git a/apps/cs/src/pages/banner/api/banner-service.ts b/apps/cs/src/pages/banner/api/banner-service.ts new file mode 100644 index 000000000..93646c770 --- /dev/null +++ b/apps/cs/src/pages/banner/api/banner-service.ts @@ -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 { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.BANNERS}`, + { params }, + ); + return response.data; + } + + public async getBannerInfo(bnrSn: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`, + ); + return response.data; + } + + public async createBanner(data: BannerFormData): Promise { + const response = await client.post( + ENDPOINT.CMS_SERVICE.BANNERS, + data, + ); + return response.data; + } + + public async updateBanner( + bnrSn: string, + data: BannerFormData, + ): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`, + data, + ); + return response.data; + } + + public async deleteBanner(bnrSn: string): Promise { + await client.delete(`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`); + } +} + +export const BannerApi = new BannerApiService(); diff --git a/apps/cs/src/pages/banner/index.ts b/apps/cs/src/pages/banner/index.ts new file mode 100644 index 000000000..b2f0c6a55 --- /dev/null +++ b/apps/cs/src/pages/banner/index.ts @@ -0,0 +1,3 @@ +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; +export { BannerPage } from "./ui/banner-page"; diff --git a/apps/cs/src/pages/banner/lib/utils.ts b/apps/cs/src/pages/banner/lib/utils.ts new file mode 100644 index 000000000..c6c8ca101 --- /dev/null +++ b/apps/cs/src/pages/banner/lib/utils.ts @@ -0,0 +1,61 @@ +import { Upload, type HttpResponse } from "tus-js-client"; +import { getCookie, setCookie } from "@/shared/lib"; +import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config"; + +type UploadOptions = { + handleUploadStart?: (upload: Upload) => void; + handleProgress?: (percentage: string) => void; + handleSuccess?: (upload: Upload) => void; + handleResponse?: (res: HttpResponse) => void; +}; + +export const handleUpload = (files: FileList, options?: UploadOptions) => { + let metadata = {}; + + const file = files.item(0); + if (!file) return; + metadata = { + filename: file.name, + filetype: file.type, + }; + + const atflCd = getCookie("atfl-cd-key"); + if (atflCd) { + metadata = { + ...metadata, + atflCd, + }; + } + + const upload = 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) { + console.error(`Failed because: ${error}`); + }, + onProgress(bytesUploaded, bytesTotal) { + const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); + options?.handleProgress?.(percentage); + }, + onAfterResponse(req, res) { + const fileInfo = res.getHeader("X-File-Info"); + if (fileInfo === undefined || fileInfo === null) return; + + setCookie("atfl-cd-key", JSON.parse(fileInfo)?.atflCd ?? ""); + options?.handleResponse?.(res); + }, + onSuccess() { + options?.handleSuccess?.(upload); + }, + }); + + upload.start(); + options?.handleUploadStart?.(upload); + + console.log("upload start >>>>>", upload); +}; diff --git a/apps/cs/src/pages/banner/model/banner-interface.ts b/apps/cs/src/pages/banner/model/banner-interface.ts new file mode 100644 index 000000000..839830dad --- /dev/null +++ b/apps/cs/src/pages/banner/model/banner-interface.ts @@ -0,0 +1,56 @@ +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; + mdfcnDt: string; + mdfr: string; + mdfrNm: string; +} + +export interface BannerInfo extends Omit { + 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; +export type BannerList = PageableList; + +export interface BannerService { + getBannerList: (params: BannerParams) => Promise; + getBannerInfo: (bnrSn: string) => Promise; + createBanner: (data: BannerFormData) => Promise; + updateBanner: (bnrSn: string, data: BannerFormData) => Promise; + deleteBanner: (bnrSn: string) => Promise; +} diff --git a/apps/cs/src/pages/banner/model/banner-table-columns.tsx b/apps/cs/src/pages/banner/model/banner-table-columns.tsx new file mode 100644 index 000000000..69833ab00 --- /dev/null +++ b/apps/cs/src/pages/banner/model/banner-table-columns.tsx @@ -0,0 +1,55 @@ +"use client"; + +import dayjs from "dayjs"; +import { type ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@common/components/ui"; +import { type Banner } from "./banner-interface"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + // TODO: 배너 이미지 셀 추가 + { + 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") : ""; + }, + }, +]; diff --git a/apps/cs/src/pages/banner/model/posting-table-columns.tsx b/apps/cs/src/pages/banner/model/posting-table-columns.tsx new file mode 100644 index 000000000..a43cf8693 --- /dev/null +++ b/apps/cs/src/pages/banner/model/posting-table-columns.tsx @@ -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[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + 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")})`; + }, + }, +]; diff --git a/apps/cs/src/pages/banner/ui/banner-form.tsx b/apps/cs/src/pages/banner/ui/banner-form.tsx new file mode 100644 index 000000000..6ebf9cffd --- /dev/null +++ b/apps/cs/src/pages/banner/ui/banner-form.tsx @@ -0,0 +1,337 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +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 { + Button, + FileUploader, + Input, + Label, + Progress, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Textarea, +} from "@common/components"; +import { CodeSelect } from "@/entities/code"; +import { AttachmentApi } from "@/entities/attachment"; +import { ENDPOINT } from "@/shared/config"; +import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; +import type { BannerFormData } from "../model/banner-interface"; +import { handleUpload } from "../lib/utils"; + +type BannerFormProps = { + form: UseFormReturn; + bnrSn?: string; + handleSave: (data: BannerFormData) => Promise; + handleDelete?: () => Promise; +}; + +function BannerForm({ + form, + bnrSn, + handleSave, + handleDelete, +}: BannerFormProps) { + const isModifyMode = !!bnrSn; + const atflCd = form.watch("atflCd"); + + const router = useRouter(); + const { confirm } = useConfirm(); + const [attachmentFileId, setAttachmentFileId] = useState(""); + const [upload, setUpload] = useState(null); + const [progress, setProgress] = useState(0); + + const { data: atflId } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd], + queryFn: () => AttachmentApi.getAttachments(atflCd!), + select: (data) => data?.[data.length - 1]?.atflId, + enabled: !!atflCd, + }); + + const onSubmit = async (data: BannerFormData) => { + const confirmMessage = isModifyMode + ? "수정하시겠습니까?" + : "저장하시겠습니까?"; + + if (await confirm(confirmMessage)) { + await handleSave(data); + } + }; + + const onDelete = async () => { + if (await confirm("삭제하시겠습니까?")) { + await handleDelete?.(); + } + }; + + 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) setAttachmentFileId(atflId); + }; + + useEffect(() => { + if (atflId) setAttachmentFileId(atflId); + }, [atflId]); + + return ( + <> +
+ +
+ + + + + + + + 배너명 + + ( + + + + + + )} + /> + + + + 배너구분 + + ( + + + + + + )} + /> + + + + 배너 이미지 + + ( + + + { + const files = e.target.files; + if (!files) return; + handleUpload(files, { + handleUploadStart: setUpload, + handleProgress, + handleSuccess: setUpload, + handleResponse: handleUploadResponse, + }); + }} + customPreview={ + <> + +
+
    +
  • + 이미지 파일(jpg, jpeg, png, gif)만 + 업로드 가능합니다. +
  • +
+
+ + } + /> +
+
+ )} + /> +
+
+ + 링크 URL + + ( + + + + + + )} + /> + + + + 링크 열기 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 배너 설명 + + ( + + +