316 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|