261 lines
10 KiB
TypeScript
261 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
import { useAlertStore } from "@/shared/lib";
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
Input,
|
|
InputDatePicker,
|
|
Label,
|
|
} from "@common/components";
|
|
|
|
export interface MainFacilityMappingSaveDialogValues {
|
|
prxyBzentyNmCn: string;
|
|
/** 위탁비용(백만원/년) */
|
|
cnsgnCstVl: number;
|
|
wholNope: number;
|
|
/** YYYYMMDD~YYYYMMDD */
|
|
prxyCtrtCn: string;
|
|
}
|
|
|
|
export interface MainFacilityMappingSaveDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
initialPrxyBzentyNmCn: string;
|
|
/** 위탁비용(백만원/년) 초기값 */
|
|
initialCnsgnCstVl: number;
|
|
initialWholNope: number;
|
|
/** 위탁계약기간 초기값 (YYYYMMDD~YYYYMMDD) */
|
|
initialPrxyCtrtCn: string;
|
|
onConfirm?: (values: MainFacilityMappingSaveDialogValues) => void | Promise<void>;
|
|
}
|
|
|
|
function formatDateToYmd(d: Date | undefined): string | null {
|
|
if (!d || Number.isNaN(d.getTime())) return null;
|
|
const y = d.getFullYear();
|
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
const day = String(d.getDate()).padStart(2, "0");
|
|
return `${y}${m}${day}`;
|
|
}
|
|
|
|
/** `YYYYMMDD` → `Date` (로컬 자정) */
|
|
function parseYmdToDate(ymd: string): Date | undefined {
|
|
const s = ymd.replace(/\D/g, "");
|
|
if (s.length !== 8) return undefined;
|
|
const y = Number(s.slice(0, 4));
|
|
const mo = Number(s.slice(4, 6)) - 1;
|
|
const day = Number(s.slice(6, 8));
|
|
const d = new Date(y, mo, day);
|
|
if (d.getFullYear() !== y || d.getMonth() !== mo || d.getDate() !== day) return undefined;
|
|
return d;
|
|
}
|
|
|
|
function parseInitialPrxyCtrtCnToDates(initial: string): { start: Date | undefined; end: Date | undefined } {
|
|
const t = initial.trim();
|
|
if (!/^\d{8}~\d{8}$/.test(t)) {
|
|
return { start: undefined, end: undefined };
|
|
}
|
|
const start = parseYmdToDate(t.slice(0, 8));
|
|
const end = parseYmdToDate(t.slice(9, 17));
|
|
return { start, end };
|
|
}
|
|
|
|
function validateContractRange(start: Date | undefined, end: Date | undefined): string | null {
|
|
const startYmd = formatDateToYmd(start);
|
|
const endYmd = formatDateToYmd(end);
|
|
if (!startYmd || !endYmd) {
|
|
return "시작일과 종료일을 선택해 주세요.";
|
|
}
|
|
if (startYmd > endYmd) {
|
|
return "시작일이 종료일보다 늦을 수 없습니다.";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function MainFacilityMappingSaveDialog({
|
|
open,
|
|
onOpenChange,
|
|
initialPrxyBzentyNmCn,
|
|
initialCnsgnCstVl,
|
|
initialWholNope,
|
|
initialPrxyCtrtCn,
|
|
onConfirm,
|
|
}: MainFacilityMappingSaveDialogProps) {
|
|
const { setMessage: alert } = useAlertStore((state) => state);
|
|
const [prxyBzentyNmCn, setPrxyBzentyNmCn] = useState("");
|
|
const [cnsgnCstInput, setCnsgnCstInput] = useState("");
|
|
const [wholNopeInput, setWholNopeInput] = useState("");
|
|
const [prxyCtrtStart, setPrxyCtrtStart] = useState<Date | undefined>(undefined);
|
|
const [prxyCtrtEnd, setPrxyCtrtEnd] = useState<Date | undefined>(undefined);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setPrxyBzentyNmCn(initialPrxyBzentyNmCn ?? "");
|
|
const initCost = Number(initialCnsgnCstVl);
|
|
setCnsgnCstInput(
|
|
initialCnsgnCstVl === null ||
|
|
initialCnsgnCstVl === undefined ||
|
|
Number.isNaN(initCost)
|
|
? ""
|
|
: String(initCost),
|
|
);
|
|
setWholNopeInput(
|
|
initialWholNope === null || initialWholNope === undefined || Number.isNaN(Number(initialWholNope))
|
|
? ""
|
|
: String(initialWholNope),
|
|
);
|
|
const { start, end } = parseInitialPrxyCtrtCnToDates(initialPrxyCtrtCn ?? "");
|
|
setPrxyCtrtStart(start);
|
|
setPrxyCtrtEnd(end);
|
|
}
|
|
}, [open, initialPrxyBzentyNmCn, initialCnsgnCstVl, initialWholNope, initialPrxyCtrtCn]);
|
|
|
|
const handleDialogOpenChange = (nextOpen: boolean) => {
|
|
if (!nextOpen && !isSubmitting) {
|
|
setPrxyBzentyNmCn("");
|
|
setCnsgnCstInput("");
|
|
setWholNopeInput("");
|
|
setPrxyCtrtStart(undefined);
|
|
setPrxyCtrtEnd(undefined);
|
|
}
|
|
onOpenChange(nextOpen);
|
|
};
|
|
|
|
const handleConfirm = async () => {
|
|
const trimmedCost = cnsgnCstInput.trim();
|
|
const cnsgnCstVl = trimmedCost === "" ? 0 : Number.parseFloat(trimmedCost.replace(/,/g, ""));
|
|
if (!Number.isFinite(cnsgnCstVl) || cnsgnCstVl < 0) {
|
|
alert("위탁비용은 0 이상의 숫자로 입력해 주세요.");
|
|
return;
|
|
}
|
|
const trimmedWhol = wholNopeInput.trim();
|
|
const wholNope = trimmedWhol === "" ? 0 : Number.parseInt(trimmedWhol, 10);
|
|
if (!Number.isFinite(wholNope) || wholNope < 0) {
|
|
alert("전체 인원은 0 이상의 정수로 입력해 주세요.");
|
|
return;
|
|
}
|
|
const prxyCtrtErr = validateContractRange(prxyCtrtStart, prxyCtrtEnd);
|
|
if (prxyCtrtErr) {
|
|
alert(prxyCtrtErr);
|
|
return;
|
|
}
|
|
const startYmd = formatDateToYmd(prxyCtrtStart)!;
|
|
const endYmd = formatDateToYmd(prxyCtrtEnd)!;
|
|
const prxyCtrtCnParam = `${startYmd}~${endYmd}`;
|
|
try {
|
|
setIsSubmitting(true);
|
|
await onConfirm?.({
|
|
prxyBzentyNmCn: prxyBzentyNmCn.trim(),
|
|
cnsgnCstVl,
|
|
wholNope,
|
|
prxyCtrtCn: prxyCtrtCnParam,
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
|
<DialogContent className="max-w-[48rem]" aria-describedby={undefined}>
|
|
<DialogHeader>
|
|
<DialogTitle>저장 정보 입력</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="flex flex-col gap-4 py-2">
|
|
<div className="flex flex-col gap-2">
|
|
<Label className="text-label">위탁업체명</Label>
|
|
<Input
|
|
value={prxyBzentyNmCn}
|
|
onChange={(e) => setPrxyBzentyNmCn(e.target.value)}
|
|
className="!text-[1.3rem]"
|
|
placeholder="위탁사업자명을 입력하세요."
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<Label className="text-label">위탁비용(백만원/년)</Label>
|
|
<Input
|
|
type="text"
|
|
inputMode="decimal"
|
|
value={cnsgnCstInput}
|
|
onChange={(e) => {
|
|
const v = e.target.value.replace(/[^\d.]/g, "");
|
|
const firstDot = v.indexOf(".");
|
|
const next =
|
|
firstDot === -1
|
|
? v
|
|
: v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, "");
|
|
setCnsgnCstInput(next);
|
|
}}
|
|
className="!text-[1.3rem]"
|
|
placeholder="0"
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<Label className="text-label">전체 인원</Label>
|
|
<Input
|
|
type="text"
|
|
inputMode="numeric"
|
|
value={wholNopeInput}
|
|
onChange={(e) => {
|
|
const v = e.target.value.replace(/\D/g, "");
|
|
setWholNopeInput(v);
|
|
}}
|
|
className="!text-[1.3rem]"
|
|
placeholder="0"
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<Label className="text-label">위탁계약기간</Label>
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
|
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
|
<Label className="text-label text-[1.2rem]">시작일</Label>
|
|
<InputDatePicker
|
|
value={prxyCtrtStart}
|
|
onChange={setPrxyCtrtStart}
|
|
className="w-full"
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
|
<Label className="text-label text-[1.2rem]">종료일</Label>
|
|
<InputDatePicker
|
|
value={prxyCtrtEnd}
|
|
onChange={setPrxyCtrtEnd}
|
|
className="w-full"
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
disabled={isSubmitting}
|
|
onClick={() => void handleConfirm()}
|
|
>
|
|
저장
|
|
</Button>
|
|
<DialogClose asChild>
|
|
<Button type="button" color="gray" disabled={isSubmitting}>
|
|
취소
|
|
</Button>
|
|
</DialogClose>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|