Files
frontend/apps/st/src/components/statistics/input/MainFacilityMappingSaveDialog.tsx
T

225 lines
8.5 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
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 isValidYmd(ymd: string): boolean {
if (!/^\d{8}$/.test(ymd)) return false;
const y = Number(ymd.slice(0, 4));
const m = Number(ymd.slice(4, 6));
const d = Number(ymd.slice(6, 8));
const dt = new Date(y, m - 1, d);
return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
}
function validatePrxyCtrtCn(value: string): string | null {
const t = value.trim();
if (!t) return "위탁계약기간을 입력해 주세요.";
if (!/^\d{8}~\d{8}$/.test(t)) {
return "위탁계약기간은 YYYYMMDD~YYYYMMDD 형식으로 입력해 주세요. (예: 20240101~20281231)";
}
const start = t.slice(0, 8);
const end = t.slice(9, 17);
if (!isValidYmd(start) || !isValidYmd(end)) {
return "시작일 또는 종료일이 유효한 날짜가 아닙니다.";
}
if (start > end) {
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 [prxyCtrtCn, setPrxyCtrtCn] = useState("");
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),
);
setPrxyCtrtCn((initialPrxyCtrtCn ?? "").trim());
}
}, [open, initialPrxyBzentyNmCn, initialCnsgnCstVl, initialWholNope, initialPrxyCtrtCn]);
const handleDialogOpenChange = (nextOpen: boolean) => {
if (!nextOpen && !isSubmitting) {
setPrxyBzentyNmCn("");
setCnsgnCstInput("");
setWholNopeInput("");
setPrxyCtrtCn("");
}
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 = validatePrxyCtrtCn(prxyCtrtCn);
if (prxyCtrtErr) {
alert(prxyCtrtErr);
return;
}
try {
setIsSubmitting(true);
await onConfirm?.({
prxyBzentyNmCn: prxyBzentyNmCn.trim(),
cnsgnCstVl,
wholNope,
prxyCtrtCn: prxyCtrtCn.trim(),
});
} 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>
<Input
value={prxyCtrtCn}
onChange={(e) => setPrxyCtrtCn(e.target.value)}
className="!text-[1.3rem]"
placeholder="YYYYMMDD~YYYYMMDD (예: 20240101~20281231)"
autoComplete="off"
disabled={isSubmitting}
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" color="gray" disabled={isSubmitting}>
취소
</Button>
</DialogClose>
<Button
type="button"
color="primary"
disabled={isSubmitting}
onClick={() => void handleConfirm()}
>
저장
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}