Files
frontend/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx
T

316 lines
12 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
StatsInputPeriodDeleteRequest,
StatsInputPeriodSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
InputDatePicker,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
export type StatisticsInputPeriodDialogValues = {
statsYr: string;
seType: "INPT" | "VRFC";
seCd: number;
inptBgngYmd: Date | undefined;
inptEndYmd: Date | undefined;
};
export type StatisticsInputPeriodDialogMode = "edit" | "create";
function normalizeStatsYrInput(raw: string): string {
return raw.replace(/\D/g, "").slice(0, 4);
}
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}`;
}
export interface StatisticsInputPeriodDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** 행 클릭(수정) 시에만 전달. 등록 모드에서는 null */
initial: StatisticsInputPeriodDialogValues | null;
mode: StatisticsInputPeriodDialogMode;
/** mode가 create일 때 SearchForm의 연도 */
createDefaultStatsYr?: string;
/** 저장 성공 후 목록 재조회 등 */
onSaveSuccess?: () => void | Promise<void>;
}
export function StatisticsInputPeriodDialog({
open,
onOpenChange,
initial,
mode,
createDefaultStatsYr,
onSaveSuccess,
}: StatisticsInputPeriodDialogProps) {
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
const [statsYr, setStatsYr] = useState("");
const [seType, setSeType] = useState<"INPT" | "VRFC" | null>(null);
const [seCdInput, setSeCdInput] = useState("");
const [inptBgngYmd, setInptBgngYmd] = useState<Date | undefined>(undefined);
const [inptEndYmd, setInptEndYmd] = useState<Date | undefined>(undefined);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (!open) return;
if (mode === "create") {
setStatsYr(normalizeStatsYrInput(createDefaultStatsYr ?? ""));
setSeType(null);
setSeCdInput("0");
setInptBgngYmd(undefined);
setInptEndYmd(undefined);
return;
}
if (!initial) return;
setStatsYr(normalizeStatsYrInput(String(initial.statsYr ?? "")));
setSeType(initial.seType);
setSeCdInput(String(Number.isFinite(initial.seCd) ? initial.seCd : 0));
setInptBgngYmd(initial.inptBgngYmd);
setInptEndYmd(initial.inptEndYmd);
}, [open, mode, initial, createDefaultStatsYr]);
const handleSave = async () => {
const bgng = formatDateToYmd(inptBgngYmd);
const end = formatDateToYmd(inptEndYmd);
if (!bgng || !end) {
alert("시작일과 종료일을 선택해주세요.");
return;
}
if (bgng > end) {
alert("시작일은 종료일보다 늦을 수 없습니다.");
return;
}
if (seType === null) {
alert("분류를 선택해주세요.");
return;
}
const seCdParsed =
mode === "create"
? 0
: (() => {
const n = parseInt(seCdInput, 10);
return Number.isFinite(n) && n >= 0 ? n : NaN;
})();
if (mode === "edit" && !Number.isFinite(seCdParsed)) {
alert("구분 값이 올바르지 않습니다.");
return;
}
const yr = normalizeStatsYrInput(statsYr.trim());
if (mode === "create" && !/^\d{4}$/.test(yr)) {
alert("연도는 숫자 4자리로 입력해 주세요.");
return;
}
if (mode === "edit" && !yr) {
alert("연도가 없습니다.");
return;
}
try {
setIsSubmitting(true);
const userInfo = await getUserInfo();
const param: StatsInputPeriodSaveRequest = {
statsYr: yr,
seType,
seCd: seCdParsed,
inptBgngYmd: bgng,
inptEndYmd: end,
rgtrId: userInfo.userUnqId,
};
const res = await statisticsApi.saveStatsInputPeriod(param);
if (res.status === "success") {
alert(`${isCreate ? '등록 되었습니다.' : '수정 되었습니다.'}`);
onOpenChange(false);
await onSaveSuccess?.();
} else {
alert(res.message || "저장에 실패했습니다.");
}
} catch {
alert("저장 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
};
const handleDelete = async () => {
if (mode === "create" || !initial) return;
if (!(await confirm("삭제하시겠습니까?"))) return;
const bgng = formatDateToYmd(initial.inptBgngYmd);
const end = formatDateToYmd(initial.inptEndYmd);
if (!bgng || !end) {
alert("삭제할 기간 정보가 없습니다.");
return;
}
const yr = normalizeStatsYrInput(String(initial.statsYr ?? ""));
if (!yr) {
alert("연도가 없습니다.");
return;
}
const seCd =
Number.isFinite(initial.seCd) && initial.seCd >= 0 ? initial.seCd : 0;
try {
setIsSubmitting(true);
const userInfo = await getUserInfo();
const param: StatsInputPeriodDeleteRequest = {
statsYr: yr,
seType: initial.seType,
seCd,
inptBgngYmd: bgng,
inptEndYmd: end,
rgtrId: userInfo.userUnqId,
};
const res = await statisticsApi.deleteStatsInputPeriod(param);
if (res.status === "success") {
alert("삭제되었습니다.");
onOpenChange(false);
await onSaveSuccess?.();
} else {
alert(res.message || "삭제에 실패했습니다.");
}
} catch {
alert("삭제 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
};
const isCreate = mode === "create";
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[48rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>{isCreate ? "입력 기간 등록" : "입력 관리"}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
<div className="flex flex-col gap-2">
<Label className="text-label">연도</Label>
{isCreate ? (
<Input
type="text"
inputMode="numeric"
maxLength={4}
autoComplete="off"
value={statsYr}
onChange={(e) =>
setStatsYr(normalizeStatsYrInput(e.target.value))
}
placeholder="예: 2025"
disabled={isSubmitting}
className="!text-[1.3rem]"
/>
) : (
<Input
value={statsYr}
readOnly
disabled
className="!text-[1.3rem]"
/>
)}
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">분류</Label>
<Select
value={seType ?? undefined}
onValueChange={(v) => setSeType(v as "INPT" | "VRFC")}
disabled={isSubmitting}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="INPT">입력</SelectItem>
<SelectItem value="VRFC">검증</SelectItem>
</SelectContent>
</Select>
</div>
{!isCreate && (
<div className="flex flex-col gap-2">
<Label className="text-label">구분</Label>
<Input
type="number"
inputMode="numeric"
min={0}
value={seCdInput}
disabled
className="!text-[1.3rem]"
/>
</div>
)}
<div className="flex flex-col gap-2">
<Label className="text-label">시작일</Label>
<InputDatePicker
value={inptBgngYmd}
onChange={setInptBgngYmd}
className="w-full"
/>
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">종료일</Label>
<InputDatePicker
value={inptEndYmd}
onChange={setInptEndYmd}
className="w-full"
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" color="gray" disabled={isSubmitting}>
닫기
</Button>
</DialogClose>
{!isCreate && (
<Button
type="button"
color="red"
disabled={isSubmitting}
onClick={() => void handleDelete()}
>
삭제
</Button>
)}
<Button
type="button"
color="green"
disabled={isSubmitting}
onClick={() => void handleSave()}
>
{isCreate ? '등록' : '수정'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}