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

343 lines
12 KiB
TypeScript

import { useMemo, useState } from "react";
import { GridFitStyle } from "realgrid";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
UnSubmitDataStatusItem,
unSubmitDataRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation } from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnConfig, ColumnLayoutItem } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
const SIDO_FIELD = "\uc2dc\ub3c4";
const SGG_FIELD = "\uc2dc\uad70\uad6c";
function sbmsnYnToOx(sbmsnYn: string): string {
const v = sbmsnYn.trim().toUpperCase();
if (v === "Y") return "O";
if (v === "N") return "X";
return sbmsnYn;
}
function splitSidoSggNm(sidoSggNm: string): { sido: string; sgg: string } {
const trimmed = sidoSggNm.trim();
if (!trimmed) return { sido: "", sgg: "" };
const spaceIdx = trimmed.indexOf(" ");
if (spaceIdx < 0) return { sido: trimmed, sgg: "" };
return {
sido: trimmed.slice(0, spaceIdx),
sgg: trimmed.slice(spaceIdx + 1).trim(),
};
}
function statColumnKey(statsNm: string, statsCd: string): string {
const trimmed = statsNm.trim();
const dotIdx = trimmed.indexOf(".");
if (dotIdx > 0) return trimmed.slice(0, dotIdx).trim();
return statsCd;
}
type StatDefinition = { statsCd: string; statsNm: string; columnKey: string };
/** submission-status statusList 기준 고정 컬럼(번호) */
const FIXED_STAT_DEFINITIONS: StatDefinition[] = [
{ columnKey: "1", statsCd: "ST00010001", statsNm: "1.하수도보급률" },
{ columnKey: "2-1", statsCd: "ST00020001", statsNm: "2-1.하수관로-지역별 시설현황" },
{ columnKey: "2-2", statsCd: "ST00020002", statsNm: "2-2.하수관로-시설별 설치현황" },
{ columnKey: "2-3", statsCd: "ST00020003", statsNm: "2-3.하수관로-관종별 현황" },
{ columnKey: "2-4", statsCd: "ST00020004", statsNm: "2-4.하수관로-관경별 현황" },
{ columnKey: "2-5", statsCd: "ST00020005", statsNm: "2-5.하수관로-맨홀 현황" },
{ columnKey: "3-1", statsCd: "ST00030001", statsNm: "3-1.하수관로유지관리-개․보수" },
{ columnKey: "3-2", statsCd: "ST00030002", statsNm: "3-2.하수관로유지관리-하수관로준설" },
{
columnKey: "4-1",
statsCd: "ST00040001",
statsNm: "4-1.하수저류시설및빗물펌프장-하수저류시설",
},
{
columnKey: "4-2",
statsCd: "ST00040002",
statsNm: "4-2.하수저류시설및빗물펌프장-빗물펌프장",
},
{ columnKey: "5", statsCd: "ST00050001", statsNm: "5.펌프장" },
{ columnKey: "6", statsCd: "ST00060001", statsNm: "6.공공하수처리시설" },
{ columnKey: "7", statsCd: "ST00070001", statsNm: "7.분뇨처리시설" },
{
columnKey: "8-1",
statsCd: "ST00080001",
statsNm: "8-1.개인하수처리시설-처리용량별 오수처리시설 현황",
},
{
columnKey: "8-2",
statsCd: "ST00080002",
statsNm: "8-2.개인하수처리시설-정화조 현황(처리방식별, 처리용량별)",
},
{
columnKey: "8-3",
statsCd: "ST00080003",
statsNm: "8-3.개인하수처리시설-개인하수도관리지역 지정공고 현황",
},
{ columnKey: "9-1", statsCd: "ST00090001", statsNm: "9-1.하수도재정-하수도 재정 결산" },
{ columnKey: "9-2", statsCd: "ST00090002", statsNm: "9-2.하수도재정-하수도 요금" },
{ columnKey: "9-3", statsCd: "ST00090003", statsNm: "9-3.하수도재정-업종별 하수도 사용료" },
{ columnKey: "9-4", statsCd: "ST00090004", statsNm: "9-4.하수도재정-하수도 원인자부담금" },
{ columnKey: "10-1", statsCd: "ST00100001", statsNm: "10-1.물재이용-공공하수처리수 재이용" },
{ columnKey: "10-2", statsCd: "ST00100002", statsNm: "10-2.물재이용-중수도 현황" },
{ columnKey: "10-3", statsCd: "ST00100003", statsNm: "10-3.물재이용-빗물이용시설현황" },
{
columnKey: "10-4",
statsCd: "ST00100004",
statsNm: "10-4.물재이용-하․폐수처리수 재이용시설 설계시공업 등록현황",
},
{
columnKey: "10-5",
statsCd: "ST00100005",
statsNm: "10-5.물재이용-하․폐수처리수 재이용사업 인가현황",
},
{
columnKey: "11-1",
statsCd: "ST00110001",
statsNm: "11-1.하수및분뇨찌꺼기(슬러지)처리-하수찌꺼기 발생 및 처리현황",
},
{
columnKey: "11-2",
statsCd: "ST00110002",
statsNm: "11-2.하수및분뇨찌꺼기(슬러지)처리-소화조 운영 현황",
},
{
columnKey: "11-3",
statsCd: "ST00110003",
statsNm: "11-3.하수및분뇨찌꺼기(슬러지)처리-하수찌꺼기 처리시설 운영 현황",
},
{
columnKey: "11-4",
statsCd: "ST00110004",
statsNm: "11-4.하수및분뇨찌꺼기(슬러지)처리-분뇨찌꺼기 처리현황",
},
{
columnKey: "12-1",
statsCd: "ST00120001",
statsNm: "12-1.하수및분뇨관련산업-공공기관 하수분야",
},
{
columnKey: "12-2",
statsCd: "ST00120002",
statsNm: "12-2.하수및분뇨관련산업-공공기관 분뇨분야",
},
{
columnKey: "12-3",
statsCd: "ST00120003",
statsNm: "12-3.하수및분뇨관련산업-분뇨수집 · 운반업 현황",
},
{
columnKey: "12-4",
statsCd: "ST00120004",
statsNm: "12-4.하수및분뇨관련산업-개인하수처리 관련업 현황",
},
{ columnKey: "13-1", statsCd: "ST00130001", statsNm: "13-1.하수도서비스수준-민원" },
{ columnKey: "13-2", statsCd: "ST00130002", statsNm: "13-2.하수도서비스수준-주민친화시설" },
];
const STAT_COLUMN_KEYS = FIXED_STAT_DEFINITIONS.map((d) => d.columnKey);
const STATS_CD_TO_COLUMN_KEY = Object.fromEntries(
FIXED_STAT_DEFINITIONS.map((d) => [d.statsCd, d.columnKey]),
) as Record<string, string>;
function resolveColumnKey(item: UnSubmitDataStatusItem): string {
return STATS_CD_TO_COLUMN_KEY[item.statsCd] ?? statColumnKey(item.statsNm, item.statsCd);
}
/** 헤더(statsNm) 텍스트 길이에 맞춘 컬럼 너비(px) */
function columnWidthForHeaderText(text: string, min = 88, max = 340): number {
return Math.min(max, Math.max(min, text.length * 9 + 28));
}
function buildColumnInfo(statDefinitions: StatDefinition[]) {
const fixedColumns: ColumnConfig[] = [
{
name: "sido",
fieldName: SIDO_FIELD,
header: { text: "시도" },
mergeCells: true,
editable: false,
sortdisable: true,
width: 100,
},
{
name: "sgg",
fieldName: SGG_FIELD,
header: { text: "시군구" },
mergeGroupField: SIDO_FIELD,
editable: false,
sortdisable: true,
width: 120,
},
];
const statColumns: ColumnConfig[] = statDefinitions.map(({ columnKey, statsNm }) => ({
name: columnKey,
fieldName: columnKey,
header: { text: statsNm },
editable: false,
sortdisable: true,
width: columnWidthForHeaderText(statsNm),
}));
const columns = [...fixedColumns, ...statColumns];
const statKeys = statDefinitions.map((s) => s.columnKey);
const columnLayout: ColumnLayoutItem[] = [
"sido",
"sgg",
{
name: "statsGroup",
hideChildHeaders: false,
header: { text: "미입력 하수도 통계 세부항목(번호)" },
items: statKeys,
},
];
return { columns, columnLayout, statKeys };
}
function buildRowsFromStatusList(
statusList: UnSubmitDataStatusItem[],
statKeys: string[],
): Record<string, string | number>[] {
const rowMap = new Map<string, Record<string, string | number>>();
for (const item of statusList) {
const { sido, sgg } = splitSidoSggNm(item.sidoSggNm);
const rowKey = `${sido}\u0001${sgg}`;
const columnKey = resolveColumnKey(item);
if (!rowMap.has(rowKey)) {
const row: Record<string, string | number> = {
[SIDO_FIELD]: sido,
[SGG_FIELD]: sgg,
};
for (const key of statKeys) {
row[key] = "";
}
rowMap.set(rowKey, row);
}
const row = rowMap.get(rowKey)!;
if (statKeys.includes(columnKey)) {
row[columnKey] = sbmsnYnToOx(item.sbmsnYn);
}
}
return Array.from(rowMap.values()).sort((a, b) => {
const sidoCmp = String(a[SIDO_FIELD]).localeCompare(String(b[SIDO_FIELD]), "ko");
if (sidoCmp !== 0) return sidoCmp;
return String(a[SGG_FIELD]).localeCompare(String(b[SGG_FIELD]), "ko");
});
}
function buildUnSubmitDataRequest(
values: SearchFormValues,
userInfo: { dtlInstCd?: string; enoCd?: string; laeSeCd?: string } | null,
): unSubmitDataRequest {
const sewerBzmnCd = values.bsnmCd?.trim();
return {
statsYr: values.year ?? "",
dtlInstCd: userInfo?.dtlInstCd ?? "",
enoCd: userInfo?.enoCd ?? "",
laeSeCd: userInfo?.laeSeCd ?? "",
...(sewerBzmnCd ? { sewerBzmnCd } : {}),
sido: !values.sido || values.sido === "ALL" ? "" : values.sido,
sggNm: !values.gugun || values.gugun === "ALL" ? "" : values.gugun,
unSubmitOnly: false,
};
}
const COLUMN_INFO = buildColumnInfo(FIXED_STAT_DEFINITIONS);
const NotEnteredStatus = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [isLoading, setIsLoading] = useState(false);
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const gridColumnInfo = useMemo(
() => ({
columns: COLUMN_INFO.columns,
columnLayout: COLUMN_INFO.columnLayout,
}),
[],
);
const handleSearch = async (values: SearchFormValues) => {
if (!values.year) {
alert("년도를 선택해주세요.");
return;
}
const params = buildUnSubmitDataRequest(values, userInfo);
console.log(params);
if (!params.dtlInstCd) {
alert("기관 정보를 확인할 수 없습니다.");
return;
}
try {
setIsLoading(true);
const response = await statisticsApi.getUnSubmitData(params);
if (response.status !== "success" || !response.data) {
alert(response.message ?? "조회에 실패했습니다.");
setRowData([]);
return;
}
const statusList = response.data.statusList ?? [];
setRowData(toGridRows(buildRowsFromStatusList(statusList, STAT_COLUMN_KEYS)));
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
setRowData([]);
} finally {
setIsLoading(false);
}
};
const handleReset = () => {
setRowData([]);
};
return (
<div className="relative">
<SearchForm
onSubmit={handleSearch}
onReset={handleReset}
sidoSelectText="전체"
/>
{isLoading && (
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
<DataGrid
columnInfo={gridColumnInfo}
rowData={rowData}
setRowData={setRowData}
isExcelImport={false}
fixedColumns={2}
rowStartNum={2}
fitStyle={GridFitStyle.NONE}
enableSort={false}
/>
</div>
);
};
export default NotEnteredStatus;