343 lines
12 KiB
TypeScript
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;
|