292 lines
10 KiB
TypeScript
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;
|