From e659f47d1de64e85918cfba203ff8e9f6618cfb8 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 3 Apr 2025 17:37:35 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=ED=8F=BC=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../posting}/api/posting-service.ts | 4 +- apps/cs/src/entities/posting/index.ts | 7 + .../posting/model/posting-interface.ts | 43 ++++ .../entities/posting/ui/posting-dialog.tsx | 197 ++++++++++++++++++ apps/cs/src/entities/site/api/site-service.ts | 2 +- .../src/pages/image/model/image-interface.ts | 4 +- .../pages/image/model/posting-interface.ts | 27 --- .../image/model/posting-table-columns.tsx | 4 +- apps/cs/src/pages/image/ui/image-form.tsx | 2 + apps/cs/src/pages/image/ui/image-page.tsx | 45 +++- apps/cs/src/pages/image/ui/modify-page.tsx | 11 +- apps/cs/src/pages/menu/ui/menu-form.tsx | 3 + apps/cs/src/pages/menu/ui/menu-page.tsx | 2 +- apps/cs/src/pages/site/ui/site-form.tsx | 2 + .../common/components/src/ui/layout/vbase.tsx | 2 +- 15 files changed, 304 insertions(+), 51 deletions(-) rename apps/cs/src/{pages/image => entities/posting}/api/posting-service.ts (88%) create mode 100644 apps/cs/src/entities/posting/index.ts create mode 100644 apps/cs/src/entities/posting/model/posting-interface.ts create mode 100644 apps/cs/src/entities/posting/ui/posting-dialog.tsx delete mode 100644 apps/cs/src/pages/image/model/posting-interface.ts diff --git a/apps/cs/src/pages/image/api/posting-service.ts b/apps/cs/src/entities/posting/api/posting-service.ts similarity index 88% rename from apps/cs/src/pages/image/api/posting-service.ts rename to apps/cs/src/entities/posting/api/posting-service.ts index a50afe5d3..b14110653 100644 --- a/apps/cs/src/pages/image/api/posting-service.ts +++ b/apps/cs/src/entities/posting/api/posting-service.ts @@ -5,7 +5,7 @@ import type { PostService, PostingParams, Post, - WithModePostData, + WithModePost, } from "../model/posting-interface"; export class PostingApiService implements PostService { @@ -19,7 +19,7 @@ export class PostingApiService implements PostService { return response.data; } - public async requestPosting(data: WithModePostData[]): Promise { + public async requestPosting(data: WithModePost[]): Promise { const response = await client.post( `${ENDPOINT.CMS_SERVICE.SITE_PSTGS}`, data, diff --git a/apps/cs/src/entities/posting/index.ts b/apps/cs/src/entities/posting/index.ts new file mode 100644 index 000000000..85416783b --- /dev/null +++ b/apps/cs/src/entities/posting/index.ts @@ -0,0 +1,7 @@ +export { PostingApi } from "./api/posting-service"; +export { + POSTING_TYPE, + type Post, + type WithModePost, +} from "./model/posting-interface"; +export { PostingDialog } from "./ui/posting-dialog"; diff --git a/apps/cs/src/entities/posting/model/posting-interface.ts b/apps/cs/src/entities/posting/model/posting-interface.ts new file mode 100644 index 000000000..3adaf3404 --- /dev/null +++ b/apps/cs/src/entities/posting/model/posting-interface.ts @@ -0,0 +1,43 @@ +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(), + pstgEndDt: z.string(), + pstgPrdSeCd: z.string(), + pstgPrdSeNm: z.string(), + delYn: z.string(), +}); + +export const withModePostSchema = postSchema + .extend({ + mode: z.string(), + }) + .omit({ + siteNm: true, + }); + +export type Post = z.infer; +export type WithModePost = z.infer; + +export interface PostService { + getPostingHistory: (params: PostingParams) => Promise>; + requestPosting: (data: WithModePost[]) => Promise; +} diff --git a/apps/cs/src/entities/posting/ui/posting-dialog.tsx b/apps/cs/src/entities/posting/ui/posting-dialog.tsx new file mode 100644 index 000000000..c28448b50 --- /dev/null +++ b/apps/cs/src/entities/posting/ui/posting-dialog.tsx @@ -0,0 +1,197 @@ +import { useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { useQuery } from "@tanstack/react-query"; +import { + Dialog, + DialogTrigger, + Button, + DialogContent, + DialogHeader, + DialogTitle, + Badge, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Checkbox, + Select, +} from "@common/components/ui"; +import { CodeSelect } from "@/entities/code"; +import { SiteApi, type Site } from "@/entities/site"; +import type { PageableList } from "@/shared/model"; +import { ENDPOINT } from "@/shared/config"; +import { type PostingType } from "../model/posting-interface"; + +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 [selectedRows, setSelectedRows] = useState([]); + + const { data: siteList } = useQuery>({ + queryKey: [ENDPOINT.CMS_SERVICE.SITES], + queryFn: () => SiteApi.getSiteList({}), + }); + + const form = useForm(); + const { fields, append, replace } = useFieldArray({ + control: form.control, + name: "pstg", + }); + + const handleSelectAll = () => { + if (selectedRows.length === (siteList?.content.length ?? 0)) { + initialize(); + } else { + setSelectedRows(siteList?.content.map((_, i) => i) ?? []); + handleAllSelectRow(); + } + }; + + 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 handleAllSelectRow = () => { + replace([]); + siteList?.content.forEach((_, i) => { + handleAppendFields(i); + }); + }; + + const handleAppendFields = (i: number) => { + for (const target of postingTargets) { + append({ + siteId: siteList?.content[i]?.siteId ?? "", + pstgTypeCd: postingType, + pstgTrgtNo: target.pstgTrgtNo, + pstgBgngDt: "", + pstgEndDt: "", + pstgPrdSeCd: "", + delYn: "N", + mode: "C", + }); + } + }; + + const handleRemoveFields = (siteId: string) => { + const filteredFields = fields.filter( + (field) => "siteId" in field && field.siteId !== siteId, + ); + replace(filteredFields); + }; + + const handleOpenChange = (open: boolean) => { + if (open) return true; + initialize(); + }; + + const initialize = () => { + setSelectedRows([]); + replace([]); + }; + + const isAllSelected = + siteList && selectedRows.length === siteList.content.length; + + return ( + + + + + + + 사이트 반영 + +
+

+ 선택된 {POSTING_TYPE_LABEL[postingType]} +

+
+ {postingTargets.map((target) => ( + + {target.pstgTrgtNm} + + ))} +
+
+
+ + + + + + + + + + + + 사이트명 + 게시 구분 + + + + {siteList?.content.map((site, i) => ( + + + handleSelectRow(i)} + /> + + {site.siteNm} + + + + + ))} + +
+
+
+
+ ); +} + +function PostingPeriodSelect({ + ...props +}: React.ComponentPropsWithoutRef) { + return ; +} + +export { PostingDialog }; diff --git a/apps/cs/src/entities/site/api/site-service.ts b/apps/cs/src/entities/site/api/site-service.ts index 37fd7e49a..68c520093 100644 --- a/apps/cs/src/entities/site/api/site-service.ts +++ b/apps/cs/src/entities/site/api/site-service.ts @@ -10,7 +10,7 @@ import { } from "../model/site-interface"; export class SiteApiService implements SiteService { - public async getSiteList(params: SiteParams): Promise { + public async getSiteList(params?: SiteParams): Promise { const response = await client.get( `${ENDPOINT.CMS_SERVICE.SITES}`, { params }, diff --git a/apps/cs/src/pages/image/model/image-interface.ts b/apps/cs/src/pages/image/model/image-interface.ts index 7748589c7..32065968d 100644 --- a/apps/cs/src/pages/image/model/image-interface.ts +++ b/apps/cs/src/pages/image/model/image-interface.ts @@ -11,8 +11,8 @@ 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 { diff --git a/apps/cs/src/pages/image/model/posting-interface.ts b/apps/cs/src/pages/image/model/posting-interface.ts deleted file mode 100644 index 0708bb29f..000000000 --- a/apps/cs/src/pages/image/model/posting-interface.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { PageableList, PageableParams } from "@/shared/model"; - -export interface PostingParams extends PageableParams { - pstgTrgtNo: number; -} - -export interface Post { - siteId: string; - siteNm: string; - pstgTypeCd: string; - pstgTypeNm: string; - pstgTrgtNo: number; - pstgBgngDt: string; - pstgEndDt: string; - pstgPrdSeCd: string; - pstgPrdSeNm: string; - delYn: string; -} - -export interface WithModePostData extends Post { - mode: string; -} - -export interface PostService { - getPostingHistory: (params: PostingParams) => Promise>; - requestPosting: (data: WithModePostData[]) => Promise; -} diff --git a/apps/cs/src/pages/image/model/posting-table-columns.tsx b/apps/cs/src/pages/image/model/posting-table-columns.tsx index c3a0830a1..a43cf8693 100644 --- a/apps/cs/src/pages/image/model/posting-table-columns.tsx +++ b/apps/cs/src/pages/image/model/posting-table-columns.tsx @@ -1,9 +1,9 @@ "use client"; import dayjs from "dayjs"; -import { type ColumnDef } from "@tanstack/react-table"; +import type { ColumnDef } from "@tanstack/react-table"; import { Checkbox } from "@common/components/ui"; -import { type Post } from "./posting-interface"; +import type { Post } from "@/entities/posting"; export const columns: ColumnDef[] = [ { diff --git a/apps/cs/src/pages/image/ui/image-form.tsx b/apps/cs/src/pages/image/ui/image-form.tsx index 9ed24c218..e9fcdc9a6 100644 --- a/apps/cs/src/pages/image/ui/image-form.tsx +++ b/apps/cs/src/pages/image/ui/image-form.tsx @@ -55,6 +55,7 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) { await handleConfirm(async () => { if (await confirm(confirmMessage)) { + setOnConfirm(false); await handleSave(data); } }); @@ -63,6 +64,7 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) { const onDelete = async () => { await handleConfirm(async () => { if (await confirm("삭제하시겠습니까?")) { + setOnConfirm(false); await handleDelete?.(); } }); diff --git a/apps/cs/src/pages/image/ui/image-page.tsx b/apps/cs/src/pages/image/ui/image-page.tsx index 810cb0b5c..025639cd5 100644 --- a/apps/cs/src/pages/image/ui/image-page.tsx +++ b/apps/cs/src/pages/image/ui/image-page.tsx @@ -9,22 +9,23 @@ import { 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/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({}); - const [imageQuery, setImageQuery] = useState({ - keyword: "", - imgTypeCd: "", - useYn: "", - page: 0, - size: 100, - }); + const [postingTargets, setPostingTargets] = useState([]); + const [imageQuery, setImageQuery] = useState({ keyword: "" }); const { data: imageList, refetch } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.IMAGES], @@ -52,6 +53,22 @@ 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(); }, []); @@ -77,8 +94,18 @@ function ImagePage() {
-

이미지 목록

- +
+

이미지 목록

+
+ + +
+
diff --git a/apps/cs/src/pages/image/ui/modify-page.tsx b/apps/cs/src/pages/image/ui/modify-page.tsx index 4ab21de5a..6704fffee 100644 --- a/apps/cs/src/pages/image/ui/modify-page.tsx +++ b/apps/cs/src/pages/image/ui/modify-page.tsx @@ -12,14 +12,13 @@ import { } from "@tanstack/react-table"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button, DataTable } from "@common/components/ui"; -import { ENDPOINT } from "@/shared/config"; +import { PostingApi, type WithModePost } 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 type { WithModePostData } from "../model/posting-interface"; import { columns } from "../model/posting-table-columns"; import { ImageApi } from "../api/image-service"; -import { PostingApi } from "../api/posting-service"; import { ImageForm } from "./image-form"; function ModifyPage({ serial }: { serial: string }) { @@ -56,7 +55,7 @@ function ModifyPage({ serial }: { serial: string }) { }); const { mutateAsync: requestPosting } = useMutation({ - mutationFn: (data: WithModePostData[]) => PostingApi.requestPosting(data), + mutationFn: (data: WithModePost[]) => PostingApi.requestPosting(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.SITE_PSTGS, serial], @@ -81,7 +80,7 @@ function ModifyPage({ serial }: { serial: string }) { }); const handleDeletePostingHistory = async () => { - const data = table.getFilteredSelectedRowModel().flatRows.map((row) => ({ + const data = table.getFilteredSelectedRowModel().rows.map((row) => ({ ...row.original, mode: "D", })); @@ -120,7 +119,7 @@ function ModifyPage({ serial }: { serial: string }) {