324 lines
9.7 KiB
TypeScript
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 };
|