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

292 lines
10 KiB
TypeScript

import { useCallback, useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
StatsInputPeriodItem,
StatsInputPeriodListRequest,
} from "@/entities/statistics/model/statistics-interface";
import { Button, LoadingWithAnimation } from "@common/components";
import {
StatisticsInputPeriodDialog,
type StatisticsInputPeriodDialogValues,
} from "./StatisticsInputPeriodDialog";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnConfig, DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
/** API `YYYYMMDD` 등 → Date */
function seTypeToDisplayLabel(seType: StatsInputPeriodItem["seType"]): string {
if (seType === "INPT") return "입력";
if (seType === "VRFC") return "검증";
return seType;
}
function rowSeTypeToCode(row: Record<string, string | number>): "INPT" | "VRFC" {
const v = String(row.seType ?? "");
if (v === "INPT" || v === "입력") return "INPT";
if (v === "VRFC" || v === "검증") return "VRFC";
return "INPT";
}
function parseExtSchdlId(value: string | number | undefined): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value;
const parsed = parseInt(String(value ?? ""), 10);
return Number.isFinite(parsed) ? parsed : undefined;
}
function parseYmdToDate(ymd: string): Date | undefined {
const s = String(ymd ?? "").replace(/\D/g, "");
if (s.length !== 8) return undefined;
const y = Number(s.slice(0, 4));
const m = Number(s.slice(4, 6));
const d = Number(s.slice(6, 8));
if (!y || m < 1 || m > 12 || d < 1 || d > 31) return undefined;
const dt = new Date(y, m - 1, d);
if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) {
return undefined;
}
return dt;
}
function formatYmdForDisplay(ymd: string): string {
const s = String(ymd ?? "").replace(/\D/g, "");
if (s.length !== 8) return String(ymd ?? "");
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
}
const PERIOD_DETAIL_FIELD = "detailInfo";
const StatisticsInputManagement = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [isLoading, setIsLoading] = useState(false);
const [rowData, setRowData] = useState<Record<string, string | number>[]>(
[],
);
const [periodDialogOpen, setPeriodDialogOpen] = useState(false);
const [periodDialogMode, setPeriodDialogMode] = useState<"edit" | "create">("edit");
const [createDefaultStatsYr, setCreateDefaultStatsYr] = useState("");
const [periodDialogInitial, setPeriodDialogInitial] =
useState<StatisticsInputPeriodDialogValues | null>(null);
const [searchFormValues, setSearchFormValues] = useState<SearchFormValues | null>(null);
const lastSearchRef = useRef<SearchFormValues | null>(null);
const handleSearch = async (values: SearchFormValues) => {
if (!values.year) {
alert("년도를 선택해주세요.");
return;
}
lastSearchRef.current = values;
try {
setIsLoading(true);
const params: StatsInputPeriodListRequest = {
statsYr: values.year,
...(values.clsfCd && values.clsfCd !== "ALL"
? { seType: values.clsfCd }
: {}),
};
const res = await statisticsApi.getStatsInputPeriodList(params);
if (res.status === "success" && Array.isArray(res.data)) {
const rowsForGrid = res.data.map((row) => ({
...row,
seType: seTypeToDisplayLabel(row.seType),
inptBgngYmd: formatYmdForDisplay(row.inptBgngYmd),
inptEndYmd: formatYmdForDisplay(row.inptEndYmd),
[PERIOD_DETAIL_FIELD]: "상세정보",
}));
setRowData(toGridRows(rowsForGrid));
} else {
setRowData([]);
alert(res.message || "데이터 조회에 실패했습니다.");
}
} catch {
setRowData([]);
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const handleReset = () => {
setRowData([]);
};
const handleRegisterClick = () => {
const yr =
searchFormValues?.year ?? lastSearchRef.current?.year ?? "";
if (!yr) {
alert("검색 폼에서 년도를 선택해 주세요.");
return;
}
setPeriodDialogMode("create");
setCreateDefaultStatsYr(yr);
setPeriodDialogInitial(null);
setPeriodDialogOpen(true);
};
const openPeriodDetailForRow = useCallback(
async (row: Record<string, string | number>) => {
const seCdRaw = row.seCd;
const seCdNum =
typeof seCdRaw === "number"
? seCdRaw
: parseInt(String(seCdRaw ?? ""), 10);
const seCd = Number.isFinite(seCdNum) ? seCdNum : 0;
const statsYr = String(row.statsYr ?? "");
const seType = rowSeTypeToCode(row);
const extSchdlId = parseExtSchdlId(row.extSchdlId);
try {
setIsLoading(true);
const res = await statisticsApi.getStatsInputPeriodDetail({
statsYr,
seType,
seCd,
...(extSchdlId != null ? { extSchdlId } : {}),
});
if (res.status !== "success" || !res.data) {
alert(res.message || "상세 정보를 조회하지 못했습니다.");
return;
}
const detail = res.data;
const detailExtSchdlId = parseExtSchdlId(detail.extSchdlId) ?? extSchdlId;
setPeriodDialogMode("edit");
setPeriodDialogInitial({
extSchdlId: detailExtSchdlId,
statsYr: String(detail.statsYr ?? statsYr),
seType: detail.seType === "VRFC" ? "VRFC" : "INPT",
seCd:
typeof detail.seCd === "number" && Number.isFinite(detail.seCd)
? detail.seCd
: seCd,
inptBgngYmd: parseYmdToDate(detail.inptBgngYmd),
inptEndYmd: parseYmdToDate(detail.inptEndYmd),
});
setPeriodDialogOpen(true);
} catch {
alert("상세 정보 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
},
[alert],
);
const handlePeriodDetailCellClick = useCallback(
({ fieldName, row }: DataCellClickCallbackParams) => {
if (fieldName !== PERIOD_DETAIL_FIELD) return;
void openPeriodDetailForRow(row);
},
[openPeriodDetailForRow],
);
const columns: ColumnConfig[] = [
{
name: "statsYr",
fieldName: "statsYr",
header: { text: "연도" },
editable: false,
},
{
name: "seType",
fieldName: "seType",
header: { text: "분류" },
editable: false,
},
{
name: "inptBgngYmd",
fieldName: "inptBgngYmd",
header: { text: "시작일" },
editable: false,
},
{
name: "inptEndYmd",
fieldName: "inptEndYmd",
header: { text: "종료일" },
editable: false,
},
{
name: PERIOD_DETAIL_FIELD,
fieldName: PERIOD_DETAIL_FIELD,
header: { text: "상세정보" },
editable: false,
width: 118,
styleName: "rg-cell-outline-action-btn",
},
];
const columnLayout = [
"statsYr",
"seType",
"inptBgngYmd",
"inptEndYmd",
PERIOD_DETAIL_FIELD,
];
return (
<div className="relative">
<SearchForm
sido={false}
gugun={false}
clsf
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchFormValues}
/>
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<Button
color="primary"
className="h-[3.2rem] w-[96px]"
type="button"
onClick={handleRegisterClick}
>
<span>등록</span>
</Button>
</div>
</div>
<DataGrid
columnInfo={{ columns, columnLayout }}
rowData={rowData}
enableSort={false}
setRowData={setRowData}
isExcelImport={false}
isExcelExport={false}
onDataCellClick={handlePeriodDetailCellClick}
/>
<StatisticsInputPeriodDialog
open={periodDialogOpen}
mode={periodDialogMode}
createDefaultStatsYr={createDefaultStatsYr}
onOpenChange={(open) => {
setPeriodDialogOpen(open);
if (!open) {
setPeriodDialogInitial(null);
setPeriodDialogMode("edit");
}
}}
initial={periodDialogInitial}
onSaveSuccess={async () => {
const v = lastSearchRef.current;
if (v) await handleSearch(v);
}}
/>
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
</div>
);
};
export default StatisticsInputManagement;