Files
frontend/apps/cs/src/entities/posting/ui/posting-dialog.tsx
T

324 lines
9.7 KiB
TypeScript

import dayjs from "dayjs";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFieldArray, useForm } from "react-hook-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Dialog,
DialogTrigger,
Button,
DialogContent,
DialogHeader,
DialogTitle,
Badge,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Checkbox,
Select,
DateRangePicker,
DialogClose,
DialogFooter,
} from "@common/components/ui";
import { cn } from "@common/components/lib";
import { CodeSelect } from "@/entities/code/@x/posting";
import { SiteApi, type Site } from "@/entities/site/@x/posting";
import type { PageableList } from "@/shared/model";
import { ENDPOINT } from "@/shared/config";
import { PostingApi } from "../api/posting-service";
import {
postingFormSchema,
type PostingFormData,
type PostingType,
type WithModePost,
} from "../model/posting-interface";
import { useAlertStore } from "@/shared/lib/use-alert-store";
type DateRange = {
from: string;
to?: string;
};
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 { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const [rememberPeriodCode, setRememberPeriodCode] = useState<
Record<string, string>
>({});
const [rememberPeriodDate, setRememberPeriodDate] = useState<
Record<string, DateRange | undefined>
>({});
const { data: siteList } = useQuery<PageableList<Site>>({
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
queryFn: () => SiteApi.getSiteList({}),
});
const { mutateAsync: requestPosting } = useMutation({
mutationFn: (data: WithModePost[]) => PostingApi.requestPosting(data),
onSuccess: () => {
alert("저장되었습니다.");
},
});
const form = useForm<PostingFormData>({
resolver: zodResolver(postingFormSchema),
});
const { fields, append, replace } = useFieldArray({
control: form.control,
name: "pstg",
});
const isFormValid =
fields.length && fields.every((field) => field.pstgPrdSeCd !== "");
const isAllSelected =
siteList && selectedRows.length === siteList.content.length;
const postingPeriod = (siteId: string) => {
const periodCode = Number(rememberPeriodCode[siteId]);
if (periodCode === 99) return "상시";
return `${dayjs().format("YYYY-MM-DD")} ~ ${dayjs()
.add(periodCode, "day")
.format("YYYY-MM-DD")}`;
};
const handleOpenChange = (open: boolean) => {
if (open) return true;
replace([]);
setSelectedRows([]);
setRememberPeriodCode({});
setRememberPeriodDate({});
};
const handleAppendFields = (i: number) => {
const siteId = siteList?.content[i]?.siteId ?? "";
const newFields = postingTargets.map((target) => ({
siteId,
pstgTypeCd: postingType,
pstgTrgtNo: target.pstgTrgtNo,
pstgBgngDt: rememberPeriodDate[siteId]?.from ?? "",
pstgEndDt: rememberPeriodDate[siteId]?.to ?? "",
pstgPrdSeCd: rememberPeriodCode[siteId] ?? "",
delYn: "N",
mode: "C",
}));
append(newFields);
};
const handleUpdateFields =
(siteId: string) => (obj: Record<string, unknown>) => {
const updatedFields = fields.map((field) =>
"siteId" in field && field.siteId === siteId
? { ...field, ...obj }
: field,
);
replace(updatedFields);
};
const handleRemoveFields = (siteId: string) => {
const filteredFields = fields.filter(
(field) => "siteId" in field && field.siteId !== siteId,
);
replace(filteredFields);
};
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 handleSelectAll = () => {
if (selectedRows.length === (siteList?.content.length ?? 0)) {
replace([]);
setSelectedRows([]);
} else {
replace([]);
setSelectedRows(siteList?.content.map((_, i) => i) ?? []);
siteList?.content.forEach((_, i) => handleAppendFields(i));
}
};
const handlePeriodCodeChange = (siteId: string) => (value: string) => {
const from = dayjs().format("YYYY-MM-DD 00:00:00");
const to = dayjs().add(Number(value), "day").format("YYYY-MM-DD 23:59:59");
setRememberPeriodCode((prev) => ({
...prev,
[siteId]: value,
}));
setRememberPeriodDate((prev) => ({
...prev,
[siteId]: { from, to },
}));
handleUpdateFields(siteId)({
pstgPrdSeCd: value,
pstgBgngDt: from,
pstgEndDt: to,
});
};
const handlePeriodDateChange =
(siteId: string) => (value: DateRange | undefined) => {
const from = dayjs(value?.from).format("YYYY-MM-DD 00:00:00");
const to = dayjs(value?.to).format("YYYY-MM-DD 23:59:59");
setRememberPeriodDate((prev) => ({
...prev,
[siteId]: { from, to },
}));
handleUpdateFields(siteId)({
pstgBgngDt: from,
pstgEndDt: to,
});
};
const handleSave = async () => {
if (await confirm("저장하시겠습니까?")) {
const data = form.getValues("pstg");
await requestPosting(data);
}
};
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-[30rem] 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">
<div className="flex items-center gap-4">
<PostingPeriodSelect
onValueChange={handlePeriodCodeChange(site.siteId)}
disabled={!selectedRows.includes(i)}
/>
{!!Number(rememberPeriodCode[site.siteId]) && (
<span
className={cn(
"text-lg text-label",
!selectedRows.includes(i) && "text-muted",
)}
>
게시 기간: <u>{postingPeriod(site.siteId)}</u>
</span>
)}
{Number(rememberPeriodCode[site.siteId]) === 0 && (
<DateRangePicker
handleDateChange={(date) =>
handlePeriodDateChange(site.siteId)(
date as DateRange | undefined,
)
}
disabled={!selectedRows.includes(i)}
/>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<DialogFooter className="!justify-center">
<Button size="lg" onClick={handleSave} disabled={!isFormValid}>
저장
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function PostingPeriodSelect({
...props
}: React.ComponentPropsWithoutRef<typeof Select>) {
return <CodeSelect upCd="PSTGPRDCD" {...props} />;
}
export { PostingDialog };