feat: 사이트 반영 폼 작업
This commit is contained in:
+2
-2
@@ -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<Post[]> {
|
||||
public async requestPosting(data: WithModePost[]): Promise<Post[]> {
|
||||
const response = await client.post<Post[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITE_PSTGS}`,
|
||||
data,
|
||||
@@ -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";
|
||||
@@ -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<typeof postSchema>;
|
||||
export type WithModePost = z.infer<typeof withModePostSchema>;
|
||||
|
||||
export interface PostService {
|
||||
getPostingHistory: (params: PostingParams) => Promise<PageableList<Post>>;
|
||||
requestPosting: (data: WithModePost[]) => Promise<Post[]>;
|
||||
}
|
||||
@@ -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<number[]>([]);
|
||||
|
||||
const { data: siteList } = useQuery<PageableList<Site>>({
|
||||
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 (
|
||||
<Dialog onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="emerald" disabled={!postingTargets.length}>
|
||||
<i className="diveicon di-layer" />
|
||||
사이트반영
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[68rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>사이트 반영</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="card bg-accent !p-4">
|
||||
<h3 className="font-semibold">
|
||||
선택된 {POSTING_TYPE_LABEL[postingType]}
|
||||
</h3>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{postingTargets.map((target) => (
|
||||
<Badge key={target.pstgTrgtNo}>
|
||||
<span className="text-xl">{target.pstgTrgtNm}</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="m-2 max-h-[28rem] overflow-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="25%" />
|
||||
<col width="65%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky top-0 z-10 bg-accent">
|
||||
<TableRow>
|
||||
<TableHead className="!px-0">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>사이트명</TableHead>
|
||||
<TableHead>게시 구분</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{siteList?.content.map((site, i) => (
|
||||
<TableRow key={site.siteId}>
|
||||
<TableCell align="center" className="!px-0">
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(i)}
|
||||
onCheckedChange={() => handleSelectRow(i)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">{site.siteNm}</TableCell>
|
||||
<TableCell align="left">
|
||||
<PostingPeriodSelect disabled={!selectedRows.includes(i)} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function PostingPeriodSelect({
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof Select>) {
|
||||
return <CodeSelect upCd="PSTGPRDCD" {...props} />;
|
||||
}
|
||||
|
||||
export { PostingDialog };
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "../model/site-interface";
|
||||
|
||||
export class SiteApiService implements SiteService {
|
||||
public async getSiteList(params: SiteParams): Promise<SiteList> {
|
||||
public async getSiteList(params?: SiteParams): Promise<SiteList> {
|
||||
const response = await client.get<SiteList>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}`,
|
||||
{ params },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<PageableList<Post>>;
|
||||
requestPosting: (data: WithModePostData[]) => Promise<Post[]>;
|
||||
}
|
||||
@@ -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<Post>[] = [
|
||||
{
|
||||
|
||||
@@ -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?.();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<RowSelectionState>({});
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({
|
||||
keyword: "",
|
||||
imgTypeCd: "",
|
||||
useYn: "",
|
||||
page: 0,
|
||||
size: 100,
|
||||
});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({ 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() {
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 목록</h3>
|
||||
<Button onClick={() => router.push("/image/register")}>등록</Button>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.IMAGE}
|
||||
postingTargets={postingTargets}
|
||||
/>
|
||||
<Button onClick={() => router.push("/image/register")}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
|
||||
@@ -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 }) {
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
disabled={!table.getFilteredSelectedRowModel().flatRows.length}
|
||||
disabled={!table.getFilteredSelectedRowModel().rows.length}
|
||||
onClick={handleDeletePostingHistory}
|
||||
>
|
||||
이력삭제
|
||||
|
||||
@@ -52,6 +52,7 @@ function MenuForm({
|
||||
const onCreate = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleCreate(data);
|
||||
}
|
||||
});
|
||||
@@ -60,6 +61,7 @@ function MenuForm({
|
||||
const onUpdate = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("수정하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleUpdate({ menuCd: menuCd!, data });
|
||||
}
|
||||
});
|
||||
@@ -68,6 +70,7 @@ function MenuForm({
|
||||
const onDelete = () => {
|
||||
handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleDelete?.(menuCd!);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -58,6 +58,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
setOnConfirm(false);
|
||||
handleSave(data);
|
||||
}
|
||||
});
|
||||
@@ -66,6 +67,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleDelete?.();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export function VBaseLayout({
|
||||
queryClient?: QueryClient;
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider client={queryClient}>
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header viewOnly />
|
||||
|
||||
Reference in New Issue
Block a user