[st] - 검토단 작업 완료 및 입력6-1, 10-1 운영데이터업데이트 기능 추가, 텝 영역 우클릭 후 텝 닫기 기능 추가

This commit is contained in:
2026-06-30 14:23:08 +09:00
parent 162e18142c
commit afdc06638f
52 changed files with 7723 additions and 1591 deletions
+12
View File
@@ -8,6 +8,9 @@
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly {
@apply !bg-[#f0f0f0] !text-[#666];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr {
@apply !bg-white !text-[#333];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error {
@apply !bg-[#ffe6e6] !text-[#c00];
}
@@ -153,6 +156,15 @@
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-other {
@apply !bg-[#fff7bf];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr,
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr {
@apply !bg-white !text-[#333];
}
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-curr,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr,
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr {
@apply !bg-white !text-[#333];
}
/*
* 검증 오류 셀(rg-cell-verify-error / -imr) 최우선 적용.
+106 -59
View File
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { useEffect, useLayoutEffect, useRef, type MutableRefObject } from "react";
import JSZip from "jszip";
import { ValueType } from "realgrid";
@@ -14,6 +14,7 @@ import type {
CellStyleCallbackParams,
CellStyleCallbackResult,
CellTooltipCallback,
ColumnConfig,
ColumnLayoutItem,
DataGridProps,
FooterItemWithCallback,
@@ -48,6 +49,63 @@ function mergeCellStyleCallbackResult(
};
}
type GridColumnWithStyleCallback = {
styleCallback?: (
grid: unknown,
dataCell: {
itemIndex?: number;
index?: { itemIndex?: number };
dataField?: string;
fieldName?: string;
},
) => CellStyleCallbackResult;
};
function applyCellStyleCallbacksToColumns(
gridView: unknown,
columns: ColumnConfig[],
rowDataRef: MutableRefObject<Record<string, string | number>[]>,
cellStyleCallbackRef: MutableRefObject<CellStyleCallback | undefined>,
) {
if (!cellStyleCallbackRef.current || !gridView) return;
const gv = gridView as {
columnByName?: (name: string) => GridColumnWithStyleCallback | null | undefined;
};
if (typeof gv.columnByName !== "function") return;
columns.forEach((column) => {
const baseStyleName = column.styleName;
let gridCol: GridColumnWithStyleCallback | null | undefined;
try {
// columnByName은 GridView 인스턴스 메서드 — this 바인딩을 위해 gv에서 직접 호출
gridCol = gv.columnByName!(column.name);
} catch {
return;
}
if (!gridCol) return;
gridCol.styleCallback = (grid, dataCell) => {
const itemIndex =
dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1;
const fieldName =
dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName;
const data = rowDataRef.current;
if (itemIndex < 0 || itemIndex >= data.length) {
return mergeCellStyleCallbackResult(baseStyleName, undefined);
}
const row = data[itemIndex] as Record<string, string | number>;
const extra = cellStyleCallbackRef.current?.({
rowIndex: itemIndex,
fieldName,
row,
grid: grid as CellStyleCallbackParams["grid"],
});
return mergeCellStyleCallbackResult(baseStyleName, extra);
};
});
}
// RealGrid 타입 확장
declare module "realgrid" {
interface GridView {
@@ -725,9 +783,16 @@ const DataGrid = ({
}, [highlightedRowId, rowData, idFieldName]);
useLayoutEffect(() => {
if (!cellStyleCallback || !gridRef.current?.gridView) return;
gridRef.current.gridView.refresh();
}, [cellStyleCallback, rowData]);
const gv = gridRef.current?.gridView;
if (!gv || !cellStyleCallback) return;
applyCellStyleCallbacksToColumns(
gv,
columns,
rowDataRef,
cellStyleCallbackRef,
);
(gv as { refresh?: () => void }).refresh?.();
}, [cellStyleCallback, columns, rowData]);
const { setMessage: alert } = useAlertStore((state) => state);
const activeTabId = useTabStore((state) => state.activeTabId);
@@ -1049,6 +1114,10 @@ const DataGrid = ({
const gridView = gridRef.current.gridView;
const dataProvider = gridRef.current.dataProvider;
const footerTimerIds: ReturnType<typeof setTimeout>[] = [];
const scheduleFooterTask = (task: () => void, delayMs: number) => {
footerTimerIds.push(setTimeout(task, delayMs));
};
applyDefaultGridEditOptions(gridView as GridViewWithEditOptions);
const columnsFieldsSig = columns.map((c) => c.fieldName).join("\u0001");
@@ -1189,8 +1258,10 @@ const DataGrid = ({
});
const applyFooterSettings = (
targetGridView: typeof gridView,
targetGridView: typeof gridView | null | undefined,
) => {
if (!targetGridView?.setOptions) return;
const hasFooter = columns.some((col) => col.footer);
if (!hasFooter) return;
@@ -1308,7 +1379,7 @@ const DataGrid = ({
};
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
setTimeout(() => {
scheduleFooterTask(() => {
if (gridRef.current?.gridView && columnLayout) {
const safeLayout = sanitizeColumnLayout(columnLayout as unknown[]);
if (safeLayout.length === 0) return;
@@ -1327,10 +1398,9 @@ const DataGrid = ({
// footer 설정 (배열 또는 단일 객체 모두 처리)
// setFooters 호출 후 footer 배열 설정
setTimeout(() => {
scheduleFooterTask(() => {
const currentGridView = gridRef.current?.gridView;
if (!currentGridView || !currentGridView.columnByName) {
console.warn("GridView not available for footer setup");
if (!currentGridView?.columnByName) {
return;
}
@@ -1385,19 +1455,28 @@ const DataGrid = ({
col,
),
);
applyCellStyleCallbacksToColumns(
currentGridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
applyFooterSettings(currentGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
}
// setColumns 호출 후 footer 배열의 valueCallback을 다시 설정
setTimeout(() => {
scheduleFooterTask(() => {
const liveGridView = gridRef.current?.gridView;
if (!liveGridView?.columnByName) return;
columns.forEach((col) => {
if (!col.footer || !Array.isArray(col.footer))
return;
try {
const column = currentGridView.columnByName(
const column = liveGridView.columnByName(
col.name,
);
if (!column) return;
@@ -1429,11 +1508,17 @@ const DataGrid = ({
);
}
});
applyFooterSettings(currentGridView);
applyCellStyleCallbacksToColumns(
liveGridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
applyFooterSettings(liveGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
liveGridView.setColumnLayout?.(columnLayout);
}
currentGridView.refresh?.();
liveGridView.refresh?.();
}, 50);
} catch (e) {
console.warn("Failed to set columns with footer array:", e);
@@ -1495,6 +1580,7 @@ const DataGrid = ({
}
return () => {
footerTimerIds.forEach(clearTimeout);
if (getGridDataRef) getGridDataRef.current = null;
};
}, [
@@ -1939,19 +2025,9 @@ const DataGrid = ({
};
}
columns.forEach((column) => {
const baseStyleName = column.styleName;
const gridCol = gv.columnByName(
column.name,
) as {
styleCallback?: (
g: unknown,
dataCell: {
itemIndex?: number;
index?: { itemIndex?: number };
dataField?: string;
fieldName?: string;
},
) => CellStyleCallbackResult;
renderer?: { showTooltip?: boolean };
} | null;
if (gridCol) {
@@ -1990,43 +2066,14 @@ const DataGrid = ({
{ showTooltip: true },
);
}
gridCol.styleCallback = (grid, dataCell) => {
const itemIndex =
dataCell?.index?.itemIndex ??
dataCell?.itemIndex ??
-1;
const fieldName =
dataCell?.dataField ??
dataCell?.fieldName ??
column.fieldName;
const data = rowDataRef.current;
if (
itemIndex < 0 ||
itemIndex >= data.length
) {
return mergeCellStyleCallbackResult(
baseStyleName,
undefined,
);
}
const row = data[itemIndex] as Record<
string,
string | number
>;
const extra =
cellStyleCallbackRef.current?.({
rowIndex: itemIndex,
fieldName,
row,
grid: grid as CellStyleCallbackParams["grid"],
});
return mergeCellStyleCallbackResult(
baseStyleName,
extra,
);
};
}
});
applyCellStyleCallbacksToColumns(
gv,
columns,
rowDataRef,
cellStyleCallbackRef,
);
gv.refresh();
}, 0);
}}
@@ -234,6 +234,7 @@ export function VerificationResultHistoryDialog({
};
const checkedVrfcByTabRef = useRef(checkedVrfcByTab);
checkedVrfcByTabRef.current = checkedVrfcByTab;
const loadedTabKeysRef = useRef<Set<VerificationHistoryTabKey>>(new Set());
const resetState = useCallback(() => {
setActiveTab("NV");
@@ -243,6 +244,7 @@ export function VerificationResultHistoryDialog({
setErrorMessage(null);
setResetSubmitting(false);
setCheckedVrfcByTab(createEmptyCheckedVrfcByTab());
loadedTabKeysRef.current.clear();
}, []);
const fetchVerificationResultPage = useCallback(
@@ -279,10 +281,10 @@ export function VerificationResultHistoryDialog({
);
const loadTabPage = useCallback(
async (tab: VerificationHistoryTabKey, pageIndex: number) => {
async (tab: VerificationHistoryTabKey, pageIndex: number): Promise<boolean> => {
if (!stableQueryContext) {
setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
return;
return false;
}
setErrorMessage(null);
setLoadingByTab((prev) => ({ ...prev, [tab]: true }));
@@ -296,10 +298,12 @@ export function VerificationResultHistoryDialog({
? Math.max(0, Number(apiCurrentPage) - 1)
: pageIndex;
applyPageResponse(tab, resolvedPageIndex, list, totalPages);
return true;
} catch {
const tabLabel =
verificationHistoryTabs.find((t) => t.value === tab)?.label ?? tab;
setErrorMessage(`${tabLabel} 조회 중 오류가 발생했습니다.`);
return false;
} finally {
setLoadingByTab((prev) => ({ ...prev, [tab]: false }));
}
@@ -314,40 +318,27 @@ export function VerificationResultHistoryDialog({
}
setErrorMessage(null);
setTabPages(initialTabPageState());
setLoadingByTab({ NV: true, IMR: true, HB: true });
try {
const results = await Promise.all(
verificationHistoryTabs.map((tab) =>
fetchVerificationResultPage(tab.value, 0),
),
);
setEntriesByTab({
NV: results[0]?.data?.verifyResultList ?? [],
IMR: results[1]?.data?.verifyResultList ?? [],
HB: results[2]?.data?.verifyResultList ?? [],
});
setTabPages({
NV: {
pageIndex: 0,
pageCount: Math.max(results[0]?.data?.pageInfo?.totalPages ?? 1, 1),
},
IMR: {
pageIndex: 0,
pageCount: Math.max(results[1]?.data?.pageInfo?.totalPages ?? 1, 1),
},
HB: {
pageIndex: 0,
pageCount: Math.max(results[2]?.data?.pageInfo?.totalPages ?? 1, 1),
},
});
} catch {
setEntriesByTab({ NV: [], IMR: [], HB: [] });
setTabPages(initialTabPageState());
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
} finally {
setLoadingByTab({ NV: false, IMR: false, HB: false });
setEntriesByTab({ NV: [], IMR: [], HB: [] });
loadedTabKeysRef.current.clear();
const loaded = await loadTabPage("NV", 0);
if (loaded) {
loadedTabKeysRef.current.add("NV");
}
}, [fetchVerificationResultPage, stableQueryContext]);
}, [loadTabPage, stableQueryContext]);
const handleActiveTabChange = useCallback(
(tab: VerificationHistoryTabKey) => {
setActiveTab(tab);
if (loadedTabKeysRef.current.has(tab)) return;
void (async () => {
const loaded = await loadTabPage(tab, 0);
if (loaded) {
loadedTabKeysRef.current.add(tab);
}
})();
},
[loadTabPage],
);
const handleTabPageChange = useCallback(
(tab: VerificationHistoryTabKey, pageIndex: number) => {
@@ -577,7 +568,7 @@ export function VerificationResultHistoryDialog({
)}
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as VerificationHistoryTabKey)}
onValueChange={(value) => handleActiveTabChange(value as VerificationHistoryTabKey)}
className="w-full"
variant="default"
>
@@ -582,11 +582,6 @@ const PublicSewageFacilityInp = () => {
})();
};
const updateOperateDataButtonFunc = () => {
console.log('updatedOperateData');
// 운영 데이터 업데이트 로직
};
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [activeTab, setActiveTab] = useState<string>(
PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001',
@@ -766,6 +761,52 @@ const PublicSewageFacilityInp = () => {
}
};
const updateOperateDataButtonFunc = () => {
void (async () => {
if (!lastSearchedValues) {
alert('먼저 조회를 진행해주세요.');
return;
}
if (!(await confirm('운영 데이터를 업데이트 하시겠습니까?'))) return;
try {
showGlobalProgress({ label: '운영 데이터 업데이트 중...', progress: 0 });
const userInfo = await getUserInfo();
const request = {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId,
};
const response =
await statisticsInputPublicSewageFacilityApiService.updatePublicSewageFacilityOperationData(
request,
);
if (response.status !== 'success') {
alert(response.message || '운영 데이터 업데이트 중 오류가 발생했습니다.');
return;
}
setGlobalProgress(50);
const param: PublicSewageFacilityRequestParams = {
statsYr: request.statsYr,
ctpvNm: request.ctpvNm,
sggNm: request.sggNm,
sewerBzmnCd: request.sewerBzmnCd,
};
await loadPublicSewageFacilityActiveTabData('ST00060001', param, lastSearchedValues);
setGlobalProgress(100);
alert('운영 데이터가 업데이트되었습니다.');
} catch {
alert('운영 데이터 업데이트 중 오류가 발생했습니다.');
} finally {
hideGlobalProgress();
}
})();
};
const runMainFacilityMappingSave = async (
draft: MainFacilityMappingSaveDraft,
dialogInput: MainFacilityMappingSaveDialogValues | null,
@@ -660,7 +660,7 @@ const SewageExcretaSlurgeInp = () => {
Map<string, SewerageStatusInfo | null>
>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
@@ -50,7 +50,7 @@ import SearchForm from "@/components/module/SearchForm";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn";
@@ -4,7 +4,6 @@ import {
useMemo,
useRef,
useState,
type RefObject,
type Dispatch,
type SetStateAction,
} from "react";
@@ -25,7 +24,7 @@ import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButt
import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid, { applyDropDownColumnToGrid } from "@/components/module/DataGrid";
import DataGrid from "@/components/module/DataGrid";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
@@ -200,20 +199,6 @@ function createEmptyRainfallPumpStationRow(
return row;
}
function applySggNmDropDownToReservoirGrid(
gridRef: RefObject<RealGridReact | null>,
labels: string[],
) {
if (labels.length === 0) return;
applyDropDownColumnToGrid(
gridRef.current?.gridView as {
setColumnProperty?: (col: string, key: string, value: unknown) => void;
},
"sggNm",
labels,
);
}
function parseSggListFromResponse(res: ApiResponse<Sgg[]>): Sgg[] {
if (res.status !== "success" || res.data == null) return [];
return Array.isArray(res.data) ? res.data : [];
@@ -283,6 +268,16 @@ function toGridNumber(value: string | number | null | undefined): number {
return Number.isFinite(n) ? n : 0;
}
/** 펌프 구경(mm) — 콤마 제거 후 숫자 변환, 소수점 버림 */
function toPumpDmtrExpln(value: string | number | null | undefined): number {
const raw = String(value ?? "").trim();
if (raw === "") return 0;
const normalized = raw.replace(/,/g, "");
const num = Number(normalized);
if (!Number.isFinite(num)) return 0;
return Math.floor(num);
}
/** 시군구명(`sggNm`) 기준 `admdstCd`(bsnmCd)·`admdstNm`(시군구명) */
function resolveSewageReservoirAdmdst(
sggNm: string,
@@ -322,15 +317,15 @@ function buildSewageReservoirSaveItem(
wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea),
impdFcltCpct: toGridNumber(row.impdFcltCpct),
grssPpqty: toGridNumber(row.grssPpqty),
pumpFcltN1DmtrExpln: String(row.pumpFcltN1DmtrExpln ?? ""),
pumpFcltN1DmtrExpln: toPumpDmtrExpln(row.pumpFcltN1DmtrExpln),
pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty),
pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt),
pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt),
pumpFcltN2DmtrExpln: String(row.pumpFcltN2DmtrExpln ?? ""),
pumpFcltN2DmtrExpln: toPumpDmtrExpln(row.pumpFcltN2DmtrExpln),
pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty),
pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt),
pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt),
pumpFcltN3DmtrExpln: String(row.pumpFcltN3DmtrExpln ?? ""),
pumpFcltN3DmtrExpln: toPumpDmtrExpln(row.pumpFcltN3DmtrExpln),
pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty),
pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt),
pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt),
@@ -375,15 +370,15 @@ function buildRainfallPumpStationSaveItem(
rsvrImpndQnt: toGridNumber(row.rsvrImpndQnt),
abstnRgnImpdQnt: toGridNumber(row.abstnRgnImpdQnt),
grssPpqty: toGridNumber(row.grssPpqty),
pumpFcltN1DmtrExpln: String(row.pumpFcltN1DmtrExpln ?? ""),
pumpFcltN1DmtrExpln: toPumpDmtrExpln(row.pumpFcltN1DmtrExpln),
pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty),
pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt),
pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt),
pumpFcltN2DmtrExpln: String(row.pumpFcltN2DmtrExpln ?? ""),
pumpFcltN2DmtrExpln: toPumpDmtrExpln(row.pumpFcltN2DmtrExpln),
pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty),
pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt),
pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt),
pumpFcltN3DmtrExpln: String(row.pumpFcltN3DmtrExpln ?? ""),
pumpFcltN3DmtrExpln: toPumpDmtrExpln(row.pumpFcltN3DmtrExpln),
pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty),
pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt),
pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt),
@@ -850,8 +850,8 @@ const WaterReuseInp = () => {
(typeof gridView.getCheckedItems === "function"
? gridView.getCheckedItems()
: typeof gridView.getCheckedRows === "function"
? gridView.getCheckedRows()
: []) ?? [];
? gridView.getCheckedRows()
: []) ?? [];
return checked.filter(
(idx) => Number.isInteger(idx) && idx >= 0 && idx < rowCount,
@@ -1166,8 +1166,8 @@ const WaterReuseInp = () => {
const submitList = noApplicable
? []
: currentData.map((row) =>
buildRecycledWaterStatusSaveItem(row, list),
);
buildRecycledWaterStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRecycledWaterStatusSaveItem(row, list));
@@ -1186,8 +1186,8 @@ const WaterReuseInp = () => {
const submitList = noApplicable
? []
: currentData.map((row) =>
buildRainFallUtilFactStatusSaveItem(row, list),
);
buildRainFallUtilFactStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRainFallUtilFactStatusSaveItem(row, list));
@@ -1206,8 +1206,8 @@ const WaterReuseInp = () => {
const submitList = noApplicable
? []
: currentData.map((row) =>
buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
);
buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) =>
@@ -1231,8 +1231,8 @@ const WaterReuseInp = () => {
const submitList = noApplicable
? []
: currentData.map((row) =>
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
);
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) =>
@@ -1417,8 +1417,8 @@ const WaterReuseInp = () => {
const saveList = noApplicable
? []
: currentData.map((row) =>
buildRecycledWaterStatusSaveItem(row, list),
);
buildRecycledWaterStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRecycledWaterStatusSaveItem(row, list));
@@ -1477,8 +1477,8 @@ const WaterReuseInp = () => {
const saveList = noApplicable
? []
: currentData.map((row) =>
buildRainFallUtilFactStatusSaveItem(row, list),
);
buildRainFallUtilFactStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRainFallUtilFactStatusSaveItem(row, list));
@@ -1538,8 +1538,8 @@ const WaterReuseInp = () => {
const saveList = noApplicable
? []
: currentData.map((row) =>
buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
);
buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) =>
@@ -1603,8 +1603,8 @@ const WaterReuseInp = () => {
const saveList = noApplicable
? []
: currentData.map((row) =>
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
);
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) =>
@@ -1652,7 +1652,56 @@ const WaterReuseInp = () => {
};
const updateOperateDataButtonFunc = () => {
alert("운영 데이터 업데이트 API 연동 준비 중입니다.");
void (async () => {
if (!lastSearchedValues) {
alert("먼저 조회를 진행해주세요.");
return;
}
if (!(await confirm("운영 데이터를 업데이트 하시겠습니까?"))) return;
try {
showGlobalProgress({ label: "운영 데이터 업데이트 중...", progress: 0 });
const userInfo = await getUserInfo();
const request = {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
userId: userInfo.userUnqId,
};
const response =
await statisticsInputWaterReuseApi.updateWaterReuseOperationData(request);
if (response.status !== "success") {
alert(response.message || "운영 데이터 업데이트 중 오류가 발생했습니다.");
return;
}
setGlobalProgress(50);
const param: WaterReuseRequestParams = {
statsYr: request.statsYr,
ctpvNm: request.ctpvNm,
sggNm: request.sggNm,
sewerBzmnCd: request.sewerBzmnCd,
};
await loadActiveTabData(
PublicSewageTreatWaterReuseInpColumn.tabKey,
param,
lastSearchedValues,
false,
false,
);
setGlobalProgress(100);
alert("운영 데이터가 업데이트되었습니다.");
} catch {
alert("운영 데이터 업데이트 중 오류가 발생했습니다.");
} finally {
hideGlobalProgress();
}
})();
};
const addArticleFunc = async () => {
@@ -1716,10 +1765,10 @@ const WaterReuseInp = () => {
checkedIndexes.length > 0
? new Set(checkedIndexes)
: new Set(
currentRows
.map((row, idx) => (isCheckedRow(row) ? idx : -1))
.filter((idx) => idx >= 0),
);
currentRows
.map((row, idx) => (isCheckedRow(row) ? idx : -1))
.filter((idx) => idx >= 0),
);
const removedRows = currentRows.filter((_, idx) => checkedIndexSet.has(idx));
const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx));
@@ -1951,8 +2000,8 @@ const WaterReuseInp = () => {
e.vrfcCd === "IMR"
? "IMR검증 - "
: e.vrfcCd === "NV"
? "일반검증 - "
: "HB검증 - ";
? "일반검증 - "
: "HB검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
})
.filter(Boolean)
@@ -1,38 +1,315 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { TabDataGrid } from "@rsone/components";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import { LoadingWithAnimation } from "@common/components";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type {
ExcretaTreatmentFacilityReviewItem,
} from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface";
import {
extractExcretaTreatmentFacilityReviewList,
extractExcretaTreatmentFacilityReviewStatsStatusInfo,
} from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewOpinionSaveRequest,
ReviewerInfoRequest,
} from "@/entities/statistics/model/statistics-interface";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { ExcretaTreatmentFacilityInfoList } from "@/components/util/column/review/ExcretaTreatmentFacilityRvwColumn";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
// 하수도 통계 관리 07.분뇨처리 시설
const excretaTreatmentFacilityTab = ExcretaTreatmentFacilityInfoList[0];
// 하수도 통계 검토 07. 분뇨처리시설
const ExcretaTreatmentFacilityRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [statsStatusInfo, setStatsStatusInfo] = useState<StatsStatusInfo>({});
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [reviewOpinionContext, setReviewOpinionContext] =
useState<ReviewOpinionDialogContext | null>(null);
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
const [latestProcessHistory, setLatestProcessHistory] = useState("");
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const columnInfo = useMemo(() => {
if (!excretaTreatmentFacilityTab) {
return { columns: [], columnLayout: [], fixedColumns: 0, rowStartNum: 0 };
}
const approveFunc = () => console.log('approveFunc');
const saveFunc = () => console.log('submit');
const reviewOpinionFunc = () => console.log('reviewOpinionFunc');
const returnFunc = () => console.log('returnFunc');
const { columns, columnLayout, fixedColumns, rowStartNum } = excretaTreatmentFacilityTab;
return {
columnLayout,
fixedColumns,
rowStartNum,
columns: columns.map((col) => ({
...col,
editable: false as const,
})),
};
}, []);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [statsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (reviewOpinionContext == null) {
return null;
}
const instSeCd = userInfo?.instSeCd ?? "";
if (!instSeCd) {
return null;
}
return {
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
instSeCd,
};
}, [reviewOpinionContext, userInfo?.instSeCd]);
const handleReset = () => {
setRowData([]);
setStatsStatusInfo({});
setReviewOpinionContext(null);
setLatestReviewOpinion("");
setLatestProcessHistory("");
setLastSearchValues(null);
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
};
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
setLastSearchValues(values);
try {
setIsLoading(true);
const response =
await statisticsManagementApi.getExcretaTreatmentFacilityReviewList({
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
swtrmtFcltNm: values.fctName,
});
if (response.status === "success" && response.data) {
const rawList = extractExcretaTreatmentFacilityReviewList(response.data);
const statsInfo = extractExcretaTreatmentFacilityReviewStatsStatusInfo(response.data);
const convertedData: ExcretaTreatmentFacilityReviewItem[] = rawList.map((item) => ({
...item,
admdstNm: item.admdstNm ?? "",
fcltPstnCn: item.fcltPstnCn ?? "",
telNo: item.telNo ?? "",
cmptnStrtYmd: item.cmptnStrtYmd ?? "",
oprtngStrtYmd: item.oprtngStrtYmd ?? "",
prcsMthdExpln: item.prcsMthdExpln ?? "",
linkTmplntCn: item.linkTmplntCn ?? "",
wtsysNm: item.wtsysNm ?? "",
trbtrNm: item.trbtrNm ?? "",
msirbCn: item.msirbCn ?? "",
ssirbCn: item.ssirbCn ?? "",
operMthdCdCn: item.operMthdCdCn ?? "",
rmrk: item.rmrk ?? "",
}));
setStatsStatusInfo(statsInfo);
setReviewOpinionContext({
statsYr: values.year ?? "",
sewerBzmnCd: values.bsnmCd ?? "",
statsCd: statsInfo.statsCd ?? "",
});
setLatestReviewOpinion(statsInfo.splmntOpnnCn ?? "");
setLatestProcessHistory(statsInfo.wrterOpnnCn ?? "");
setRowData(toGridRows(convertedData as unknown as object[]));
} else {
setRowData([]);
setStatsStatusInfo({});
setReviewOpinionContext(null);
setLatestReviewOpinion("");
setLatestProcessHistory("");
alert(response.message || "데이터가 없습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setLatestReviewOpinion(row?.splmntOpnnCn ?? "");
setLatestProcessHistory(row?.wrterOpnnCn ?? "");
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setStatsStatusInfo((prev) => ({
...prev,
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
}));
setLatestReviewOpinion(param.splmntOpnnCn);
setLatestProcessHistory(param.wrterOpnnCn);
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<>
<SearchForm fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
<div className="relative">
<SearchForm fctName={true} onSubmit={handleSearch} onReset={handleReset} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && <SubmitButton submitFunc={submitFunc} />}
</div>
</div>
)}
{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 className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<DataGrid
columnInfo={columnInfo}
rowStartNum={columnInfo.rowStartNum}
excelExportFileSuffix={excretaTreatmentFacilityTab?.tabLabel ?? "분뇨처리시설"}
rowData={rowData}
setRowData={setRowData}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={columnInfo.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</div>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
<TabDataGrid columnInfo={ExcretaTreatmentFacilityInfoList} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
);
};
export default ExcretaTreatmentFacilityRvw;
export default ExcretaTreatmentFacilityRvw;
@@ -1,51 +1,428 @@
import { useState } from "react";
import { useMemo, useRef, useState } from "react";
import { TabDataGrid } from "@rsone/components";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
extractPrivateSewageFacilityReviewList,
extractPrivateSewageFacilityReviewStatsStatusInfo,
mapPrivateSewageFacilityReviewRows,
type PrivateSewageFacilityReviewRequestParams,
} from "@/entities/statistics/model/statistics-private-sewage-facility-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { PrivateSewageFacilityInfoListRvw } from "@/components/util/column/review/PrivateSewageFacilityColumn";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
// 하수도 통계 관리 08.개인하수처리시설
const firstTabKey = PrivateSewageFacilityInfoListRvw[0]?.tabKey ?? "ST00080001";
async function fetchPrivateSewageFacilityReviewTab(
tabKey: string,
searchParam: PrivateSewageFacilityReviewRequestParams,
) {
if (tabKey === "ST00080001") {
return statisticsManagementApi.getPrivateSewageTreatFactStatusReviewList(searchParam);
}
if (tabKey === "ST00080002") {
return statisticsManagementApi.getSepticTankStatusReviewList(searchParam);
}
if (tabKey === "ST00080003") {
return statisticsManagementApi.getPrivateSewageDesigStatusReviewList(searchParam);
}
return null;
}
// 하수도 통계 검토 08. 개인하수처리시설
const PrivateSewageFacilityRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
>(new Map());
const approveFunc = () => {
console.log('approveFunc');
// 승인 로직
const searchParamRef = useRef<PrivateSewageFacilityReviewRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
const activeStatsStatusInfo = useMemo(
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
[tabStatsStatusInfoMap, activeTab],
);
const columnInfo = useMemo(() => {
return PrivateSewageFacilityInfoListRvw.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
}));
}, []);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
if (lastSearchValues == null) return null;
return {
statsYr: lastSearchValues.year ?? "",
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
statsCd: activeStatsStatusInfo.statsCd ?? "",
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchValues == null) return null;
const statsYr = lastSearchValues.year ?? "";
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
const statsCd = activeStatsStatusInfo.statsCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
statsCd,
instSeCd,
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoggedRef.current.clear();
};
const saveFunc = () => {
console.log('submit');
// 저장 로직
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: PrivateSewageFacilityReviewRequestParams = {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
};
searchParamRef.current = searchParam;
searchDoneRef.current = true;
lazyTabLoggedRef.current.clear();
const response = await fetchPrivateSewageFacilityReviewTab(activeTab, searchParam);
if (response?.status === "success" && response.data) {
const ssi = extractPrivateSewageFacilityReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, ssi);
return next;
});
const listData = extractPrivateSewageFacilityReviewList(activeTab, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(mapPrivateSewageFacilityReviewRows(listData)));
return next;
});
} else {
setTabStatsStatusInfoMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
setTabStatsStatusInfoMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const activeTabData = rowDataMap.get(tabKey);
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
const canLazyLoad =
searchParamRef.current &&
!hasActiveTabData &&
!lazyTabLoggedRef.current.has(tabKey) &&
searchDoneRef.current;
if (!canLazyLoad) return;
const searchParam = searchParamRef.current;
if (!searchParam) return;
try {
setIsLoading(true);
const response = await fetchPrivateSewageFacilityReviewTab(tabKey, searchParam);
lazyTabLoggedRef.current.add(tabKey);
if (response?.status === "success" && response.data) {
const ssi = extractPrivateSewageFacilityReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, ssi);
return next;
});
setRowDataMap((prev) => {
const next = new Map(prev);
const listData = extractPrivateSewageFacilityReviewList(tabKey, response.data);
next.set(tabKey, toGridRows(mapPrivateSewageFacilityReviewRows(listData)));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const returnFunc = () => {
console.log('returnFunc');
// 반려
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: row?.splmntOpnnCn ?? "",
wrterOpnnCn: row?.wrterOpnnCn ?? "",
});
return next;
});
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
});
return next;
});
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<>
<SearchForm fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
)}
{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 className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<TabDataGrid columnInfo={PrivateSewageFacilityInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
@@ -22,7 +22,7 @@ import {
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
import { PublicSewageFacilityInfoList } from "@/components/util/column/review/PublicSewageFacilityRvwColumn";
@@ -250,6 +250,7 @@ const PublicSewageFacilityRvw = () => {
rowStartNum={columnInfo.rowStartNum}
rowData={rowData}
setRowData={setRowData}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
<ReviewOpinionDialog
open={reviewOpinionOpen}
@@ -1,26 +1,52 @@
import { useState, useMemo } from "react";
import { useMemo, useState } from "react";
import { LoadingWithAnimation } from "@common/components";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import {
extractMediationPumpStationList,
extractMediationPumpStationStatsStatusInfo,
type MediationPumpStationItem,
} from "@/entities/statistics/model/statistics-pumping-station-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewOpinionSaveRequest,
ReviewerInfoRequest,
} from "@/entities/statistics/model/statistics-interface";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import ReturnButton from "@/components/button/ReturnButton";
import { LoadingWithAnimation } from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { PumpingStationInfoList } from "@/components/util/column/review/PumpingStationRvwColumn";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import type { MediationPumpStationItem } from "@/entities/statistics/model/statistics-pumping-station-review-interface";
import { PumpingStationInfoList } from "@/components/util/column/review/PumpingStationRvwColumn";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
/**
* 하수도 통계 검토 05.중계펌프장
*/
const PumpingStationRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [statsStatusInfo, setStatsStatusInfo] = useState<StatsStatusInfo>({});
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [reviewOpinionContext, setReviewOpinionContext] =
useState<ReviewOpinionDialogContext | null>(null);
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
const [latestProcessHistory, setLatestProcessHistory] = useState("");
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const columnInfo = useMemo(() => {
const { columns, columnLayout, fixedColumns, rowStartNum } = PumpingStationInfoList;
@@ -36,65 +62,193 @@ const PumpingStationRvw = () => {
};
}, []);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [statsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (reviewOpinionContext == null) {
return null;
}
const instSeCd = userInfo?.instSeCd ?? "";
if (!instSeCd) {
return null;
}
return {
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
instSeCd,
};
}, [reviewOpinionContext, userInfo?.instSeCd]);
const handleReset = () => {
setRowData([]);
setStatsStatusInfo({});
setReviewOpinionContext(null);
setLatestReviewOpinion("");
setLatestProcessHistory("");
setLastSearchValues(null);
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
};
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
setLastSearchValues(values);
try {
setIsLoading(true);
const response = await statisticsManagementApi.getMediationPumpStationList({
ctpvNm: values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
pumpstNm: values.fctName,
});
if (response.status === 'success' && response.data) {
const rawList = response.data as MediationPumpStationItem[];
const convertedData = rawList.map(item => ({
if (response.status === "success" && response.data) {
const rawList = extractMediationPumpStationList(response.data);
const statsInfo = extractMediationPumpStationStatsStatusInfo(response.data);
const convertedData = rawList.map((item: MediationPumpStationItem) => ({
...item,
rmrk: item.rmrk || ''
admdstNm: item.admdstNm ?? "",
pumpstNm: item.fcltNm ?? "",
impdFcltPstnCn: item.fcltPstnCn ?? "",
pumpSeNm: item.pumpSeNm ?? "",
swtrmtFcltNm: item.swtrmtFcltNm ?? "",
oprtngStrtYmd: item.oprtngStrtYmd ?? "",
exlnPumpN4RsrvCnt: item.exlnPumpN1RsrvCnt ?? 0,
rmrk: item.rmrk ?? "",
}));
setStatsStatusInfo(statsInfo);
setReviewOpinionContext({
statsYr: values.year ?? "",
sewerBzmnCd: values.bsnmCd ?? "",
statsCd: statsInfo.statsCd ?? "",
});
setLatestReviewOpinion(statsInfo.splmntOpnnCn ?? "");
setLatestProcessHistory(statsInfo.wrterOpnnCn ?? "");
setRowData(toGridRows(convertedData));
} else {
setRowData([]);
setStatsStatusInfo({});
setReviewOpinionContext(null);
setLatestReviewOpinion("");
setLatestProcessHistory("");
alert(response.message || "데이터가 없습니다.");
}
} catch (error) {
console.error("중계펌프장 조회 중 에러 발생:", error);
alert('데이터 조회 중 오류가 발생했습니다.');
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const approveFunc = () => console.log('approveFunc');
const saveFunc = () => console.log('submit');
const reviewOpinionFunc = () => console.log('reviewOpinionFunc');
const returnFunc = () => console.log('returnFunc');
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setLatestReviewOpinion(row?.splmntOpnnCn ?? "");
setLatestProcessHistory(row?.wrterOpnnCn ?? "");
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setStatsStatusInfo((prev) => ({
...prev,
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
}));
setLatestReviewOpinion(param.splmntOpnnCn);
setLatestProcessHistory(param.wrterOpnnCn);
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<div className="relative">
<SearchForm fctName={true} onSubmit={handleSearch} onReset={handleReset} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && <SubmitButton submitFunc={submitFunc} />}
</div>
</div>
</div>
)}
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -104,16 +258,48 @@ const PumpingStationRvw = () => {
</div>
)}
<DataGrid
columnInfo={columnInfo}
isExcelImport={false}
fixedColumns={columnInfo.fixedColumns}
rowStartNum={columnInfo.rowStartNum}
rowData={rowData}
setRowData={setRowData}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<DataGrid
columnInfo={columnInfo}
rowStartNum={columnInfo.rowStartNum}
excelExportFileSuffix="중계펌프장"
rowData={rowData}
setRowData={setRowData}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={columnInfo.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</div>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
export default PumpingStationRvw;
export default PumpingStationRvw;
@@ -65,7 +65,6 @@ export function ReviewSubmitDialog({
if (cancelled) return;
if (response.status !== "success" || !response.data) {
alert("제출 정보를 불러오지 못했습니다.");
setReviewerInfo(null);
return;
}
@@ -73,7 +72,6 @@ export function ReviewSubmitDialog({
setReviewerInfo(response.data);
} catch {
if (!cancelled) {
alert("제출 정보를 불러오지 못했습니다.");
setReviewerInfo(null);
}
} finally {
@@ -91,7 +89,7 @@ export function ReviewSubmitDialog({
}, [open, context, alert]);
const handleConfirm = async () => {
if (context == null || reviewerInfo == null) {
if (context == null) {
alert("제출 정보를 확인한 뒤 다시 시도해주세요.");
return;
}
@@ -105,8 +103,8 @@ export function ReviewSubmitDialog({
sewerBzmnCd: context.sewerBzmnCd,
statsCd: context.statsCd,
mdfrId: userInfo.userUnqId,
splmntOpnnCn: reviewerInfo.splmntOpnnCn ?? "",
wrterOpnnCn: reviewerInfo.wrterOpnnCn ?? "",
splmntOpnnCn: reviewerInfo?.splmntOpnnCn ?? "",
wrterOpnnCn: reviewerInfo?.wrterOpnnCn ?? "",
};
const response = await statisticsApi.submitReview(submitParam);
@@ -177,7 +175,7 @@ export function ReviewSubmitDialog({
type="button"
color="primary"
onClick={() => void handleConfirm()}
disabled={isLoading || isSubmitting || reviewerInfo == null}
disabled={isLoading || isSubmitting}
>
제출
</Button>
@@ -1,53 +1,434 @@
import { useState } from "react";
import { useMemo, useRef, useState } from "react";
import { TabDataGrid } from "@rsone/components";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
extractSewageExcretaRelatedReviewList,
extractSewageExcretaRelatedReviewStatsStatusInfo,
mapSewageExcretaRelatedReviewRows,
type SewageExcretaRelatedReviewRequestParams,
} from "@/entities/statistics/model/statistics-sewage-excreta-related-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewageExcretaRelatedInfoListRvw } from "@/components/util/column/review/SewageExcretaRelatedRvwColumn";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
// 하수도 통계 관리 12. 하수 및 분뇨 관련 산업
const firstTabKey = SewageExcretaRelatedInfoListRvw[0]?.tabKey ?? "ST00120001";
async function fetchExcretaRelatedReviewTab(
tabKey: string,
searchParam: SewageExcretaRelatedReviewRequestParams,
) {
if (tabKey === "ST00120001") {
return statisticsManagementApi.getPublicSewageStaffReviewList(searchParam);
}
if (tabKey === "ST00120002") {
return statisticsManagementApi.getPublicHumanWasteStaffReviewList(searchParam);
}
if (tabKey === "ST00120003") {
return statisticsManagementApi.getHumanWasteCollectionReviewList(searchParam);
}
if (tabKey === "ST00120004") {
return statisticsManagementApi.getIndvSwtrmtRelBsnsReviewList(searchParam);
}
return null;
}
// 하수도 통계 검토 12. 하수 및 분뇨 관련 산업
const SewageExcretaRelatedRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
>(new Map());
const approveFunc = () => {
console.log('approveFunc');
// 승인 로직
const searchParamRef = useRef<SewageExcretaRelatedReviewRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoadedRef = useRef<Set<string>>(new Set());
const activeStatsStatusInfo = useMemo(
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
[tabStatsStatusInfoMap, activeTab],
);
const columnInfo = useMemo(() => {
return SewageExcretaRelatedInfoListRvw.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
}));
}, []);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
if (lastSearchValues == null) return null;
return {
statsYr: lastSearchValues.year ?? "",
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
statsCd: activeStatsStatusInfo.statsCd ?? "",
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchValues == null) return null;
const statsYr = lastSearchValues.year ?? "";
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
const statsCd = activeStatsStatusInfo.statsCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
statsCd,
instSeCd,
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoadedRef.current.clear();
};
const saveFunc = () => {
console.log('submit');
// 저장 로직
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: SewageExcretaRelatedReviewRequestParams = {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
};
searchParamRef.current = searchParam;
searchDoneRef.current = true;
lazyTabLoadedRef.current.clear();
const response = await fetchExcretaRelatedReviewTab(activeTab, searchParam);
if (response?.status === "success" && response.data) {
const ssi = extractSewageExcretaRelatedReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, ssi);
return next;
});
const listData = extractSewageExcretaRelatedReviewList(activeTab, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(mapSewageExcretaRelatedReviewRows(listData)));
return next;
});
} else {
setTabStatsStatusInfoMap(new Map());
setRowDataMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
setTabStatsStatusInfoMap(new Map());
setRowDataMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const activeTabData = rowDataMap.get(tabKey);
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
const canLazyLoad =
searchParamRef.current &&
!hasActiveTabData &&
!lazyTabLoadedRef.current.has(tabKey) &&
searchDoneRef.current;
if (!canLazyLoad) return;
const searchParam = searchParamRef.current;
if (!searchParam) return;
try {
setIsLoading(true);
const response = await fetchExcretaRelatedReviewTab(tabKey, searchParam);
lazyTabLoadedRef.current.add(tabKey);
if (response?.status === "success" && response.data) {
const ssi = extractSewageExcretaRelatedReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, ssi);
return next;
});
setRowDataMap((prev) => {
const next = new Map(prev);
const listData = extractSewageExcretaRelatedReviewList(tabKey, response.data);
next.set(tabKey, toGridRows(mapSewageExcretaRelatedReviewRows(listData)));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const returnFunc = () => {
console.log('returnFunc');
// 반려
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: row?.splmntOpnnCn ?? "",
wrterOpnnCn: row?.wrterOpnnCn ?? "",
});
return next;
});
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
});
return next;
});
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<>
<SearchForm fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
)}
{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 className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<TabDataGrid columnInfo={SewageExcretaRelatedInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
export default SewageExcretaRelatedRvw;
@@ -1,52 +1,435 @@
import { useState } from "react";
import { useMemo, useRef, useState } from "react";
import { TabDataGrid } from "@rsone/components";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
extractSewageExcretaSlurgeReviewList,
extractSewageExcretaSlurgeReviewStatsStatusInfo,
mapSewageExcretaSlurgeReviewRows,
type SewageExcretaSlurgeReviewRequestParams,
} from "@/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewageExcretaSludgeInfoListRvw } from "@/components/util/column/review/SewageExcretaSlurgeRvwColumn";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
// 하수도 통계 관리 11. 하수 및 분뇨 찌꺼기(슬러지) 처리
const firstTabKey = SewageExcretaSludgeInfoListRvw[0]?.tabKey ?? "ST00110001";
async function fetchSludgeReviewTab(
tabKey: string,
searchParam: SewageExcretaSlurgeReviewRequestParams,
) {
if (tabKey === "ST00110001") {
return statisticsManagementApi.getSludgeProcessingReviewList(searchParam);
}
if (tabKey === "ST00110002") {
return statisticsManagementApi.getSludgeDigestionTankReviewList(searchParam);
}
if (tabKey === "ST00110003") {
return statisticsManagementApi.getSludgeFacilityOperReviewList(searchParam);
}
if (tabKey === "ST00110004") {
return statisticsManagementApi.getHumanWasteSludgeReviewList(searchParam);
}
return null;
}
// 하수도 통계 검토 11. 하수 및 분뇨 찌꺼기(슬러지) 처리
const SewageExcretaSlurgeRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
>(new Map());
const approveFunc = () => {
console.log('approveFunc');
// 승인 로직
const searchParamRef = useRef<SewageExcretaSlurgeReviewRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoadedRef = useRef<Set<string>>(new Set());
const activeStatsStatusInfo = useMemo(
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
[tabStatsStatusInfoMap, activeTab],
);
const columnInfo = useMemo(() => {
return SewageExcretaSludgeInfoListRvw.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
}));
}, []);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
if (lastSearchValues == null) return null;
return {
statsYr: lastSearchValues.year ?? "",
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
statsCd: activeStatsStatusInfo.statsCd ?? "",
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchValues == null) return null;
const statsYr = lastSearchValues.year ?? "";
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
const statsCd = activeStatsStatusInfo.statsCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
statsCd,
instSeCd,
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoadedRef.current.clear();
};
const saveFunc = () => {
console.log('submit');
// 저장 로직
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: SewageExcretaSlurgeReviewRequestParams = {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
swtrmtFcltNm: values.fctName,
};
searchParamRef.current = searchParam;
searchDoneRef.current = true;
lazyTabLoadedRef.current.clear();
const response = await fetchSludgeReviewTab(activeTab, searchParam);
if (response?.status === "success" && response.data) {
const ssi = extractSewageExcretaSlurgeReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, ssi);
return next;
});
const listData = extractSewageExcretaSlurgeReviewList(activeTab, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(mapSewageExcretaSlurgeReviewRows(listData)));
return next;
});
} else {
setTabStatsStatusInfoMap(new Map());
setRowDataMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
setTabStatsStatusInfoMap(new Map());
setRowDataMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const activeTabData = rowDataMap.get(tabKey);
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
const canLazyLoad =
searchParamRef.current &&
!hasActiveTabData &&
!lazyTabLoadedRef.current.has(tabKey) &&
searchDoneRef.current;
if (!canLazyLoad) return;
const searchParam = searchParamRef.current;
if (!searchParam) return;
try {
setIsLoading(true);
const response = await fetchSludgeReviewTab(tabKey, searchParam);
lazyTabLoadedRef.current.add(tabKey);
if (response?.status === "success" && response.data) {
const ssi = extractSewageExcretaSlurgeReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, ssi);
return next;
});
setRowDataMap((prev) => {
const next = new Map(prev);
const listData = extractSewageExcretaSlurgeReviewList(tabKey, response.data);
next.set(tabKey, toGridRows(mapSewageExcretaSlurgeReviewRows(listData)));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const returnFunc = () => {
console.log('returnFunc');
// 반려
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: row?.splmntOpnnCn ?? "",
wrterOpnnCn: row?.wrterOpnnCn ?? "",
});
return next;
});
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
});
return next;
});
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<>
<SearchForm fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
)}
{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 className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<TabDataGrid columnInfo={SewageExcretaSludgeInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
export default SewageExcretaSlurgeRvw;
export default SewageExcretaSlurgeRvw;
@@ -1,21 +1,33 @@
import { useMemo, useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SewerPipeMtcRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
SewerPipeMtcRequestParams,
StatsStatusInfo,
} from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import ApproveButton from "@/components/button/ApproveButton";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SaveButton from "@/components/button/SaveButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
const firstTabKey = SewagePipeMaintainColumnInfoList[0]?.tabKey ?? "ST00030001";
@@ -50,17 +62,36 @@ function getListDataByTab(tabKey: string, responseData: unknown): object[] {
return [];
}
function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null {
if (!data || typeof data !== "object") return null;
const ssi = (data as { statsStatusInfo?: StatsStatusInfo }).statsStatusInfo;
return ssi && typeof ssi === "object" ? ssi : null;
}
// 하수도 통계 검토 03. 하수관로 유지 관리
const SewagePipeMaintainRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
>(new Map());
const searchParamRef = useRef<SewerPipeMtcRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
const activeStatsStatusInfo = useMemo(
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
[tabStatsStatusInfoMap, activeTab],
);
const columnInfo = useMemo(() => {
return SewagePipeMaintainColumnInfoList.map((tab: ColumnInfoList) => ({
...tab,
@@ -72,9 +103,66 @@ const SewagePipeMaintainRvw = () => {
}));
}, []);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
if (lastSearchValues == null) return null;
return {
statsYr: lastSearchValues.year ?? "",
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
statsCd: activeStatsStatusInfo.statsCd ?? "",
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchValues == null) return null;
const statsYr = lastSearchValues.year ?? "";
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
const statsCd = activeStatsStatusInfo.statsCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
statsCd,
instSeCd,
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoggedRef.current.clear();
@@ -88,6 +176,7 @@ const SewagePipeMaintainRvw = () => {
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: SewerPipeMtcRequestParams = {
statsYr: values.year,
@@ -103,6 +192,12 @@ const SewagePipeMaintainRvw = () => {
const response = await fetchSewagePipeMaintainReviewTab(activeTab, searchParam);
if (response?.status === "success" && response.data) {
const ssi = extractStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, ssi ?? {});
return next;
});
const listData = getListDataByTab(activeTab, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
@@ -110,9 +205,11 @@ const SewagePipeMaintainRvw = () => {
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
setTabStatsStatusInfoMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
setTabStatsStatusInfoMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
@@ -143,6 +240,12 @@ const SewagePipeMaintainRvw = () => {
lazyTabLoggedRef.current.add(tabKey);
if (response?.status === "success" && response.data) {
const ssi = extractStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, ssi ?? {});
return next;
});
setRowDataMap((prev) => {
const next = new Map(prev);
const listData = getListDataByTab(tabKey, response.data);
@@ -159,18 +262,96 @@ const SewagePipeMaintainRvw = () => {
}
};
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: row?.splmntOpnnCn ?? "",
wrterOpnnCn: row?.wrterOpnnCn ?? "",
});
return next;
});
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
});
return next;
});
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={() => { }} />
<ApproveButton approveFunc={() => { }} />
<ReturnButton returnFunc={() => { }} />
<SaveButton saveFunc={() => { }} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
</div>
)}
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -228,14 +409,39 @@ const SewagePipeMaintainRvw = () => {
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
export default SewagePipeMaintainRvw;
export default SewagePipeMaintainRvw;
@@ -32,7 +32,7 @@ import type {
} from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
const firstTabKey = SewagePipeColumnInfoListRvw[0]?.tabKey ?? "ST00020001";
@@ -459,6 +459,7 @@ const SewagePipeRvw = () => {
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</TabsContent>
);
@@ -1,19 +1,72 @@
import { useState, useMemo, useRef } from "react";
import TabDataGrid from "@/components/module/TabDataGrid";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import ReturnButton from "@/components/button/ReturnButton";
import { LoadingWithAnimation } from "@common/components";
import { useMemo, useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn";
import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type { SewerReservoirRequestParams } from "@/entities/statistics/model/sewage-reservoir-pump-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
const firstTabKey = SewageReservoirPumpInfoListRvw[0]?.tabKey ?? "ST00040001";
async function fetchSewageReservoirPumpReviewTab(
tabKey: string,
searchParam: SewerReservoirRequestParams,
) {
if (tabKey === "ST00040001") {
return statisticsManagementApi.getSewerReservoirList(searchParam);
}
if (tabKey === "ST00040002") {
return statisticsManagementApi.getPumpStationList(searchParam);
}
return null;
}
function getListDataByTab(tabKey: string, responseData: unknown): object[] {
if (Array.isArray(responseData)) {
return responseData as object[];
}
if (!responseData || typeof responseData !== "object") {
return [];
}
const data = responseData as Record<string, unknown>;
if (tabKey === "ST00040001") {
const list = data.sewerReservoirList ?? data.facilitiesList;
return Array.isArray(list) ? (list as object[]) : [];
}
if (tabKey === "ST00040002") {
const list = data.pumpStationList ?? data.pumpingStationList;
return Array.isArray(list) ? (list as object[]) : [];
}
return [];
}
function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null {
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
const ssi = (data as { statsStatusInfo?: StatsStatusInfo }).statsStatusInfo;
return ssi && typeof ssi === "object" ? ssi : null;
}
/**
* 하수도 통계 검토 04. 하수저류 시설 및 빗물 펌프장
@@ -22,11 +75,26 @@ import type { SewerReservoirRequestParams } from "@/entities/statistics/model/se
*/
const SewageReservoirPumpRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>('ST00040001');
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
>(new Map());
const searchParamRef = useRef<SewerReservoirRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
const activeStatsStatusInfo = useMemo(
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
[tabStatsStatusInfoMap, activeTab],
);
const columnInfo = useMemo(() => {
return SewageReservoirPumpInfoListRvw.map((tab: ColumnInfoList) => ({
@@ -39,44 +107,116 @@ const SewageReservoirPumpRvw = () => {
}));
}, []);
const activeStatsCd = activeStatsStatusInfo.statsCd ?? activeTab;
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
if (lastSearchValues == null) return null;
return {
statsYr: lastSearchValues.year ?? "",
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
statsCd: activeStatsCd,
};
}, [activeStatsCd, lastSearchValues]);
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchValues == null) return null;
const statsYr = lastSearchValues.year ?? "";
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
const statsCd = activeStatsCd;
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
statsCd,
instSeCd,
};
}, [activeStatsCd, lastSearchValues, userInfo?.instSeCd]);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab('ST00040001');
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoggedRef.current.clear();
};
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
setActiveTab('ST00040001');
setRowDataMap(new Map());
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: SewerReservoirRequestParams = {
const searchParam: SewerReservoirRequestParams = {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
};
searchParamRef.current = searchParam;
searchDoneRef.current = true;
lazyTabLoggedRef.current.clear();
const response = await statisticsManagementApi.getSewerReservoirList(searchParam);
const response = await fetchSewageReservoirPumpReviewTab(activeTab, searchParam);
if (response?.status === 'success' && response.data) {
const newRowDataMap = new Map<string, Record<string, string | number>[]>();
newRowDataMap.set('ST00040001', toGridRows(response.data as object[]));
setRowDataMap(newRowDataMap);
if (response?.status === "success" && response.data) {
const ssi = extractStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, ssi ?? {});
return next;
});
const listData = getListDataByTab(activeTab, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(listData));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
setTabStatsStatusInfoMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
setTabStatsStatusInfoMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
@@ -85,38 +225,119 @@ const SewageReservoirPumpRvw = () => {
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const existingAData = rowDataMap.get('ST00040001');
const activeTabData = rowDataMap.get(tabKey);
const hasAData = Array.isArray(existingAData) && existingAData.length > 0;
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
if (hasAData && !hasActiveTabData && searchParamRef.current) {
try {
setIsLoading(true);
const canLazyLoad =
searchParamRef.current &&
!hasActiveTabData &&
!lazyTabLoggedRef.current.has(tabKey) &&
searchDoneRef.current;
let response;
if (!canLazyLoad) return;
if (tabKey === 'ST00040001') {
response = await statisticsManagementApi.getSewerReservoirList(searchParamRef.current);
} else if (tabKey === 'ST00040002') {
response = await statisticsManagementApi.getPumpStationList(searchParamRef.current);
}
const searchParam = searchParamRef.current;
if (!searchParam) return;
if (response?.status === 'success' && response.data) {
setRowDataMap(prev => {
const next = new Map(prev);
next.set(tabKey, toGridRows(response.data as object[]));
return next;
});
} else if (response) {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
try {
setIsLoading(true);
const response = await fetchSewageReservoirPumpReviewTab(tabKey, searchParam);
lazyTabLoggedRef.current.add(tabKey);
if (response?.status === "success" && response.data) {
const ssi = extractStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, ssi ?? {});
return next;
});
const listData = getListDataByTab(tabKey, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(tabKey, toGridRows(listData));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: row?.splmntOpnnCn ?? "",
wrterOpnnCn: row?.wrterOpnnCn ?? "",
});
return next;
});
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
});
return next;
});
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
@@ -124,14 +345,19 @@ const SewageReservoirPumpRvw = () => {
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={() => { }} />
<ApproveButton approveFunc={() => { }} />
<ReturnButton returnFunc={() => { }} />
<SaveButton saveFunc={() => { }} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
</div>
)}
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -141,16 +367,87 @@ const SewageReservoirPumpRvw = () => {
</div>
)}
<TabDataGrid
columnInfo={columnInfo}
isExcelImport={false}
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
export default SewageReservoirPumpRvw;
export default SewageReservoirPumpRvw;
@@ -1,54 +1,432 @@
import { useState } from "react";
import { useMemo, useRef, useState } from "react";
import { TabDataGrid } from "@rsone/components";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
extractSewerageFinanceReviewList,
extractSewerageFinanceReviewStatsStatusInfo,
mapSewerageFinanceReviewRows,
type SewerageFinanceReviewRequestParams,
} from "@/entities/statistics/model/statistics-sewerage-finance-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewerageFinanceInfoListRvw } from "@/components/util/column/review/SewerageFinanceRvwColumn";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
const firstTabKey = SewerageFinanceInfoListRvw[0]?.tabKey ?? "ST00090001";
// 하수도 통계 관리 09. 하수도 재정
async function fetchSewerageFinanceReviewTab(
tabKey: string,
searchParam: SewerageFinanceReviewRequestParams,
) {
if (tabKey === "ST00090001") {
return statisticsManagementApi.getSewerageFinanceSettleReviewList(searchParam);
}
if (tabKey === "ST00090002") {
return statisticsManagementApi.getSewerageChargeReviewList(searchParam);
}
if (tabKey === "ST00090003") {
return statisticsManagementApi.getSewerageUsageFeeReviewList(searchParam);
}
if (tabKey === "ST00090004") {
return statisticsManagementApi.getCauserBurdenReviewList(searchParam);
}
return null;
}
// 하수도 통계 검토 09. 하수도 재정
const SewerageFinanceRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
>(new Map());
const approveFunc = () => {
console.log('approveFunc');
// 승인 로직
const searchParamRef = useRef<SewerageFinanceReviewRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
const activeStatsStatusInfo = useMemo(
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
[tabStatsStatusInfoMap, activeTab],
);
const columnInfo = useMemo(() => {
return SewerageFinanceInfoListRvw.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
}));
}, []);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
if (lastSearchValues == null) return null;
return {
statsYr: lastSearchValues.year ?? "",
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
statsCd: activeStatsStatusInfo.statsCd ?? "",
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchValues == null) return null;
const statsYr = lastSearchValues.year ?? "";
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
const statsCd = activeStatsStatusInfo.statsCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
statsCd,
instSeCd,
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoggedRef.current.clear();
};
const saveFunc = () => {
console.log('submit');
// 저장 로직
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: SewerageFinanceReviewRequestParams = {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
};
searchParamRef.current = searchParam;
searchDoneRef.current = true;
lazyTabLoggedRef.current.clear();
const response = await fetchSewerageFinanceReviewTab(activeTab, searchParam);
if (response?.status === "success" && response.data) {
const ssi = extractSewerageFinanceReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, ssi);
return next;
});
const listData = extractSewerageFinanceReviewList(activeTab, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(mapSewerageFinanceReviewRows(listData)));
return next;
});
} else {
setTabStatsStatusInfoMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
setTabStatsStatusInfoMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const activeTabData = rowDataMap.get(tabKey);
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
const canLazyLoad =
searchParamRef.current &&
!hasActiveTabData &&
!lazyTabLoggedRef.current.has(tabKey) &&
searchDoneRef.current;
if (!canLazyLoad) return;
const searchParam = searchParamRef.current;
if (!searchParam) return;
try {
setIsLoading(true);
const response = await fetchSewerageFinanceReviewTab(tabKey, searchParam);
lazyTabLoggedRef.current.add(tabKey);
if (response?.status === "success" && response.data) {
const ssi = extractSewerageFinanceReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, ssi);
return next;
});
setRowDataMap((prev) => {
const next = new Map(prev);
const listData = extractSewerageFinanceReviewList(tabKey, response.data);
next.set(tabKey, toGridRows(mapSewerageFinanceReviewRows(listData)));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const returnFunc = () => {
console.log('returnFunc');
// 반려
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: row?.splmntOpnnCn ?? "",
wrterOpnnCn: row?.wrterOpnnCn ?? "",
});
return next;
});
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
});
return next;
});
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<>
<SearchForm fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
)}
{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 className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<TabDataGrid columnInfo={SewerageFinanceInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
export default SewerageFinanceRvw;
export default SewerageFinanceRvw;
@@ -18,7 +18,7 @@ import type {
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
@@ -245,6 +245,7 @@ const SeweragePenetrationRateRvw = () => {
rowStartNum={columnInfo.rowStartNum}
rowData={rowData}
setRowData={setRowData}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
<ReviewOpinionDialog
@@ -1,53 +1,433 @@
import { useState } from "react";
import { useMemo, useRef, useState } from "react";
import { TabDataGrid } from "@rsone/components";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
extractSewerageServiceLevelReviewList,
extractSewerageServiceLevelReviewStatsStatusInfo,
mapSewerageServiceLevelReviewRows,
type SewerageServiceLevelReviewRequestParams,
} from "@/entities/statistics/model/statistics-sewerage-service-level-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type {
ColumnConfig,
ColumnInfo,
ColumnInfoList,
} from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewerageServiceLevelInfoListRvw } from "@/components/util/column/review/SewerageServiceLevelRvwColumn";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
// 하수도 통계 관리 13. 하수도 서비스 수준
const firstTabKey = SewerageServiceLevelInfoListRvw[0]?.tabKey ?? "ST00130001";
async function fetchServiceLevelReviewTab(
tabKey: string,
searchParam: SewerageServiceLevelReviewRequestParams,
) {
if (tabKey === "ST00130001") {
return statisticsManagementApi.getSewageComplaintReviewList(searchParam);
}
if (tabKey === "ST00130002") {
return statisticsManagementApi.getResidentFriendlyFacilityReviewList(searchParam);
}
return null;
}
// 하수도 통계 검토 13. 하수도 서비스 수준
const SewerageServiceLevelRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
>(new Map());
const approveFunc = () => {
console.log('approveFunc');
// 승인 로직
const searchParamRef = useRef<SewerageServiceLevelReviewRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoadedRef = useRef<Set<string>>(new Set());
const activeStatsStatusInfo = useMemo(
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
[tabStatsStatusInfoMap, activeTab],
);
const columnInfo = useMemo(() => {
return SewerageServiceLevelInfoListRvw.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
}));
}, []);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
if (lastSearchValues == null) return null;
return {
statsYr: lastSearchValues.year ?? "",
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
statsCd: activeStatsStatusInfo.statsCd ?? "",
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchValues == null) return null;
const statsYr = lastSearchValues.year ?? "";
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
const statsCd = activeStatsStatusInfo.statsCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
statsCd,
instSeCd,
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoadedRef.current.clear();
};
const saveFunc = () => {
console.log('submit');
// 저장 로직
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: SewerageServiceLevelReviewRequestParams = {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
swtrmtFcltNm: values.fctName,
};
searchParamRef.current = searchParam;
searchDoneRef.current = true;
lazyTabLoadedRef.current.clear();
const response = await fetchServiceLevelReviewTab(activeTab, searchParam);
if (response?.status === "success" && response.data) {
const ssi = extractSewerageServiceLevelReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, ssi);
return next;
});
const listData = extractSewerageServiceLevelReviewList(activeTab, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(mapSewerageServiceLevelReviewRows(listData)));
return next;
});
} else {
setTabStatsStatusInfoMap(new Map());
setRowDataMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
setTabStatsStatusInfoMap(new Map());
setRowDataMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const activeTabData = rowDataMap.get(tabKey);
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
const canLazyLoad =
searchParamRef.current &&
!hasActiveTabData &&
!lazyTabLoadedRef.current.has(tabKey) &&
searchDoneRef.current;
if (!canLazyLoad) return;
const searchParam = searchParamRef.current;
if (!searchParam) return;
try {
setIsLoading(true);
const response = await fetchServiceLevelReviewTab(tabKey, searchParam);
lazyTabLoadedRef.current.add(tabKey);
if (response?.status === "success" && response.data) {
const ssi = extractSewerageServiceLevelReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, ssi);
return next;
});
setRowDataMap((prev) => {
const next = new Map(prev);
const listData = extractSewerageServiceLevelReviewList(tabKey, response.data);
next.set(tabKey, toGridRows(mapSewerageServiceLevelReviewRows(listData)));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const returnFunc = () => {
console.log('returnFunc');
// 반려
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: row?.splmntOpnnCn ?? "",
wrterOpnnCn: row?.wrterOpnnCn ?? "",
});
return next;
});
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
});
return next;
});
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<>
<SearchForm fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
)}
{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 className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<TabDataGrid columnInfo={SewerageServiceLevelInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
export default SewerageServiceLevelRvw;
@@ -1,51 +1,436 @@
import { useState } from "react";
import { useMemo, useRef, useState } from "react";
import { TabDataGrid } from "@rsone/components";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
extractWaterReuseReviewList,
extractWaterReuseReviewStatsStatusInfo,
mapWaterReuseReviewRows,
type WaterReuseReviewRequestParams,
} from "@/entities/statistics/model/statistics-water-reuse-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { WaterReuseInfoListRvw } from "@/components/util/column/review/WaterReuseRvwColumn";
import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction";
import { WaterReuseInfoList } from "@/components/util/column/review/WaterReuseRvwColumn";
const firstTabKey = WaterReuseInfoListRvw[0]?.tabKey ?? "ST00100001";
async function fetchWaterReuseReviewTab(
tabKey: string,
searchParam: WaterReuseReviewRequestParams,
) {
if (tabKey === "ST00100001") {
return statisticsManagementApi.getWaterReuseReuseStatusReviewList(searchParam);
}
if (tabKey === "ST00100002") {
return statisticsManagementApi.getWaterReuseGrayWaterReviewList(searchParam);
}
if (tabKey === "ST00100003") {
return statisticsManagementApi.getWaterReuseRainwaterReviewList(searchParam);
}
if (tabKey === "ST00100004") {
return statisticsManagementApi.getWaterReuseRegStatusReviewList(searchParam);
}
if (tabKey === "ST00100005") {
return statisticsManagementApi.getWaterReuseSncnStatusReviewList(searchParam);
}
return null;
}
// 하수도 통계 검토 10. 물 재이용
const WaterReuseRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
>(new Map());
const approveFunc = () => {
console.log('approveFunc');
// 승인 로직
const searchParamRef = useRef<WaterReuseReviewRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoadedRef = useRef<Set<string>>(new Set());
const activeStatsStatusInfo = useMemo(
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
[tabStatsStatusInfoMap, activeTab],
);
const columnInfo = useMemo(() => {
return WaterReuseInfoListRvw.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
}));
}, []);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
if (lastSearchValues == null) return null;
return {
statsYr: lastSearchValues.year ?? "",
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
statsCd: activeStatsStatusInfo.statsCd ?? "",
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchValues == null) return null;
const statsYr = lastSearchValues.year ?? "";
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
const statsCd = activeStatsStatusInfo.statsCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
statsCd,
instSeCd,
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
: "";
const prvn =
statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
const submitIs3 = submit === "3";
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
return {
showReviewOpinionButton: reviewSubmitVisible,
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoadedRef.current.clear();
};
const saveFunc = () => {
console.log('submit');
// 저장 로직
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: WaterReuseReviewRequestParams = {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
swtrmtFcltNm: values.fctName,
};
searchParamRef.current = searchParam;
searchDoneRef.current = true;
lazyTabLoadedRef.current.clear();
const response = await fetchWaterReuseReviewTab(activeTab, searchParam);
if (response?.status === "success" && response.data) {
const ssi = extractWaterReuseReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, ssi);
return next;
});
const listData = extractWaterReuseReviewList(activeTab, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(mapWaterReuseReviewRows(listData)));
return next;
});
} else {
setTabStatsStatusInfoMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
setTabStatsStatusInfoMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const activeTabData = rowDataMap.get(tabKey);
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
const canLazyLoad =
searchParamRef.current &&
!hasActiveTabData &&
!lazyTabLoadedRef.current.has(tabKey) &&
searchDoneRef.current;
if (!canLazyLoad) return;
const searchParam = searchParamRef.current;
if (!searchParam) return;
try {
setIsLoading(true);
const response = await fetchWaterReuseReviewTab(tabKey, searchParam);
lazyTabLoadedRef.current.add(tabKey);
if (response?.status === "success" && response.data) {
const ssi = extractWaterReuseReviewStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, ssi);
return next;
});
setRowDataMap((prev) => {
const next = new Map(prev);
const listData = extractWaterReuseReviewList(tabKey, response.data);
next.set(tabKey, toGridRows(mapWaterReuseReviewRows(listData)));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const returnFunc = () => {
console.log('returnFunc');
// 반려
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: row?.splmntOpnnCn ?? "",
wrterOpnnCn: row?.wrterOpnnCn ?? "",
});
return next;
});
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
});
return next;
});
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<>
<SearchForm fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
)}
{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 className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={reviewRowTypeCurrCellStyleCallback}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<TabDataGrid columnInfo={WaterReuseInfoList} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
export default WaterReuseRvw;
export default WaterReuseRvw;
@@ -3,7 +3,6 @@ import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import CellColorInfo from "./CellColorInfo";
@@ -118,7 +118,7 @@ const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<b
} else {
lastSearchedValuesRef.current = null;
}
} catch (error) {
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
lastSearchedValuesRef.current = null;
} finally {
@@ -137,7 +137,7 @@ const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<b
try {
setIsLoading(true);
await loadGridData(tabKey, values);
} catch (error) {
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
@@ -3,7 +3,6 @@ import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import CellColorInfo from "./CellColorInfo";
@@ -1,118 +1,298 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 07.분뇨처리 시설
const rawServiceLevelList = [
{
tabKey: 'a',
tabLabel: '민원',
rowStartNum: 2,
fixedColumns: 2,
columns: [
/* 1. 기본 정보 */
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
import type { GridView } from "@/components/type/DataGrid.type";
import {
footerDecimal1ArrayByDivision,
footerDecimal3ArrayByDivision,
footerIntArrayByDivision,
} from "@/components/util/grid/footerSummary";
/* 2. 하수처리 인구 */
{ name: 'POPLTN_SM', fieldName: '하수처리인구(명)', header: { text: '하수처리인구\n(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => {
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
/* 3. 민원유형 (단위: 건) */
{ name: 'CVPL_CM', fieldName: '총계(민원)', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_DCWTR_QLTWTR', fieldName: '악취', header: { text: '악취' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_BSML', fieldName: '하수관로', header: { text: '하수관로' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_FIVW', fieldName: '배수설비', header: { text: '배수설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_POLLUTN_ACDNT', fieldName: '맨홀', header: { text: '맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_SSOS', fieldName: '정화조', header: { text: '정화조' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_CHRGE', fieldName: '하수도요금', header: { text: '하수도요금' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_ETC', fieldName: '기타(민원)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
/* 4. 성과 지표 및 비고 */
{ name: 'STDR_POPLTN_PER_CVPL', fieldName: '기준인구당 민원건수(건/10,000명)', header: { text: '기준인구당\n민원건수\n(건/10,000명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } }
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN',
'POPLTN_SM',
{
name: 'complaintTypeGroup',
header: { text: '민원유형(건)' },
items: [
'CVPL_CM',
'CVPL_DCWTR_QLTWTR',
'CVPL_BSML',
'CVPL_FIVW',
'CVPL_POLLUTN_ACDNT',
'CVPL_SSOS',
'CVPL_CHRGE',
'CVPL_ETC'
]
},
'STDR_POPLTN_PER_CVPL',
'RMK'
]
},
{
tabKey: 'b',
tabLabel: '주민친화시설',
rowStartNum: 2,
fixedColumns: 5,
columns: [
/* 1. 기본 및 시설 정보 */
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'BSC_FCT_NM', fieldName: '처리장명', header: { text: '처리장명' } },
{ name: 'FCT_CPC', fieldName: '시설용량(㎥/일)', header: { text: '시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
/* 2. 처리장 전체 면적 */
{ name: 'PRCPLT_AR', fieldName: '처리장 면적(㎡)', header: { text: '처리장\n면적(㎡)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 3. 주민친화시설 면적 상세 (단위: ㎡) */
{ name: 'TOTAL_AR', fieldName: '총계(시설)', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RESTRM_AR', fieldName: '휴게시설', header: { text: '휴게시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CHILDPLAY_AR', fieldName: '어린이놀이공간', header: { text: '어린이놀이공간' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ALSFC_AR', fieldName: '체력단련시설', header: { text: '체력단련시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'PLGRND_AR', fieldName: '운동시설', header: { text: '운동시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'PRKPLCE_AR', fieldName: '주차장', header: { text: '주차장' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CTLSTT_AR', fieldName: '교육 및 문화시설', header: { text: '교육 및\n문화시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'NRPRK_AR', fieldName: '습지조성(생태공원)', header: { text: '습지조성\n(생태공원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ETC_AR', fieldName: '기타(시설)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
/* 4. 비고 */
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } }
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'BSC_FCT_NM',
'FCT_CPC',
'YR_GBN',
'PRCPLT_AR',
{
name: 'friendlyFacilityGroup',
header: { text: '주민친화시설 면적(㎡)' },
items: [
'TOTAL_AR',
'RESTRM_AR',
'CHILDPLAY_AR',
'ALSFC_AR',
'PLGRND_AR',
'PRKPLCE_AR',
'CTLSTT_AR',
'NRPRK_AR',
'ETC_AR'
]
},
'RMK'
]
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, "displayYear");
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
if (divisionValues.length <= divisionIndex) {
return withSuffix ? "합계" : "";
}
const label = divisionValues[divisionIndex] ?? "";
return withSuffix ? `${label}합계` : label;
};
const divisionFooterCallbacks = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
];
const divisionFooterLabelOnly = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
];
export const ExcretaTreatmentFacilityInfoList = rawServiceLevelList.map(tab => ({
// 하수도 통계 검토 07. 분뇨처리시설
const rawExcretaTreatmentFacilityList = [
{
tabKey: "ST00070001",
tabLabel: "분뇨처리시설",
rowStartNum: 3,
fixedColumns: 4,
columns: [
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
mergeCells: true,
sortdisable: true,
footer: divisionFooterCallbacks,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
mergeCells: true,
sortdisable: true,
},
{
name: "displayYear",
fieldName: "displayYear",
header: { text: "구분" },
sortdisable: true,
footer: divisionFooterLabelOnly,
},
{ name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" } },
{ name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "소재지" } },
{ name: "telNo", fieldName: "telNo", header: { text: "전화번호" } },
{ name: "cmptnStrtYmd", fieldName: "cmptnStrtYmd", header: { text: "준공일" } },
{ name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시일" } },
{
name: "wholPjtcoAmt",
fieldName: "wholPjtcoAmt",
header: { text: "사업비\n(백만원)" },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
},
{
name: "prcsFcltCpct",
fieldName: "prcsFcltCpct",
header: { text: "시설용량\n(㎥/일)" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
},
{
name: "cpct",
fieldName: "cpct",
header: { text: "처리량\n(㎥/일)" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
},
{
name: "oprtngDayCnt",
fieldName: "oprtngDayCnt",
header: { text: "가동일수\n(일/년)" },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
},
{ name: "prcsMthdExpln", fieldName: "prcsMthdExpln", header: { text: "처리공법" } },
{ name: "linkTmplntCn", fieldName: "linkTmplntCn", header: { text: "연계처리장명" } },
{
name: "linkPrcsQnt",
fieldName: "linkPrcsQnt",
header: { text: "연계처리량\n(㎥/일)" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
},
{
name: "inflwBodWqVl",
fieldName: "inflwBodWqVl",
header: { text: "BOD" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
{
name: "inflwTocWqVl",
fieldName: "inflwTocWqVl",
header: { text: "TOC" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
{
name: "inflwSsWqVl",
fieldName: "inflwSsWqVl",
header: { text: "SS" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
{
name: "inflwTnWqVl",
fieldName: "inflwTnWqVl",
header: { text: "T-N" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
{
name: "inflwTpWqVl",
fieldName: "inflwTpWqVl",
header: { text: "T-P" },
footer: footerDecimal3ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.000",
},
{
name: "inflwClfgCnt",
fieldName: "inflwClfgCnt",
header: { text: "대장균\n군수" },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
},
{
name: "dschgBodWqVl",
fieldName: "dschgBodWqVl",
header: { text: "BOD" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
{
name: "dschgTocWqVl",
fieldName: "dschgTocWqVl",
header: { text: "TOC" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
{
name: "dschgSsWqVl",
fieldName: "dschgSsWqVl",
header: { text: "SS" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
{
name: "dschgTnWqVl",
fieldName: "dschgTnWqVl",
header: { text: "T-N" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
{
name: "dschgTpWqVl",
fieldName: "dschgTpWqVl",
header: { text: "T-P" },
footer: footerDecimal3ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.000",
},
{
name: "dschgClfgCnt",
fieldName: "dschgClfgCnt",
header: { text: "대장균\n군수" },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
},
{ name: "wtsysNm", fieldName: "wtsysNm", header: { text: "수계" } },
{ name: "trbtrNm", fieldName: "trbtrNm", header: { text: "지류" } },
{ name: "msirbCn", fieldName: "msirbCn", header: { text: "중권역" } },
{ name: "ssirbCn", fieldName: "ssirbCn", header: { text: "소권역" } },
{ name: "operMthdCdCn", fieldName: "operMthdCdCn", header: { text: "운영주체\n(자체/공기업/\n민간위탁)" } },
{
name: "wholNope",
fieldName: "wholNope",
header: { text: "직원총수\n(명)" },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
},
{
name: "sftyAcdntEvntcsCnt",
fieldName: "sftyAcdntEvntcsCnt",
header: { text: "안전사고\n건수\n(건/인/년)" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
],
columnLayout: [
{
name: "areaGroup",
header: { text: "행정구역" },
items: [
{ column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
"sggNm",
],
},
"displayYear",
"fcltNm",
"fcltPstnCn",
"telNo",
"cmptnStrtYmd",
"oprtngStrtYmd",
"wholPjtcoAmt",
"prcsFcltCpct",
"cpct",
"oprtngDayCnt",
"prcsMthdExpln",
"linkTmplntCn",
"linkPrcsQnt",
{
name: "inflwWqGroup",
header: { text: "유입수질(mg/L, 개/mL)" },
items: [
"inflwBodWqVl",
"inflwTocWqVl",
"inflwSsWqVl",
"inflwTnWqVl",
"inflwTpWqVl",
"inflwClfgCnt",
],
},
{
name: "dschgWqGroup",
header: { text: "방류수질(mg/L, 개/mL)" },
items: [
"dschgBodWqVl",
"dschgTocWqVl",
"dschgSsWqVl",
"dschgTnWqVl",
"dschgTpWqVl",
"dschgClfgCnt",
],
},
{
name: "wtrAreaGroup",
header: { text: "방류수역" },
items: ["wtsysNm", "trbtrNm", "msirbCn", "ssirbCn"],
},
"operMthdCdCn",
"wholNope",
"sftyAcdntEvntcsCnt",
],
},
];
export const ExcretaTreatmentFacilityInfoList = rawExcretaTreatmentFacilityList.map((tab) => ({
...tab,
columns: tab.columns.map(col => ({
columns: tab.columns.map((col) => ({
...col,
editable: false
}))
}));
editable: false,
styleName: "rg-cell-readonly",
})),
}));
@@ -1,139 +1,336 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 08.개인하수처리시설
import type { GridView } from "@/components/type/DataGrid.type";
import {
footerDecimal1ArrayByDivision,
footerDecimal2ArrayByDivision,
footerIntArrayByDivision,
} from "@/components/util/grid/footerSummary";
const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => {
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, "displayYear");
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
if (divisionValues.length <= divisionIndex) {
return withSuffix ? "합계" : "";
}
const label = divisionValues[divisionIndex] ?? "";
return withSuffix ? `${label}합계` : label;
};
const divisionFooterCallbacks = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
];
const divisionFooterLabelOnly = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
];
const reviewAddressColumns = [
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
mergeCells: true,
sortdisable: true,
footer: divisionFooterCallbacks,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
mergeCells: true,
sortdisable: true,
},
{
name: "displayYear",
fieldName: "displayYear",
header: { text: "구분" },
sortdisable: true,
footer: divisionFooterLabelOnly,
},
];
const reviewAddressLayout = {
name: "areaGroup",
header: { text: "행정구역" },
items: [
{ column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
"sggNm",
],
};
const intCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
});
// 하수도 통계 검토 08. 개인하수처리시설
const rawPrivateSewageList = [
{
tabKey: 'a',
tabLabel: '처리용량별 오수처리시설 현황',
tabKey: "ST00080001",
tabLabel: "처리용량별 오수처리시설",
rowStartNum: 3,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'STTY_SM', fieldName: '총계', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_INNER_SM', fieldName: '계(처리구역 내)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_INNER_2', fieldName: '2이하(처리구역 내)', header: { text: '2이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_INNER_3', fieldName: '3~10(처리구역 내)', header: { text: '3~10' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_INNER_11', fieldName: '11~20(처리구역 내)', header: { text: '11~20' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_INNER_21', fieldName: '21~50(처리구역 내)', header: { text: '21~50' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_INNER_51', fieldName: '51~100(처리구역 내)', header: { text: '51~100' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_INNER_101', fieldName: '101~500(처리구역 내)', header: { text: '101~500' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_INNER_501', fieldName: '501이상(처리구역 내)', header: { text: '501이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_OUTER_SM', fieldName: '계(처리구역 외)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_OUTER_2', fieldName: '2이하(처리구역 외)', header: { text: '2이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_OUTER_3', fieldName: '3~10(처리구역 외)', header: { text: '3~10' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_OUTER_11', fieldName: '11~20(처리구역 외)', header: { text: '11~20' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_OUTER_21', fieldName: '21~50(처리구역 외)', header: { text: '21~50' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_OUTER_51', fieldName: '51~100(처리구역 외)', header: { text: '51~100' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_OUTER_101', fieldName: '101~500(처리구역 외)', header: { text: '101~500' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'STTY_ZONE_OUTER_501', fieldName: '501이상(처리구역 외)', header: { text: '501이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
...reviewAddressColumns,
intCol("wholDrtwPrcsFcltCnt", "총계"),
intCol("wholStaaCnt", "계"),
intCol("step1StaaPrcsQnt", "2이하"),
intCol("step2StaaPrcsQnt", "3~10"),
intCol("step3StaaPrcsQnt", "11~20"),
intCol("step4StaaPrcsQnt", "21~50"),
intCol("step5StaaPrcsQnt", "51~100"),
intCol("step6StaaPrcsQnt", "101~500"),
intCol("step7StaaPrcsQnt", "501이상"),
intCol("staaExcpWhol", "계"),
intCol("step1StaaExcpPrcsQnt", "2이하"),
intCol("step2StaaExcpPrcsQnt", "3~10"),
intCol("step3StaaExcpPrcsQnt", "11~20"),
intCol("step4StaaExcpPrcsQnt", "21~50"),
intCol("step5StaaExcpPrcsQnt", "51~100"),
intCol("step6StaaExcpPrcsQnt", "101~500"),
intCol("step7StaaExcpPrcsQnt", "501이상"),
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN', 'STTY_SM',
reviewAddressLayout,
"displayYear",
"wholDrtwPrcsFcltCnt",
{
name: 'individualGroup', header: { text: '오수처리시설' },
name: "privateSewageFacilityGroup",
header: { text: "오수처리시설" },
items: [
{
name: 'innerGroup', header: { text: '하수처리구역 내(㎥/일)' },
items: ['STTY_ZONE_INNER_SM', 'STTY_ZONE_INNER_2', 'STTY_ZONE_INNER_3', 'STTY_ZONE_INNER_11', 'STTY_ZONE_INNER_21', 'STTY_ZONE_INNER_51', 'STTY_ZONE_INNER_101', 'STTY_ZONE_INNER_501']
name: "insideGroup",
header: { text: "하수처리구역 내(㎥/일)" },
items: [
"wholStaaCnt",
"step1StaaPrcsQnt",
"step2StaaPrcsQnt",
"step3StaaPrcsQnt",
"step4StaaPrcsQnt",
"step5StaaPrcsQnt",
"step6StaaPrcsQnt",
"step7StaaPrcsQnt",
],
},
{
name: 'outerGroup', header: { text: '하수처리구역 외(㎥/일)' },
items: ['STTY_ZONE_OUTER_SM', 'STTY_ZONE_OUTER_2', 'STTY_ZONE_OUTER_3', 'STTY_ZONE_OUTER_11', 'STTY_ZONE_OUTER_21', 'STTY_ZONE_OUTER_51', 'STTY_ZONE_OUTER_101', 'STTY_ZONE_OUTER_501']
}
]
name: "outsideGroup",
header: { text: "하수처리구역 외(㎥/일)" },
items: [
"staaExcpWhol",
"step1StaaExcpPrcsQnt",
"step2StaaExcpPrcsQnt",
"step3StaaExcpPrcsQnt",
"step4StaaExcpPrcsQnt",
"step5StaaExcpPrcsQnt",
"step6StaaExcpPrcsQnt",
"step7StaaExcpPrcsQnt",
],
},
],
},
'RMK'
]
"rmrk",
],
},
{
tabKey: 'b',
tabLabel: '정화조현황',
tabKey: "ST00080002",
tabLabel: "정화조현황",
rowStartNum: 4,
fixedColumns: 1,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'TOT_FCLTY_CNT', fieldName: '총 시설 수(A+D)', header: { text: '총 시설 수\n(A+D)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_SM', fieldName: '총계 A=(B+C)/2', header: { text: '총계\nA=(B+C)/2' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_NPFL', fieldName: '자연유하', header: { text: '자연유하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_ENFCT_PUMP', fieldName: '강제펌핑식', header: { text: '강제펌핑식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_TFM', fieldName: '살수여상', header: { text: '살수여상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_ARTM', fieldName: '폭기방법', header: { text: '폭기방법' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_CTARM', fieldName: '접촉폭기', header: { text: '접촉폭기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_ETC', fieldName: '기타(방식)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_PROC_CPC_SM', fieldName: '계(C)', header: { text: '계(C)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_PROC_CPC_1', fieldName: '10인조 이하', header: { text: '10인조 이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_PROC_CPC_2', fieldName: '11~20', header: { text: '11~20' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_PROC_CPC_3', fieldName: '21~30', header: { text: '21~30' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_PROC_CPC_4', fieldName: '31~50', header: { text: '31~50' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_PROC_CPC_5', fieldName: '51~199', header: { text: '51~199' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_PROC_CPC_6', fieldName: '200~499', header: { text: '200~499' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_PROC_CPC_7', fieldName: '500~999', header: { text: '500~999' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SPZI_PROC_CPC_8', fieldName: '1,000이상', header: { text: '1,000이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } }
...reviewAddressColumns,
intCol("grssFcltCnt", "총 시설 수\n(A+D)"),
intCol("staaInsdWhol", "총계\nA=(B+C)/2"),
intCol("staaInsdPrcsCpctByWhol", "계(C)"),
intCol("staaInsdSttkNprsflCnt", "자연유하"),
intCol("step1StaaInsdSttkFrcCnt", "강제펌핑식"),
intCol("staaInsdSprktpFltrtbCnt", "살수여상"),
intCol("staaInsdAertMthdCnt", "폭기방법"),
intCol("staaInsdCntcAertCnt", "접촉폭기"),
intCol("staaInsdEtcCnt", "기타"),
intCol("step1StaaInsdNope", "10인조\n이하"),
intCol("step2StaaInsdNope", "11~20"),
intCol("step3StaaInsdNope", "21~30"),
intCol("step4StaaInsdNope", "31~50"),
intCol("step5StaaInsdNope", "51~199"),
intCol("step6StaaInsdNope", "200~499"),
intCol("step7StaaInsdNope", "500~999"),
intCol("step8StaaInsdNope", "1,000이상"),
intCol("staaExcpWhol", "총계\nD=(E+F)/2"),
intCol("staaExcpPrcsCpctByWhol", "계(F)"),
intCol("staaExcpSttkNprsflCnt", "자연유하"),
intCol("step1StaaExcpSttkFrcCnt", "강제펌핑식"),
intCol("staaExcpSprktpFltrtbCnt", "살수여상"),
intCol("staaExcpAertMthdCnt", "폭기방법"),
intCol("staaExcpCntcAertCnt", "접촉폭기"),
intCol("staaExcpEtcCnt", "기타"),
intCol("step1StaaExcpNope", "10인조\n이하"),
intCol("step2StaaExcpNope", "11~20"),
intCol("step3StaaExcpNope", "21~30"),
intCol("step4StaaExcpNope", "31~50"),
intCol("step5StaaExcpNope", "51~199"),
intCol("step6StaaExcpNope", "200~499"),
intCol("step7StaaExcpNope", "500~999"),
intCol("step8StaaExcpNope", "1,000이상"),
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN', 'TOT_FCLTY_CNT',
reviewAddressLayout,
"displayYear",
"grssFcltCnt",
{
name: 'innerMainGroup', header: { text: '하수처리구역 내' },
name: "insideSewerGroup",
header: { text: "하수처리구역 내" },
items: [
'SPZI_SM',
"staaInsdWhol",
{
name: 'innerMethodGroup', header: { text: '처리방식별' },
items: [{ name: 'innerTankGroup', header: { text: '부패탱크' }, items: ['SPZI_NPFL', 'SPZI_ENFCT_PUMP'] }, 'SPZI_TFM', 'SPZI_ARTM', 'SPZI_CTARM', 'SPZI_ETC']
name: "insdBGroup",
header: { text: "계(B)" },
items: [
"staaInsdSttkNprsflCnt",
"step1StaaInsdSttkFrcCnt",
"staaInsdSprktpFltrtbCnt",
"staaInsdAertMthdCnt",
"staaInsdCntcAertCnt",
"staaInsdEtcCnt",
],
},
{
name: 'innerCpcGroup', header: { text: '처리용량별' },
items: ['SPZI_PROC_CPC_SM', 'SPZI_PROC_CPC_1', 'SPZI_PROC_CPC_2', 'SPZI_PROC_CPC_3', 'SPZI_PROC_CPC_4', 'SPZI_PROC_CPC_5', 'SPZI_PROC_CPC_6', 'SPZI_PROC_CPC_7', 'SPZI_PROC_CPC_8']
}
]
name: "insdCGroup",
header: { text: "처리용량별" },
items: [
"staaInsdPrcsCpctByWhol",
"step1StaaInsdNope",
"step2StaaInsdNope",
"step3StaaInsdNope",
"step4StaaInsdNope",
"step5StaaInsdNope",
"step6StaaInsdNope",
"step7StaaInsdNope",
"step8StaaInsdNope",
],
},
],
},
'RMK'
]
{
name: "outsideSewerGroup",
header: { text: "하수처리구역 외" },
items: [
"staaExcpWhol",
{
name: "excpEGroup",
header: { text: "계(E)" },
items: [
"staaExcpSttkNprsflCnt",
"step1StaaExcpSttkFrcCnt",
"staaExcpSprktpFltrtbCnt",
"staaExcpAertMthdCnt",
"staaExcpCntcAertCnt",
"staaExcpEtcCnt",
],
},
{
name: "excpFGroup",
header: { text: "처리용량별" },
items: [
"staaExcpPrcsCpctByWhol",
"step1StaaExcpNope",
"step2StaaExcpNope",
"step3StaaExcpNope",
"step4StaaExcpNope",
"step5StaaExcpNope",
"step6StaaExcpNope",
"step7StaaExcpNope",
"step8StaaExcpNope",
],
},
],
},
"rmrk",
],
},
{
tabKey: 'c',
tabLabel: '개인하수도 관리지역 지정공고 현황',
tabKey: "ST00080003",
tabLabel: "개인하수도 관리지역 지정공고 현황",
rowStartNum: 3,
fixedColumns: 1,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'MGMT_AR', fieldName: '공고지역(개소)', header: { text: '공고지역\n(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'APPN_SCOPE', fieldName: '지정범위(㎢)', header: { text: '지정범위\n(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'WRRTN_CNT', fieldName: '개소수(정화조)', header: { text: '개소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'WRRTN_CPC', fieldName: '처리용량(인/일)', header: { text: '처리용량\n(인/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'STTY_CNT', fieldName: '개소수(오수시설)', header: { text: '개소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'STTY_CPC', fieldName: '처리용량(㎥/일)', header: { text: '처리용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
...reviewAddressColumns,
intCol("indvSwagePbancRgnCnt", "공고지역\n(개소)"),
{
name: "indvSwageDsgnScpLen",
fieldName: "indvSwageDsgnScpLen",
header: { text: "지정범위\n(㎢)" },
footer: footerDecimal2ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
},
intCol("spctnkGrssFcltCnt", "총시설수"),
{
name: "spctnkQnt",
fieldName: "spctnkQnt",
header: { text: "처리용량\n(인/일)" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
},
intCol("drtwPrcsFcltGrssFcltCnt", "총시설수"),
{
name: "drtwPrcsFcltQnt",
fieldName: "drtwPrcsFcltQnt",
header: { text: "처리용량\n(㎥/일)" },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
},
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN',
reviewAddressLayout,
"displayYear",
{
name: 'mgmtMainGroup', header: { text: '개인하수도관리지역공고현황\n(하수도법 제34조의2)' },
name: "groupNotice",
header: { text: "개인하수도관리지역공고현황 (하수도법제34조의2)" },
items: [
'MGMT_AR', 'APPN_SCOPE',
{ name: 'septictankGroup', header: { text: '정화조' }, items: ['WRRTN_CNT', 'WRRTN_CPC'] },
{ name: 'sewageFacGroup', header: { text: '오수시설' }, items: ['STTY_CNT', 'STTY_CPC'] }
]
"indvSwagePbancRgnCnt",
"indvSwageDsgnScpLen",
{
name: "septicTankGroup",
header: { text: "정화조" },
items: ["spctnkGrssFcltCnt", "spctnkQnt"],
},
{
name: "sewageTreatmentGroup",
header: { text: "오수처리시설" },
items: ["drtwPrcsFcltGrssFcltCnt", "drtwPrcsFcltQnt"],
},
],
},
'RMK'
]
}
"rmrk",
],
},
];
export const PrivateSewageFacilityInfoListRvw = rawPrivateSewageList.map(tab => ({
export const PrivateSewageFacilityInfoListRvw = rawPrivateSewageList.map((tab) => ({
...tab,
columns: tab.columns.map(col => ({
columns: tab.columns.map((col) => ({
...col,
editable: false
}))
}));
editable: false,
styleName: "rg-cell-readonly",
})),
}));
@@ -1,135 +1,276 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 12. 하수 및 분뇨 관련 산업
const rawExcretaRelatedList = [
{
tabKey: 'a',
tabLabel: '공공기관 하수분야',
rowStartNum: 2,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'OCGNZ', fieldName: '직제', header: { text: '직제' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'TCHNLGY_JSSFC_SM', fieldName: '계(기술직)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'TCHNLGY_ETC', fieldName: '기타(기술직)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ETC', fieldName: '기타', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'OCGNZ', 'YR_GBN', 'TOT_WTR_QRM', 'ADMINIST_JSSFC',
{
name: 'techJobGroup',
header: { text: '기술직' },
items: ['TCHNLGY_JSSFC_SM', 'ENGRK_JSSFC', 'MCHN_JSSFC', 'ELCTY_JSSFC', 'CHMSTRY_JSSFC', 'ENVRN_JSSFC', 'TCHNLGY_ETC']
},
'ETC', 'RMK'
]
},
{
tabKey: 'b',
tabLabel: '공공기관 분뇨분야',
rowStartNum: 2,
fixedColumns: 1,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'OCGNZ', fieldName: '직제', header: { text: '직제' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'TCHNLGY_JSSFC_SM', fieldName: '계(기술직)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'TCHNLGY_ETC', fieldName: '기타(기술직)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ETC', fieldName: '기타', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'OCGNZ', 'YR_GBN', 'TOT_WTR_QRM', 'ADMINIST_JSSFC',
{
name: 'techJobGroup',
header: { text: '기술직' },
items: ['TCHNLGY_JSSFC_SM', 'ENGRK_JSSFC', 'MCHN_JSSFC', 'ELCTY_JSSFC', 'CHMSTRY_JSSFC', 'ENVRN_JSSFC', 'TCHNLGY_ETC']
},
'ETC', 'RMK'
]
},
{
tabKey: 'c',
tabLabel: '분뇨수집·운반업',
rowStartNum: 2,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'ECSCS_SM', fieldName: '업체수(개소)', header: { text: '업체수(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ECSCS_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'VHCLE_SM', fieldName: '계(차량)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'THREE_BELOW', fieldName: '3톤이하', header: { text: '3톤이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'FOUR_BELOW', fieldName: '4.5톤이하', header: { text: '4.5톤이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'EIGHT_BELOW', fieldName: '8톤이하', header: { text: '8톤이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ETC_VHCLE', fieldName: '기타(차량)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // name 중복 방지를 위해 ETC_VHCLE로 수정 권장
{ name: 'COLCT_TRSP_ACMSLT', fieldName: '수집·운반실적(㎥/년)', header: { text: '수집·운반실적\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN', 'ECSCS_SM', 'ECSCS_BSMN',
{
name: 'vehicleGroup',
header: { text: '시설(차량) 확보현황(대수)' },
items: ['VHCLE_SM', 'THREE_BELOW', 'FOUR_BELOW', 'EIGHT_BELOW', 'ETC_VHCLE']
},
'COLCT_TRSP_ACMSLT', 'RMK'
]
},
{
tabKey: 'd',
tabLabel: '개인하수처리관련업',
rowStartNum: 2,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'CNSTRCT_ENTRPS', fieldName: '업소수(시공업)', header: { text: '업소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CNSTRCT_BSMN', fieldName: '종사인원(명)_시공업', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'MNFCTUR_ENTRPS', fieldName: '업소수(제조업)', header: { text: '업소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'MNFCTUR_BSMN', fieldName: '종사인원(명)_제조업', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'MGMT_ENTRPS', fieldName: '업소수(관리업)', header: { text: '업소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'MGMT_BSMN', fieldName: '종사인원(명)_관리업', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } }
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN',
{ name: 'constructGroup', header: { text: '개인하수처리시설 설계ㆍ시공업' }, items: ['CNSTRCT_ENTRPS', 'CNSTRCT_BSMN'] },
{ name: 'manufactureGroup', header: { text: '개인하수처리시설 제조업' }, items: ['MNFCTUR_ENTRPS', 'MNFCTUR_BSMN'] },
{ name: 'managementGroup', header: { text: '개인하수처리시설 관리업' }, items: ['MGMT_ENTRPS', 'MGMT_BSMN'] },
'RMK'
]
}
import type { GridView } from "@/components/type/DataGrid.type";
import {
footerDecimal1ArrayByDivision,
footerIntArrayByDivision,
} from "@/components/util/grid/footerSummary";
const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => {
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, "displayYear");
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
if (divisionValues.length <= divisionIndex) {
return withSuffix ? "합계" : "";
}
const label = divisionValues[divisionIndex] ?? "";
return withSuffix ? `${label}합계` : label;
};
const divisionFooterCallbacks = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
];
export const SewageExcretaRelatedInfoListRvw = rawExcretaRelatedList.map(tab => ({
...tab,
columns: tab.columns.map(col => ({
...col,
editable: false
}))
}));
const divisionFooterLabelOnly = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
];
const reviewAddressColumns = [
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
mergeCells: true,
sortdisable: true,
footer: divisionFooterCallbacks,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
mergeCells: true,
sortdisable: true,
},
// {
// name: "admdstNm",
// fieldName: "admdstNm",
// header: { text: "행정구역명" },
// mergeCells: true,
// sortdisable: true,
// },
{
name: "displayYear",
fieldName: "displayYear",
header: { text: "구분" },
sortdisable: true,
footer: divisionFooterLabelOnly,
},
];
const reviewAddressLayout = {
name: "areaGroup",
header: { text: "행정구역" },
items: [
{ column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
"sggNm",
// "admdstNm",
],
};
const textCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
});
const intCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
});
const decimal1Col = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
});
// 하수도 통계 검토 12. 하수 및 분뇨 관련 산업
const rawExcretaRelatedList = [
{
tabKey: "ST00120001",
tabLabel: "공공기관 하수분야",
rowStartNum: 2,
fixedColumns: 2,
columns: [
...reviewAddressColumns,
textCol("ognzNm", "직제"),
intCol("empGrssCnt", "직원총수"),
intCol("pbadmsNope", "행정직"),
intCol("techWholNope", "계"),
intCol("engrkCnt", "토목"),
intCol("mchnCnt", "기계"),
intCol("prvtmCnt", "전기"),
intCol("chmengCnt", "화공"),
intCol("envCnt", "환경"),
intCol("enjbEtcCnt", "기타"),
intCol("etcCnt", "기타"),
textCol("rmrk", "비고"),
],
columnLayout: [
reviewAddressLayout,
"displayYear",
"ognzNm",
"empGrssCnt",
"pbadmsNope",
{
name: "techJobGroup",
header: { text: "기술직" },
items: [
"techWholNope",
"engrkCnt",
"mchnCnt",
"prvtmCnt",
"chmengCnt",
"envCnt",
"enjbEtcCnt",
],
},
"etcCnt",
"rmrk",
],
},
{
tabKey: "ST00120002",
tabLabel: "공공기관 분뇨분야",
rowStartNum: 2,
fixedColumns: 2,
columns: [
...reviewAddressColumns,
textCol("ognzNm", "직제"),
intCol("empGrssCnt", "직원총수"),
intCol("pbadmsNope", "행정직"),
intCol("techWholNope", "계"),
intCol("engrkCnt", "토목"),
intCol("mchnCnt", "기계"),
intCol("prvtmCnt", "전기"),
intCol("chmengCnt", "화공"),
intCol("envCnt", "환경"),
intCol("enjbEtcCnt", "기타"),
intCol("etcCnt", "기타"),
textCol("rmrk", "비고"),
],
columnLayout: [
reviewAddressLayout,
"displayYear",
"ognzNm",
"empGrssCnt",
"pbadmsNope",
{
name: "techJobGroup",
header: { text: "기술직" },
items: [
"techWholNope",
"engrkCnt",
"mchnCnt",
"prvtmCnt",
"chmengCnt",
"envCnt",
"enjbEtcCnt",
],
},
"etcCnt",
"rmrk",
],
},
{
tabKey: "ST00120003",
tabLabel: "분뇨수집·운반업",
rowStartNum: 2,
fixedColumns: 2,
columns: [
...reviewAddressColumns,
intCol("bzentyCnt", "업체수(개소)"),
intCol("empNope", "종사인원(명)"),
intCol("fcltSecrPrstCnt", "계"),
intCol("n3TonUdrCnt", "3톤이하"),
intCol("n4TonUdrCnt", "4.5톤이하"),
intCol("n8TonUdrCnt", "8톤이하"),
intCol("etcCnt", "기타"),
decimal1Col("clctCnvncPrfmncQnt", "수집·운반실적\n(㎥/년)"),
textCol("rmrk", "비고"),
],
columnLayout: [
reviewAddressLayout,
"displayYear",
"bzentyCnt",
"empNope",
{
name: "vehicleGroup",
header: { text: "시설(차량) 확보현황(대수)" },
items: [
"fcltSecrPrstCnt",
"n3TonUdrCnt",
"n4TonUdrCnt",
"n8TonUdrCnt",
"etcCnt",
],
},
"clctCnvncPrfmncQnt",
"rmrk",
],
},
{
tabKey: "ST00120004",
tabLabel: "개인하수처리관련업",
rowStartNum: 2,
fixedColumns: 2,
columns: [
...reviewAddressColumns,
intCol("designMdsrCnt", "업소수"),
intCol("designMdsrNope", "종사인원(명)"),
intCol("mfbizCnt", "업소수"),
intCol("mfbizMdsrNope", "종사인원(명)"),
intCol("mngCnt", "업소수"),
intCol("mngMdsrNope", "종사인원(명)"),
textCol("rmrk", "비고"),
],
columnLayout: [
reviewAddressLayout,
"displayYear",
{
name: "constructGroup",
header: { text: "개인하수처리시설 설계ㆍ시공업" },
items: ["designMdsrCnt", "designMdsrNope"],
},
{
name: "manufactureGroup",
header: { text: "개인하수처리시설 제조업" },
items: ["mfbizCnt", "mfbizMdsrNope"],
},
{
name: "managementGroup",
header: { text: "개인하수처리시설 관리업" },
items: ["mngCnt", "mngMdsrNope"],
},
"rmrk",
],
},
];
export const SewageExcretaRelatedInfoListRvw = rawExcretaRelatedList.map((tab) => ({
...tab,
columns: tab.columns.map((col) => ({
...col,
editable: false,
styleName: "rg-cell-readonly",
})),
}));
/** @deprecated `SewageExcretaRelatedInfoListRvw` 사용 */
export const SewageExcretaRelatedInfoList = SewageExcretaRelatedInfoListRvw;
@@ -1,199 +1,385 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 11. 하수 및 분뇨 찌꺼기(슬러지) 처리
import type { GridView } from "@/components/type/DataGrid.type";
import {
footerDecimal1ArrayByDivision,
footerIntArrayByDivision,
} from "@/components/util/grid/footerSummary";
const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => {
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, "displayYear");
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
if (divisionValues.length <= divisionIndex) {
return withSuffix ? "합계" : "";
}
const label = divisionValues[divisionIndex] ?? "";
return withSuffix ? `${label}합계` : label;
};
const divisionFooterCallbacks = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
];
const divisionFooterLabelOnly = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
];
const reviewFacilityAddressColumns = [
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
mergeCells: true,
sortdisable: true,
footer: divisionFooterCallbacks,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
mergeCells: true,
sortdisable: true,
},
{
name: "displayYear",
fieldName: "displayYear",
header: { text: "구분" },
sortdisable: true,
footer: divisionFooterLabelOnly,
},
];
const reviewFacilityAddressLayout = {
name: "areaGroup",
header: { text: "행정구역" },
items: [
{ column: "ctpvNm", footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] },
"sggNm",
],
};
const textCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
});
const decimal1Col = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
});
const intCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
});
// 하수도 통계 검토 11. 하수 및 분뇨 찌꺼기(슬러지) 처리
const rawSludgeList = [
{
tabKey: 'a',
tabLabel: '찌꺼기 처리시설 현황',
tabKey: "ST00110001",
tabLabel: "찌꺼기(슬러지)처리시설현황",
rowStartNum: 4,
fixedColumns: 2,
columns: [
/* 기본 정보 */
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수\n처리장명' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
/* 찌꺼기발생량 */
{ name: 'TOT_AMNT', fieldName: '합계(발생)', header: { text: '합계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'P_AMNT', fieldName: '자체(발생)', header: { text: '자체' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'O_AMNT', fieldName: '외부유입량(발생)', header: { text: '외부유입량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 처리 정보 */
{ name: 'OUT_FCT_NM', fieldName: '이송처리장', header: { text: '이송처리장' } },
{ name: 'OUT_AMNT', fieldName: '이송처리량(㎥/년)', header: { text: '이송처리량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DSP_MOIRAT', fieldName: '함수율(%,탈수기준)', header: { text: '함수율\n(%,\n탈수기준)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DSP_SM', fieldName: '찌꺼기 처리량 합계(톤/년)', header: { text: '찌꺼기\n처리량\n합계(톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 자체 찌꺼기 처리량 */
{ name: 'I_1CH_SM', fieldName: '자체찌꺼기 처리량소계', header: { text: '자체찌꺼기\n처리량소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_REC', fieldName: '매립(자체)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_INC', fieldName: '소각(자체)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_INC_2CH_SM', fieldName: '소계(소각후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_INC_2CH_REC', fieldName: '매립(소각후)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_INC_2CH_RAW', fieldName: '제품원료(소각후)', header: { text: '제품원료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_INC_2CH_COV', fieldName: '복토재(소각후)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_INC_2CH_FCT', fieldName: '시설(업체명)_소각후', header: { text: '시설(업체명)' } },
{ name: 'I_1CH_SOL', fieldName: '고화(자체)', header: { text: '고화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_SOL_2CH_SM', fieldName: '소계(고화후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_SOL_2CH_REC', fieldName: '매립(고화후)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_SOL_2CH_COV', fieldName: '복토재(고화후)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_SOL_2CH_FCT', fieldName: '시설(업체명)_고화후', header: { text: '시설(업체명)' } },
{ name: 'I_1CH_DRY', fieldName: '건조(자체)', header: { text: '건조' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_DRY_2CH_SM', fieldName: '소계(건조후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_DRY_2CH_REC', fieldName: '매립(건조후)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_DRY_2CH_INC', fieldName: '소각(건조후)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_DRY_2CH_FUE', fieldName: '연료(건조후)', header: { text: '연료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_DRY_2CH_RAW', fieldName: '제품원료(건조후)', header: { text: '제품원료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_DRY_2CH_COV', fieldName: '복토재(건조후)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_DRY_2CH_COM', fieldName: '퇴비화(건조후)', header: { text: '퇴비화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_DRY_2CH_FCT', fieldName: '시설(업체명)_건조후', header: { text: '시설(업체명)' } },
{ name: 'I_1CH_CAR', fieldName: '탄화(자체)', header: { text: '탄화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_CAR_2CH_SM', fieldName: '소계(탄화후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_CAR_2CH_INC', fieldName: '소각(탄화후)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_CAR_2CH_FUE', fieldName: '연료(탄화후)', header: { text: '연료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_CAR_2CH_FCT', fieldName: '시설(업체명)_탄화후', header: { text: '시설(업체명)' } },
{ name: 'I_1CH_COM', fieldName: '퇴비화(자체)', header: { text: '퇴비화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'I_1CH_COM_FCT', fieldName: '시설(업체명)_퇴비화', header: { text: '시설(업체명)' } },
/* 외부위탁 처리량 */
{ name: 'O_CON_SM', fieldName: '소계(외부위탁)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'O_CON_REC', fieldName: '매립(외부위탁)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'O_CON_INC', fieldName: '소각(외부위탁)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'O_CON_FUE1', fieldName: '연료(외부위탁)', header: { text: '연료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'O_CON_RAW', fieldName: '제품원료(외부위탁)', header: { text: '제품원료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'O_CON_COV', fieldName: '복토재(외부위탁)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'O_CON_COM', fieldName: '퇴비화(외부위탁)', header: { text: '퇴비화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'O_CON_FCT_NM', fieldName: '시설(업체명)_외부위탁', header: { text: '시설(업체명)' } },
/* 기타 */
{ name: 'OM_1CH_SM', fieldName: '수도권광역 위탁처리량(톤/년)', header: { text: '수도권광역\n위탁처리량\n(톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
...reviewFacilityAddressColumns,
textCol("fcltNm", "하수\n처리장명"),
decimal1Col("gnamWhol", "합계"),
decimal1Col("itslfQnt", "자체"),
decimal1Col("otsdInflwQnt", "외부유입량"),
textCol("transfTmplntNm", "이송처리장"),
decimal1Col("transfPrcsQnt", "이송처리량\n(㎥/년)"),
decimal1Col("srpFncRt", "함수율\n(%, 탈수기준)"),
decimal1Col("wholSrpPrcsQnt", "찌꺼기\n처리량 합계\n(톤/년)"),
decimal1Col("itslfTs1Whol", "자체찌꺼기\n처리량 소계"),
decimal1Col("itslfTs1RclmQnt", "매립"),
decimal1Col("itslfTs1IncnrQnt", "소각"),
decimal1Col("incnrPrcsWhol", "소계"),
decimal1Col("incnrPrcsLfvlm", "매립"),
decimal1Col("incnrPrcsPrdctRmtrlQnt", "제품원료"),
decimal1Col("incnrPrcsFmlyrQnt", "복토재"),
textCol("incnrPrcsFcltNm", "시설(업체명)"),
decimal1Col("itslfTs1AltdQnt", "고화"),
decimal1Col("stbznPrcsWhol", "소계"),
decimal1Col("stbznPrcsLfvlm", "매립"),
decimal1Col("stbznPrcsFmlyrQnt", "복토재"),
textCol("stbznPrcsFcltNm", "시설(업체명)"),
decimal1Col("itslfTs1DryQnt", "건조"),
decimal1Col("dryPrcsWhol", "소계"),
decimal1Col("dryPrcsLfvlm", "매립"),
decimal1Col("dryPrcsIncnrQnt", "소각"),
decimal1Col("dryPrcsFuelQnt", "연료"),
decimal1Col("dryPrcsPrdctRmtrlQnt", "제품원료"),
decimal1Col("dryPrcsFmlyrQnt", "복토재"),
decimal1Col("dryPrcsCmpstgQnt", "퇴비화"),
textCol("dryPrcsFcltNm", "시설(업체명)"),
decimal1Col("itslfTs1FuelQnt", "탄화"),
decimal1Col("fuelPrcsWhol", "소계"),
decimal1Col("fuelPrcsIncnrQnt", "소각"),
decimal1Col("fuelPrcsFuelQnt", "연료"),
textCol("fuelPrcsFcltNm", "시설(업체명)"),
decimal1Col("itslfTs1CmpstgQnt", "퇴비화"),
textCol("cmpsPrcsFcltBzentyNm", "시설(업체명)"),
decimal1Col("otsdCnsgnWhol", "소계"),
decimal1Col("otsdCnsgnRclmQnt", "매립"),
decimal1Col("otsdCnsgnIncnrQnt", "소각"),
decimal1Col("otsdCnsgnFuelPwplQnt", "연료"),
decimal1Col("otsdCnsgnPrdctRmtrlQnt", "제품원료"),
decimal1Col("otsdCnsgnFmlyrQnt", "복토재"),
decimal1Col("otsdCnsgnCmpstgQnt", "퇴비화"),
textCol("otsdCnsgnFcltNm", "시설(업체명)"),
decimal1Col("cparTs2Whol", "수도권광역\n위탁처리량\n(톤/년)"),
textCol("rmrk", "비고"),
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'BSC_FCT_NM', 'YR_GBN',
{ name: 'genAmntGroup', header: { text: '찌꺼기발생량(톤/년,탈수기준)' }, items: ['TOT_AMNT', 'P_AMNT', 'O_AMNT'] },
'OUT_FCT_NM', 'OUT_AMNT', 'DSP_MOIRAT', 'DSP_SM',
reviewFacilityAddressLayout,
"displayYear",
"fcltNm",
{
name: 'internalProcessGroup', header: { text: '자체 찌꺼기 처리량(톤/년)' },
name: "gnamGroup",
header: { text: "찌꺼기발생량(톤/년, 탈수기준)" },
items: ["gnamWhol", "itslfQnt", "otsdInflwQnt"],
},
"transfTmplntNm",
"transfPrcsQnt",
"srpFncRt",
"wholSrpPrcsQnt",
{
name: "itslfProcessingGroup",
header: { text: "자체 찌꺼기 처리량 현황(톤/년)" },
items: [
'I_1CH_SM', 'I_1CH_REC', 'I_1CH_INC',
{ name: 'inc2ndGroup', header: { text: '소각 후 처리(2차)' }, items: ['I_1CH_INC_2CH_SM', 'I_1CH_INC_2CH_REC', 'I_1CH_INC_2CH_RAW', 'I_1CH_INC_2CH_COV', 'I_1CH_INC_2CH_FCT'] },
'I_1CH_SOL',
{ name: 'sol2ndGroup', header: { text: '고화 후 처리(2차)' }, items: ['I_1CH_SOL_2CH_SM', 'I_1CH_SOL_2CH_REC', 'I_1CH_SOL_2CH_COV', 'I_1CH_SOL_2CH_FCT'] },
'I_1CH_DRY',
{ name: 'dry2ndGroup', header: { text: '건조 후 처리(2차)' }, items: ['I_1CH_DRY_2CH_SM', 'I_1CH_DRY_2CH_REC', 'I_1CH_DRY_2CH_INC', 'I_1CH_DRY_2CH_FUE', 'I_1CH_DRY_2CH_RAW', 'I_1CH_DRY_2CH_COV', 'I_1CH_DRY_2CH_COM', 'I_1CH_DRY_2CH_FCT'] },
'I_1CH_CAR',
{ name: 'car2ndGroup', header: { text: '탄화 후 처리(2차)' }, items: ['I_1CH_CAR_2CH_SM', 'I_1CH_CAR_2CH_INC', 'I_1CH_CAR_2CH_FUE', 'I_1CH_CAR_2CH_FCT'] },
{ name: 'compostGroup', header: { text: '퇴비화' }, items: ['I_1CH_COM_FCT'] }
]
"itslfTs1Whol",
"itslfTs1RclmQnt",
"itslfTs1IncnrQnt",
{
name: "incnrGroup",
header: { text: "소각 후 처리(2차)" },
items: [
"incnrPrcsWhol",
"incnrPrcsLfvlm",
"incnrPrcsPrdctRmtrlQnt",
"incnrPrcsFmlyrQnt",
"incnrPrcsFcltNm",
],
},
"itslfTs1AltdQnt",
{
name: "stbznGroup",
header: { text: "고화 후 처리(2차)" },
items: [
"stbznPrcsWhol",
"stbznPrcsLfvlm",
"stbznPrcsFmlyrQnt",
"stbznPrcsFcltNm",
],
},
"itslfTs1DryQnt",
{
name: "dryGroup",
header: { text: "건조 후 처리(2차)" },
items: [
"dryPrcsWhol",
"dryPrcsLfvlm",
"dryPrcsIncnrQnt",
"dryPrcsFuelQnt",
"dryPrcsPrdctRmtrlQnt",
"dryPrcsFmlyrQnt",
"dryPrcsCmpstgQnt",
"dryPrcsFcltNm",
],
},
"itslfTs1FuelQnt",
{
name: "fuelGroup",
header: { text: "탄화 후 처리(2차)" },
items: [
"fuelPrcsWhol",
"fuelPrcsIncnrQnt",
"fuelPrcsFuelQnt",
"fuelPrcsFcltNm",
],
},
"itslfTs1CmpstgQnt",
{
name: "cmpsGroup",
header: { text: "퇴비화" },
items: ["cmpsPrcsFcltBzentyNm"],
},
],
},
{
name: 'externalProcessGroup', header: { text: '외부(민간)위탁 처리량(톤/년)' },
items: ['O_CON_SM', 'O_CON_REC', 'O_CON_INC', 'O_CON_FUE1', 'O_CON_RAW', 'O_CON_COV', 'O_CON_COM', 'O_CON_FCT_NM']
name: "otsdGroup",
header: { text: "외부(민간) 위탁 처리량(톤/년)" },
items: [
"otsdCnsgnWhol",
"otsdCnsgnRclmQnt",
"otsdCnsgnIncnrQnt",
"otsdCnsgnFuelPwplQnt",
"otsdCnsgnPrdctRmtrlQnt",
"otsdCnsgnFmlyrQnt",
"otsdCnsgnCmpstgQnt",
"otsdCnsgnFcltNm",
],
},
'OM_1CH_SM', 'RMK'
]
"cparTs2Whol",
"rmrk",
],
},
{
tabKey: 'b',
tabLabel: '소화조 운영현황',
tabKey: "ST00110002",
tabLabel: "소화조 운영현황",
rowStartNum: 3,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수\n처리장명' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'DGSTR_FNNC', fieldName: '소화조 유/무(Y/N)선택', header: { text: '소화조 유/무\n(Y/N)선택' } },
{ name: 'DGSTR_CPC', fieldName: '설계용량(㎥)', header: { text: '설계\n용량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_OPR_CPC', fieldName: '실제운전용량(㎥)', header: { text: '실제운전\n용량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_IN', fieldName: '투입량(㎥/일)', header: { text: '투입량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_TS', fieldName: '투입 TS 농도(%)', header: { text: '투입 TS 농도\n(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_YRSTP_RN_DYN', fieldName: '가동일수(연간)', header: { text: '가동일수\n(연간)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_OCCUR_GAS', fieldName: '발생가스량(㎥/일)', header: { text: '발생가스량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_SM', fieldName: '계(이용현황)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_SLE', fieldName: '판매', header: { text: '판매' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_ELTGNR', fieldName: '발전', header: { text: '발전' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_ONSLF', fieldName: '자체활용', header: { text: '자체활용' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_ETC', fieldName: '기타(CNG등)', header: { text: '기타(CNG등)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DGSTR_GAS', fieldName: '잉여가스량(㎥/일)', header: { text: '잉여가스량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
...reviewFacilityAddressColumns,
textCol("fcltNm", "하수\n처리장명"),
textCol("digesterYn", "소화조 유/무\n(Y/N)선택"),
intCol("dsgnCpct", "설계\n용량(㎥)"),
intCol("actlOperCpct", "실제운전\n용량(㎥)"),
decimal1Col("inputQnt", "투입량\n(㎥/일)"),
decimal1Col("tsConc", "투입 TS 농도\n(%)"),
intCol("oprtngDays", "가동일수\n(연간)"),
decimal1Col("gnrtGasQnt", "발생가스량\n(㎥/일)"),
decimal1Col("gasTotal", "계"),
decimal1Col("gasSale", "판매"),
decimal1Col("gasGen", "발전"),
decimal1Col("gasSelf", "자체활용"),
decimal1Col("gasEtc", "기타(CNG등)"),
decimal1Col("surplusGas", "잉여가스량\n(㎥/일)"),
textCol("rmrk", "비고"),
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'BSC_FCT_NM', 'YR_GBN',
reviewFacilityAddressLayout,
"displayYear",
"fcltNm",
{
name: 'digesterMainGroup', header: { text: '소화조 운영현황' },
name: "operGroup",
header: { text: "소화조 운영현황" },
items: [
'DGSTR_FNNC', 'DGSTR_CPC', 'DGSTR_OPR_CPC', 'DGSTR_IN', 'DGSTR_TS', 'DGSTR_YRSTP_RN_DYN', 'DGSTR_OCCUR_GAS',
{ name: 'gasUsageGroup', header: { text: '소화가스 이용현황(㎥/일)' }, items: ['DGSTR_SM', 'DGSTR_SLE', 'DGSTR_ELTGNR', 'DGSTR_ONSLF', 'DGSTR_ETC'] },
'DGSTR_GAS'
]
"digesterYn",
"dsgnCpct",
"actlOperCpct",
"inputQnt",
"tsConc",
"oprtngDays",
"gnrtGasQnt",
{
name: "gasUsageGroup",
header: { text: "소화가스 이용현황(㎥/일)" },
items: ["gasTotal", "gasSale", "gasGen", "gasSelf", "gasEtc"],
},
"surplusGas",
],
},
'RMK'
]
"rmrk",
],
},
{
tabKey: 'c',
tabLabel: '하수찌꺼기 처리시설 운영현황',
tabKey: "ST00110003",
tabLabel: "하수찌꺼기 처리시설 운영현황",
rowStartNum: 2,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'SLG_FCT_NM', fieldName: '찌꺼기처리시설명', header: { text: '찌꺼기처리시설명' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'TRT_METHOD', fieldName: '처리방식', header: { text: '처리방식' } },
{ name: 'SLG_FCT_CPC', fieldName: '시설용량(톤/일)', header: { text: '시설용량\n(톤/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OPR_CPC', fieldName: '연간처리량(톤/년)', header: { text: '연간처리량\n(톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ODY_RN_TIME', fieldName: '일평균 가동시간(1~24)', header: { text: '일평균\n가동시간\n(1~24)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'YRSTP_RN_DYN', fieldName: '연중 가동일(1~330)', header: { text: '연중 가동일\n(1~330)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'OPR_RT', fieldName: '가동율', header: { text: '가동율' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
...reviewFacilityAddressColumns,
textCol("fcltNm", "찌꺼기처리\n시설명"),
textCol("prcsMthExpln", "처리방식"),
decimal1Col("fcltCpct", "시설용량\n(톤/일)"),
decimal1Col("anlPrcsQnt", "연간처리량\n(톤/년)"),
decimal1Col("dayAvgOprtngHr", "일평균\n가동시간\n(1~24)"),
intCol("anlOprtngDayCnt", "연중 가동일\n(1~330)"),
decimal1Col("oprtngRt", "가동율(%)"),
textCol("rmrk", "비고"),
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'SLG_FCT_NM', 'YR_GBN',
{ name: 'sludgeFctOprGroup', header: { text: '처리시설 운영현황' }, items: ['TRT_METHOD', 'SLG_FCT_CPC', 'OPR_CPC', 'ODY_RN_TIME', 'YRSTP_RN_DYN', 'OPR_RT'] },
'RMK'
]
reviewFacilityAddressLayout,
"displayYear",
"fcltNm",
{
name: "operStatusGroup",
header: { text: "처리시설 운영현황" },
items: [
"prcsMthExpln",
"fcltCpct",
"anlPrcsQnt",
"dayAvgOprtngHr",
"anlOprtngDayCnt",
"oprtngRt",
],
},
"rmrk",
],
},
{
tabKey: 'd',
tabLabel: '분뇨찌꺼기 처리현황',
tabKey: "ST00110004",
tabLabel: "분뇨찌꺼기(슬러지)처리현황",
rowStartNum: 2,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'ECSCS_NM', fieldName: '분뇨 처리장명', header: { text: '분 뇨\n처리장명' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'OCTY', fieldName: '발생량(A)=(B)+(C)(㎥/년)', header: { text: '발생량\n(A)=(B)+(C)\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'PROCESS_CPCTY_SM', fieldName: '계(B)', header: { text: '계(B)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RUSE', fieldName: '재활용', header: { text: '재활용' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'LAND_RCLM', fieldName: '육상매립', header: { text: '육상매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'INCNR', fieldName: '소각', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OCEAN_DPSL', fieldName: '해양투기', header: { text: '해양투기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'FUEL', fieldName: '연료화', header: { text: '연료화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ETC', fieldName: '기타', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'CYFD_CPCTY', fieldName: '미처분량(이월량)(C)(㎥/년)', header: { text: '미처분량\n(이월량)\n(C)\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OUT_BSC_FCT_NM', fieldName: '타시설 연계여부', header: { text: '타시설\n연계여부' } },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
{ name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } },
{ name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } },
...reviewFacilityAddressColumns,
textCol("fcltNm", "분뇨\n처리장명"),
decimal1Col("gnam", "발생량\n(A)=(B)+(C)"),
decimal1Col("wholDspsQnt", "계(B)"),
decimal1Col("rcycQnt", "재활용"),
decimal1Col("olandLfvlm", "육상매립"),
decimal1Col("incnrQnt", "소각"),
decimal1Col("oceanInputQnt", "해양투기"),
decimal1Col("fuelQnt", "연료화"),
decimal1Col("etcQnt", "기타"),
decimal1Col("nondspsQnt", "미처분량\n(이월량)(C)"),
textCol("swtrmtFcltNm", "타시설\n연계여부"),
textCol("rmrk", "비고"),
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'ECSCS_NM', 'YR_GBN', 'OCTY',
{ name: 'disposalAmntGroup', header: { text: '처 분 량(㎥/년)' }, items: ['PROCESS_CPCTY_SM', 'RUSE', 'LAND_RCLM', 'INCNR', 'OCEAN_DPSL', 'FUEL', 'ETC'] },
'CYFD_CPCTY', 'OUT_BSC_FCT_NM', 'RMK'
]
}
reviewFacilityAddressLayout,
"displayYear",
"fcltNm",
"gnam",
{
name: "dspsGroup",
header: { text: "처분량" },
items: [
"wholDspsQnt",
"rcycQnt",
"olandLfvlm",
"incnrQnt",
"oceanInputQnt",
"fuelQnt",
"etcQnt",
],
},
"nondspsQnt",
"swtrmtFcltNm",
"rmrk",
],
},
];
export const SewageExcretaSludgeInfoListRvw = rawSludgeList.map(tab => ({
export const SewageExcretaSludgeInfoListRvw = rawSludgeList.map((tab) => ({
...tab,
columns: tab.columns.map(col => ({
columns: tab.columns.map((col) => ({
...col,
editable: false
}))
}));
editable: false,
styleName: "rg-cell-readonly",
})),
}));
/** @deprecated `SewageExcretaSludgeInfoListRvw` 사용 */
export const SewageExcretaSludgeInfoList = SewageExcretaSludgeInfoListRvw;
@@ -1,215 +1,369 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 09. 하수도 재정
const rawFinanceList = [
{
tabKey: 'a',
tabLabel: '하수도재정 결산',
rowStartNum: 3,
fixedColumns: 1,
columns: [
/* 1. 기본 정보 */
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
import type { GridView } from "@/components/type/DataGrid.type";
import {
footerAvgDecimalArrayByDivision,
footerDecimal1ArrayByDivision,
footerIntArrayByDivision,
} from "@/components/util/grid/footerSummary";
/* 2. 재정별 사업비 (수입) */
{ name: 'FNANC_WCT_SM', fieldName: '계(수입)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'NTNXPN', fieldName: '중앙정부 지원액', header: { text: '중앙정부\n지원액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'LCLEXPNS', fieldName: '특․광역시, 도', header: { text: '특․광역시, 도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'LCLTY_CT', fieldName: '시,군,구 지자체', header: { text: '시,군,구 지자체' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SEWER_RNTFEE', fieldName: '하수도 사용료', header: { text: '하수도\n사용료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'AMNTBRNCSR', fieldName: '원인자 부담금', header: { text: '원인자\n부담금' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ETC_INCME', fieldName: '기타 잡수입', header: { text: '기타\n잡수입' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'CYFD_AMOUNT', fieldName: '이월액', header: { text: '이월액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 3. 부채액 */
{ name: 'DEBT_AMOUNT', fieldName: '부채액(백만원)', header: { text: '부채액\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => {
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
/* 4. 용도별 사업비 (지출) */
{ name: 'PRPOS_WCT_SM', fieldName: '계(지출)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'PRCPLT_SM', fieldName: '소계(처리장)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'PRCPLT_FCLTY', fieldName: '시설비(처리장)', header: { text: '시설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'PRCPLT_RPAIR', fieldName: '개보수비(처리장)', header: { text: '개보수비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'PRCPLT_ORPNS', fieldName: '운영 유지비(처리장)', header: { text: '운영\n유지비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'PRCPLT_ETC', fieldName: '기타(처리장)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SEWAGE_SM', fieldName: '소계(관로)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SEWAGE_FCLTY', fieldName: '시설비(관로)', header: { text: '시설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SEWAGE_RPAIR', fieldName: '개보수비(관로)', header: { text: '개보수비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SEWAGE_ORPNS', fieldName: '운영 유지비(관로)', header: { text: '운영\n유지비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SEWAGE_ETC', fieldName: '기타(관로)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SWGE_STRGE_SM', fieldName: '소계(저류시설)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SWGE_STRGE_FCLTY', fieldName: '시설비(저류시설)', header: { text: '시설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SWGE_STRGE_RPAIR', fieldName: '개보수비(저류시설)', header: { text: '개보수비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SWGE_STRGE_ORPNS', fieldName: '운영 유지비(저류시설)', header: { text: '운영\n유지비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SWGE_STRGE_ETC', fieldName: '기타(저류시설)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ETC_SM', fieldName: '소계(기타지출)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ACDNT_CYFD_AMOUNT', fieldName: '사고 이월액', header: { text: '사고\n이월액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'UNRCIV_AMOUNT', fieldName: '미수납액', header: { text: '미수납액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ETC_AMOUNT', fieldName: '기타(지출)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 5. 비고 */
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'ADST_CD',
{
name: 'financeGroup',
header: { text: '재정별 사업비(백만원)' },
items: [
'FNANC_WCT_SM', 'NTNXPN',
{ name: 'localGroup', header: { text: '지방비' }, items: ['LCLEXPNS', 'LCLTY_CT'] },
'SEWER_RNTFEE', 'AMNTBRNCSR', 'ETC_INCME', 'CYFD_AMOUNT'
]
},
'DEBT_AMOUNT',
{
name: 'usageGroup',
header: { text: '용도별사업비(백만원)' },
items: [
'PRPOS_WCT_SM',
{ name: 'plantGroup', header: { text: '처리장' }, items: ['PRCPLT_SM', 'PRCPLT_FCLTY', 'PRCPLT_RPAIR', 'PRCPLT_ORPNS', 'PRCPLT_ETC'] },
{ name: 'pipeGroup', header: { text: '하수관로' }, items: ['SEWAGE_SM', 'SEWAGE_FCLTY', 'SEWAGE_RPAIR', 'SEWAGE_ORPNS', 'SEWAGE_ETC'] },
{ name: 'storageGroup', header: { text: '하수저류시설' }, items: ['SWGE_STRGE_SM', 'SWGE_STRGE_FCLTY', 'SWGE_STRGE_RPAIR', 'SWGE_STRGE_ORPNS', 'SWGE_STRGE_ETC'] },
{ name: 'etcUsageGroup', header: { text: '기타' }, items: ['ETC_SM', 'ACDNT_CYFD_AMOUNT', 'UNRCIV_AMOUNT', 'ETC_AMOUNT'] }
]
},
'RMK'
]
},
{
tabKey: 'b',
tabLabel: '하수도 요금',
rowStartNum: 2,
fixedColumns: 1,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'FYER_LEVY_QY', fieldName: '연간조정량(㎥)(A)', header: { text: '연간조정량\n(㎥)\n(A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'LEVY_AMOUNT', fieldName: '하수도사용료수익(원)(B)', header: { text: '하수도사용료수익\n(원)\n(B)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'AVRG_UNTPC', fieldName: '㎥당요금(원/㎥) C=(B/A)', header: { text: '㎥당요금(원/㎥)\nC=(B/A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SMRIZE_PRMPC', fieldName: '총괄원가(원)(D)', header: { text: '총괄원가(원)\n(D)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SMRIZE_UNIT_PRMPC', fieldName: '㎥당원가(원/㎥) E=(D/A)', header: { text: '㎥당원가(원/㎥)\nE=(D/A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RLTY_RT', fieldName: '요금현실화율(%) F=(C/E*100)', header: { text: '요금현실화율(%)\nF=(C/E*100)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } }
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'FYER_LEVY_QY', 'LEVY_AMOUNT', 'AVRG_UNTPC', 'SMRIZE_PRMPC', 'SMRIZE_UNIT_PRMPC', 'RLTY_RT', 'RMK'
]
},
{
tabKey: 'c',
tabLabel: '하수도 사용료',
rowStartNum: 2,
fixedColumns: 1,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'LEVY_AMOUNT', fieldName: '부과액(총계)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'LEVY_QY', fieldName: '부과량(총계)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'HOME_LEVY_AMOUNT', fieldName: '부과액(가정용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'HOME_LEVY_QY', fieldName: '부과량(가정용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'HOME_UNTPC', fieldName: '단가(가정용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'JOB_LEVY_AMOUNT', fieldName: '부과액(업무용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'JOB_LEVY_QY', fieldName: '부과량(업무용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'JOB_UNTPC', fieldName: '단가(업무용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'BSN_LEVY_AMOUNT', fieldName: '부과액(영업용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'BSN_LEVY_QY', fieldName: '부과량(영업용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'BSN_UNTPC', fieldName: '단가(영업용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'BATHS1_LEVY_AMOUNT', fieldName: '부과액(욕탕1)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'BATHS1_LEVY_QY', fieldName: '부과량(욕탕1)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'BATHS1_UNTPC', fieldName: '단가(욕탕1)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'BATHS2_LEVY_AMOUNT', fieldName: '부과액(욕탕2)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'BATHS2_LEVY_QY', fieldName: '부과량(욕탕2)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'BATHS2_UNTPC', fieldName: '단가(욕탕2)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'INDUST_LEVY_AMOUNT', fieldName: '부과액(산업용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'INDUST_LEVY_QY', fieldName: '부과량(산업용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'INDUST_UNTPC', fieldName: '단가(산업용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ETC_LEVY_AMOUNT', fieldName: '부과액(기타사용료)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ETC_LEVY_QY', fieldName: '부과량(기타사용료)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ETC_UNTPC', fieldName: '단가(기타사용료)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
{
name: 'totalGroup', header: { text: '총계' },
items: [{ name: 'tAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['LEVY_AMOUNT'] }, { name: 'tQyUnit', header: { text: '부과량\n(천톤)' }, items: ['LEVY_QY'] }]
},
{
name: 'homeGroup', header: { text: '가 정 용' },
items: [{ name: 'hAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['HOME_LEVY_AMOUNT'] }, { name: 'hQyUnit', header: { text: '부과량\n(천톤)' }, items: ['HOME_LEVY_QY'] }, { name: 'hUntUnit', header: { text: '단가\n(원/톤)' }, items: ['HOME_UNTPC'] }]
},
{
name: 'jobGroup', header: { text: '업 무 용' },
items: [{ name: 'jAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['JOB_LEVY_AMOUNT'] }, { name: 'jQyUnit', header: { text: '부과량\n(천톤)' }, items: ['JOB_LEVY_QY'] }, { name: 'jUntUnit', header: { text: '단가\n(원/톤)' }, items: ['JOB_UNTPC'] }]
},
{
name: 'bsnGroup', header: { text: '영 업 용(일 반 용)' },
items: [{ name: 'bAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['BSN_LEVY_AMOUNT'] }, { name: 'bQyUnit', header: { text: '부과량\n(천톤)' }, items: ['BSN_LEVY_QY'] }, { name: 'bUntUnit', header: { text: '단가\n(원/톤)' }, items: ['BSN_UNTPC'] }]
},
{
name: 'bath1Group', header: { text: '욕 탕 1종(대중탕용)' },
items: [{ name: 'bt1AmtUnit', header: { text: '부과액\n(백만원)' }, items: ['BATHS1_LEVY_AMOUNT'] }, { name: 'bt1QyUnit', header: { text: '부과량\n(천톤)' }, items: ['BATHS1_LEVY_QY'] }, { name: 'bt1UntUnit', header: { text: '단가\n(원/톤)' }, items: ['BATHS1_UNTPC'] }]
},
{
name: 'bath2Group', header: { text: '욕 탕 2종' },
items: [{ name: 'bt2AmtUnit', header: { text: '부과액\n(백만원)' }, items: ['BATHS2_LEVY_AMOUNT'] }, { name: 'bt2QyUnit', header: { text: '부과량\n(천톤)' }, items: ['BATHS2_LEVY_QY'] }, { name: 'bt2UntUnit', header: { text: '단가\n(원/톤)' }, items: ['BATHS2_UNTPC'] }]
},
{
name: 'industGroup', header: { text: '산업용 및 공업용' },
items: [{ name: 'iAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['INDUST_LEVY_AMOUNT'] }, { name: 'iQyUnit', header: { text: '부과량\n(천톤)' }, items: ['INDUST_LEVY_QY'] }, { name: 'iUntUnit', header: { text: '단가\n(원/톤)' }, items: ['INDUST_UNTPC'] }]
},
{
name: 'etcGroup', header: { text: '기타' },
items: [{ name: 'eAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['ETC_LEVY_AMOUNT'] }, { name: 'eQyUnit', header: { text: '부과량\n(천톤)' }, items: ['ETC_LEVY_QY'] }, { name: 'eUntUnit', header: { text: '단가\n(원/톤)' }, items: ['ETC_UNTPC'] }]
},
'RMK'
]
},
{
tabKey: 'd',
tabLabel: '원인자부담금',
rowStartNum: 3,
fixedColumns: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'LEVY_CO', fieldName: '건수(부과액)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'LEVY_AMOUNT', fieldName: '금액(백만원)(부과액)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'TRGET_LEV_STTUS_CO', fieldName: '건수(계)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'TRGET_LEV_STTUS_AMOUNT', fieldName: '금액(백만원)(계)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DRNG_EQP_CO', fieldName: '건수(배수설비)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'DRNG_EQP_AMOUNT', fieldName: '금액(백만원)(배수설비)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'CNTRWK_CO', fieldName: '건수(타공사)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CNTRWK_AMOUNT', fieldName: '금액(백만원)(타공사)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN',
{ name: 'levyMainGroup', header: { text: '부과액' }, items: ['LEVY_CO', 'LEVY_AMOUNT'] },
{
name: 'collectGroup', header: { text: '대상별 징수현황' },
items: [
{ name: 'collectTotal', header: { text: '계' }, items: ['TRGET_LEV_STTUS_CO', 'TRGET_LEV_STTUS_AMOUNT'] },
{ name: 'contractorGroup', header: { text: '배수설비설치자\n(하수도법 제61조 제1항)' }, items: ['DRNG_EQP_CO', 'DRNG_EQP_AMOUNT'] },
{ name: 'otherGroup', header: { text: '타공사·타행위자\n(하수도법 제61조 제2항)' }, items: ['CNTRWK_CO', 'CNTRWK_AMOUNT'] }
]
},
'RMK'
]
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, "displayYear");
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
if (divisionValues.length <= divisionIndex) {
return withSuffix ? "합계" : "";
}
const label = divisionValues[divisionIndex] ?? "";
return withSuffix ? `${label}합계` : label;
};
const divisionFooterCallbacks = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
];
export const SewerageFinanceInfoListRvw = rawFinanceList.map(tab => ({
const divisionFooterLabelOnly = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
];
const reviewAddressColumns = [
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
mergeCells: true,
sortdisable: true,
footer: divisionFooterCallbacks,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
mergeCells: true,
sortdisable: true,
},
{
name: "admdstNm",
fieldName: "admdstNm",
header: { text: "행정구역명" },
mergeCells: true,
sortdisable: true,
},
{
name: "displayYear",
fieldName: "displayYear",
header: { text: "구분" },
sortdisable: true,
footer: divisionFooterLabelOnly,
},
];
const reviewAddressLayout = {
name: "areaGroup",
header: { text: "행정구역" },
items: [
{ column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
"sggNm",
"admdstNm",
],
};
const decimal1Col = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
});
const intCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
});
// 하수도 통계 검토 09. 하수도 재정
const rawFinanceList = [
{
tabKey: "ST00090001",
tabLabel: "하수도재정 결산",
rowStartNum: 3,
fixedColumns: 2,
columns: [
...reviewAddressColumns,
decimal1Col("fnnByWholPjtcoAmt", "계"),
decimal1Col("cnterGovSprtAmt", "중앙정부\n지원액"),
decimal1Col("mctpvLcexpAmt", "특․광역시, 도"),
decimal1Col("wareaSggLclAmt", "시,군,구 지자체"),
decimal1Col("sewerUseAmt", "하수도\n사용료"),
decimal1Col("orgprBrdnAmt", "원인자\n부담금"),
decimal1Col("etcIncmAmt", "기타\n잡수입"),
decimal1Col("crfwAmt", "이월액"),
decimal1Col("lbltAmt", "부채액\n(백만원)"),
decimal1Col("usgByWholPjtcoAmt", "계"),
decimal1Col("tmplntWholAmt", "소계"),
decimal1Col("tmplntFcltAmt", "시설비"),
decimal1Col("tmplntMendngAmt", "개보수비"),
decimal1Col("tmplntOperKeepAmt", "운영\n유지비"),
decimal1Col("tmplntEtcAmt", "기타"),
decimal1Col("wholSwagePplAmt", "소계"),
decimal1Col("swagePplFcltAmt", "시설비"),
decimal1Col("swagePplMendngAmt", "개보수비"),
decimal1Col("swagePplOperKeepAmt", "운영\n유지비"),
decimal1Col("swagePplEtcAmt", "기타"),
decimal1Col("wholSwageImpdFcltAmt", "소계"),
decimal1Col("swageImpdFcltAmt", "시설비"),
decimal1Col("swageImpdMendngAmt", "개보수비"),
decimal1Col("swageImpdOperKeepAmt", "운영\n유지비"),
decimal1Col("swageImpdFcltEtcAmt", "기타"),
decimal1Col("etcWholAmt", "소계"),
decimal1Col("etcAcdntCrfwAmt", "사고\n이월액"),
decimal1Col("etcNrcpAmt", "미수납액"),
decimal1Col("etcAmt", "기타"),
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
],
columnLayout: [
reviewAddressLayout,
"displayYear",
{
name: "financeGroup",
header: { text: "재정별 사업비(백만원)" },
items: [
"fnnByWholPjtcoAmt",
"cnterGovSprtAmt",
{
name: "localGroup",
header: { text: "지방비" },
items: ["mctpvLcexpAmt", "wareaSggLclAmt"],
},
"sewerUseAmt",
"orgprBrdnAmt",
"etcIncmAmt",
"crfwAmt",
],
},
"lbltAmt",
{
name: "purposeGroup",
header: { text: "용도별사업비(백만원)" },
items: [
"usgByWholPjtcoAmt",
{
name: "t1Group",
header: { text: "처리장" },
items: [
"tmplntWholAmt",
"tmplntFcltAmt",
"tmplntMendngAmt",
"tmplntOperKeepAmt",
"tmplntEtcAmt",
],
},
{
name: "t2Group",
header: { text: "하수관로" },
items: [
"wholSwagePplAmt",
"swagePplFcltAmt",
"swagePplMendngAmt",
"swagePplOperKeepAmt",
"swagePplEtcAmt",
],
},
{
name: "t3Group",
header: { text: "유수지 및 배수펌프장" },
items: [
"wholSwageImpdFcltAmt",
"swageImpdFcltAmt",
"swageImpdMendngAmt",
"swageImpdOperKeepAmt",
"swageImpdFcltEtcAmt",
],
},
{
name: "t4Group",
header: { text: "기타" },
items: ["etcWholAmt", "etcAcdntCrfwAmt", "etcNrcpAmt", "etcAmt"],
},
],
},
"rmrk",
],
},
{
tabKey: "ST00090002",
tabLabel: "하수도 요금",
rowStartNum: 2,
fixedColumns: 2,
columns: [
...reviewAddressColumns,
intCol("anlLevyQnt", "연간조정량(㎡)"),
intCol("levyAmt", "하수도사용료수익(원)"),
decimal1Col("avgUntprcAmt", "㎡당 요금(원/㎡)"),
intCol("grssPrmcoAmt", "총관원가(원)"),
decimal1Col("grssUnitPrmcoAmt", "㎡당원가(원/㎡)"),
{
name: "rlztnRt",
fieldName: "rlztnRt",
header: { text: "현실화율(%)" },
footer: footerAvgDecimalArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
},
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
],
columnLayout: [
reviewAddressLayout,
"displayYear",
"anlLevyQnt",
"levyAmt",
"avgUntprcAmt",
"grssPrmcoAmt",
"grssUnitPrmcoAmt",
"rlztnRt",
"rmrk",
],
},
{
tabKey: "ST00090003",
tabLabel: "하수도 사용료",
rowStartNum: 3,
fixedColumns: 2,
columns: [
...reviewAddressColumns,
decimal1Col("levyAmt", "부과액(백만원)"),
decimal1Col("levyQnt", "부과량(천톤)"),
decimal1Col("hshusLevyAmt", "부과액"),
decimal1Col("hshusLevyQnt", "부과량"),
intCol("hshusUntprcAmt", "단가"),
decimal1Col("taskFrLevyAmt", "부과액"),
decimal1Col("taskFrLevyQnt", "부과량"),
intCol("taskFrUntprcAmt", "단가"),
decimal1Col("bsnGnrlLevyAmt", "부과액(백만원)"),
decimal1Col("bsnGnrlLevyQnt", "부과량(천톤)"),
intCol("bsnGnrlUntprcAmt", "단가(원/톤)"),
decimal1Col("massBthhsFrLevyAmt", "부과액(백만원)"),
decimal1Col("massBthhsFrLevyQnt", "부과량(천톤)"),
intCol("massBthhsFrUntprcAmt", "단가(원/톤)"),
decimal1Col("bthhsFrSpcs2LevyAmt", "부과액"),
decimal1Col("bthhsFrSpcs2LevyQnt", "부과량"),
intCol("bthhsFrSpcs2UntprcAmt", "단가"),
decimal1Col("induMdsrLevyAmt", "부과액(백만원)"),
decimal1Col("induMdsrLevyQnt", "부과량(천톤)"),
intCol("induMdsrUntprcAmt", "단가(원/톤)"),
decimal1Col("etcLevyAmt", "부과액"),
decimal1Col("etcLevyQnt", "부과량"),
intCol("etcLevyUntprcAmt", "단가"),
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
],
columnLayout: [
reviewAddressLayout,
"displayYear",
{ name: "totalGroup", header: { text: "총계" }, items: ["levyAmt", "levyQnt"] },
{
name: "g1Group",
header: { text: "가정용" },
items: ["hshusLevyAmt", "hshusLevyQnt", "hshusUntprcAmt"],
},
{
name: "g2Group",
header: { text: "업무용" },
items: ["taskFrLevyAmt", "taskFrLevyQnt", "taskFrUntprcAmt"],
},
{
name: "g3Group",
header: { text: "영업용(일반용)" },
items: ["bsnGnrlLevyAmt", "bsnGnrlLevyQnt", "bsnGnrlUntprcAmt"],
},
{
name: "g4Group",
header: { text: "욕탕 1종(대중탕용)" },
items: ["massBthhsFrLevyAmt", "massBthhsFrLevyQnt", "massBthhsFrUntprcAmt"],
},
{
name: "g5Group",
header: { text: "욕탕2종" },
items: ["bthhsFrSpcs2LevyAmt", "bthhsFrSpcs2LevyQnt", "bthhsFrSpcs2UntprcAmt"],
},
{
name: "g6Group",
header: { text: "산업 및 공업용" },
items: ["induMdsrLevyAmt", "induMdsrLevyQnt", "induMdsrUntprcAmt"],
},
{
name: "g7Group",
header: { text: "기타" },
items: ["etcLevyAmt", "etcLevyQnt", "etcLevyUntprcAmt"],
},
"rmrk",
],
},
{
tabKey: "ST00090004",
tabLabel: "원인자부담금",
rowStartNum: 3,
fixedColumns: 2,
columns: [
...reviewAddressColumns,
intCol("levyEvntcsCnt", "건수"),
decimal1Col("levyAmt", "금액(백만원)"),
intCol("trgtByWholClctnEvntcsCnt", "건수"),
decimal1Col("trgtByWholClctnAmt", "금액(백만원)"),
intCol("mltvEqpmEvntcsCnt", "건수"),
decimal1Col("mltvEqpmAmt", "금액(백만원)"),
intCol("othrCstrnOthrActrCnt", "건수"),
decimal1Col("othrCstrnOthrActrAmt", "금액(백만원)"),
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
],
columnLayout: [
reviewAddressLayout,
"displayYear",
{ name: "levyGroup", header: { text: "부과액" }, items: ["levyEvntcsCnt", "levyAmt"] },
{
name: "collectionStatusGroup",
header: { text: "대상별 징수현황" },
items: [
{
name: "c1Group",
header: { text: "계" },
items: ["trgtByWholClctnEvntcsCnt", "trgtByWholClctnAmt"],
},
{
name: "c2Group",
header: { text: "배수설비설치자\n(하수도법 제61조 제1항)" },
items: ["mltvEqpmEvntcsCnt", "mltvEqpmAmt"],
},
{
name: "c3Group",
header: { text: "타공사·타행위자\n(하수도법 제61조 제2항)" },
items: ["othrCstrnOthrActrCnt", "othrCstrnOthrActrAmt"],
},
],
},
"rmrk",
],
},
];
export const SewerageFinanceInfoListRvw = rawFinanceList.map((tab) => ({
...tab,
columns: tab.columns.map(col => ({
columns: tab.columns.map((col) => ({
...col,
editable: false
}))
}));
editable: false,
styleName: "rg-cell-readonly",
})),
}));
@@ -1,117 +1,213 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 13. 하수도 서비스 수준
import type { GridView } from "@/components/type/DataGrid.type";
import {
footerAvgDecimalArrayByDivision,
footerDecimal1ArrayByDivision,
footerIntArrayByDivision,
} from "@/components/util/grid/footerSummary";
const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => {
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, "displayYear");
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
if (divisionValues.length <= divisionIndex) {
return withSuffix ? "합계" : "";
}
const label = divisionValues[divisionIndex] ?? "";
return withSuffix ? `${label}합계` : label;
};
const divisionFooterCallbacks = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
];
const divisionFooterLabelOnly = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
];
const reviewAddressColumns = [
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
mergeCells: true,
sortdisable: true,
footer: divisionFooterCallbacks,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
mergeCells: true,
sortdisable: true,
},
// {
// name: "admdstNm",
// fieldName: "admdstNm",
// header: { text: "행정구역명" },
// mergeCells: true,
// sortdisable: true,
// },
{
name: "displayYear",
fieldName: "displayYear",
header: { text: "구분" },
sortdisable: true,
footer: divisionFooterLabelOnly,
},
];
const reviewAddressLayout = {
name: "areaGroup",
header: { text: "행정구역" },
items: [
{ column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
"sggNm",
// "admdstNm",
],
};
const textCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
});
const intCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
});
const decimal1Col = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
});
const avgDecimalCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerAvgDecimalArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
});
// 하수도 통계 검토 13. 하수도 서비스 수준
const rawServiceLevelList = [
{
tabKey: 'a',
tabLabel: '민원',
tabKey: "ST00130001",
tabLabel: "민원",
rowStartNum: 2,
fixedColumns: 2,
columns: [
/* 1. 기본 정보 */
{ name: 'SIDO', fieldName: 'SIDO', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: 'GUGUN', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: 'YR_GBN', header: { text: '구분' } },
/* 2. 하수처리 인구 (정수 반영) */
{ name: 'POPLTN_SM', fieldName: 'POPLTN_SM', header: { text: '하수처리인구\n(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
/* 3. 민원유형 (단위: 건 / 정수 반영) */
{ name: 'CVPL_CM', fieldName: 'CVPL_CM', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_DCWTR_QLTWTR', fieldName: 'CVPL_DCWTR_QLTWTR', header: { text: '악취' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_BSML', fieldName: 'CVPL_BSML', header: { text: '하수관로' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_FIVW', fieldName: 'CVPL_FIVW', header: { text: '배수설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_POLLUTN_ACDNT', fieldName: 'CVPL_POLLUTN_ACDNT', header: { text: '맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_SSOS', fieldName: 'CVPL_SSOS', header: { text: '정화조' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_CHRGE', fieldName: 'CVPL_CHRGE', header: { text: '하수도요금' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CVPL_ETC', fieldName: 'CVPL_ETC', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
/* 4. 성과 지표 및 비고 (소수점 1자리 반영) */
{ name: 'STDR_POPLTN_PER_CVPL', fieldName: 'STDR_POPLTN_PER_CVPL', header: { text: '기준인구당\n민원건수\n(건/10,000명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: 'RMK', header: { text: '비고' } }
...reviewAddressColumns,
intCol("swtrmtPpltnCnt", "하수처리인구\n(명)"),
intCol("whol", "총계"),
intCol("dschgWqCnt", "악취"),
intCol("odorCnt", "하수관로"),
intCol("ldsCnt", "배수설비"),
intCol("plltnAcdntCnt", "맨홀"),
intCol("swagePplCnt", "정화조"),
intCol("sewerCrgAmt", "하수도요금"),
intCol("etcCnt", "기타"),
avgDecimalCol("crtrPpltnCvlcptEvntcsCnt", "기준인구당\n민원건수\n(건/10,000명)"),
textCol("rmrk", "비고"),
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN',
'POPLTN_SM',
reviewAddressLayout,
"displayYear",
"swtrmtPpltnCnt",
{
name: 'complaintTypeGroup',
header: { text: '민원유형(건)' },
name: "complaintTypeGroup",
header: { text: "민원유형(건)" },
items: [
'CVPL_CM',
'CVPL_DCWTR_QLTWTR',
'CVPL_BSML',
'CVPL_FIVW',
'CVPL_POLLUTN_ACDNT',
'CVPL_SSOS',
'CVPL_CHRGE',
'CVPL_ETC'
]
"whol",
"dschgWqCnt",
"odorCnt",
"ldsCnt",
"plltnAcdntCnt",
"swagePplCnt",
"sewerCrgAmt",
"etcCnt",
],
},
'STDR_POPLTN_PER_CVPL',
'RMK'
]
"crtrPpltnCvlcptEvntcsCnt",
"rmrk",
],
},
{
tabKey: 'b',
tabLabel: '주민친화시설',
tabKey: "ST00130002",
tabLabel: "주민친화시설",
rowStartNum: 2,
fixedColumns: 5,
fixedColumns: 2,
columns: [
/* 1. 기본 및 시설 정보 */
{ name: 'SIDO', fieldName: 'SIDO', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: 'GUGUN', header: { text: '시군구' } },
{ name: 'BSC_FCT_NM', fieldName: 'BSC_FCT_NM', header: { text: '처리장명' } },
{ name: 'FCT_CPC', fieldName: 'FCT_CPC', header: { text: '시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'YR_GBN', fieldName: 'YR_GBN', header: { text: '구분' } },
/* 2. 처리장 전체 면적 (소수점 1자리 반영) */
{ name: 'PRCPLT_AR', fieldName: 'PRCPLT_AR', header: { text: '처리장\n면적(㎡)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 3. 주민친화시설 면적 상세 (단위: ㎡ / 정수 반영) */
{ name: 'TOTAL_AR', fieldName: 'TOTAL_AR', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RESTRM_AR', fieldName: 'RESTRM_AR', header: { text: '휴게시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CHILDPLAY_AR', fieldName: 'CHILDPLAY_AR', header: { text: '어린이놀이공간' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ALSFC_AR', fieldName: 'ALSFC_AR', header: { text: '체력단련시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'PLGRND_AR', fieldName: 'PLGRND_AR', header: { text: '운동시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'PRKPLCE_AR', fieldName: 'PRKPLCE_AR', header: { text: '주차장' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'CTLSTT_AR', fieldName: 'CTLSTT_AR', header: { text: '교육 및\n문화시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'NRPRK_AR', fieldName: 'NRPRK_AR', header: { text: '습지조성\n(생태공원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ETC_AR', fieldName: 'ETC_AR', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
/* 4. 비고 */
{ name: 'RMK', fieldName: 'RMK', header: { text: '비고' } }
...reviewAddressColumns,
textCol("swtrmtFcltNm", "처리장명"),
intCol("fcltCpct", "시설용량\n(㎥/일)"),
decimal1Col("tmplntArea", "처리장\n면적(㎡)"),
decimal1Col("whol", "총계"),
decimal1Col("brkFcltArea", "휴게시설"),
decimal1Col("childFrshpSpacArea", "어린이놀이공간"),
decimal1Col("bdySportsFcltArea", "체력단련시설"),
decimal1Col("sportsFcltArea", "운동시설"),
decimal1Col("pkltArea", "주차장"),
decimal1Col("eduMultiFcltArea", "교육 및\n문화시설"),
decimal1Col("wtlndMakeArea", "습지조성\n(생태공원)"),
decimal1Col("etcCnt", "기타"),
textCol("rmrk", "비고"),
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'BSC_FCT_NM',
'FCT_CPC',
'YR_GBN',
'PRCPLT_AR',
reviewAddressLayout,
"displayYear",
"swtrmtFcltNm",
"fcltCpct",
"tmplntArea",
{
name: 'friendlyFacilityGroup',
header: { text: '주민친화시설 면적(㎡)' },
name: "friendlyFacilityGroup",
header: { text: "주민친화시설 면적(㎡)" },
items: [
'TOTAL_AR',
'RESTRM_AR',
'CHILDPLAY_AR',
'ALSFC_AR',
'PLGRND_AR',
'PRKPLCE_AR',
'CTLSTT_AR',
'NRPRK_AR',
'ETC_AR'
]
"whol",
"brkFcltArea",
"childFrshpSpacArea",
"bdySportsFcltArea",
"sportsFcltArea",
"pkltArea",
"eduMultiFcltArea",
"wtlndMakeArea",
"etcCnt",
],
},
'RMK'
]
}
"rmrk",
],
},
];
export const SewerageServiceLevelInfoListRvw = rawServiceLevelList.map(tab => ({
export const SewerageServiceLevelInfoListRvw = rawServiceLevelList.map((tab) => ({
...tab,
columns: tab.columns.map(col => ({
columns: tab.columns.map((col) => ({
...col,
editable: false
}))
}));
editable: false,
styleName: "rg-cell-readonly",
})),
}));
/** @deprecated `SewerageServiceLevelInfoListRvw` 사용 */
export const SewerageServiceLevelInfoList = SewerageServiceLevelInfoListRvw;
@@ -1,224 +1,366 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 10. 물 재이용
const rawWaterReuseList = [
{
tabKey: 'a',
tabLabel: '공공하수처리수 재이용',
rowStartNum: 4,
fixedColumns: 2,
columns: [
/* 1. 기본 및 행정 정보 */
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
import type { GridView } from "@/components/type/DataGrid.type";
import {
footerAvgDecimalArrayByDivision,
footerDecimal1ArrayByDivision,
footerDecimal2ArrayByDivision,
footerIntArrayByDivision,
} from "@/components/util/grid/footerSummary";
/* 2. 하수처리장 기본 정보 */
{ name: 'BSC_FCT_NM', fieldName: '시설명', header: { text: '시설명' } },
{ name: 'EQP_PRMS_DT', fieldName: '인가일(년/월/일)', header: { text: '인가일\n(년/월/일)' } },
{ name: 'FCT_CPC', fieldName: '시설용량(㎥/일)', header: { text: '시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'TPUT_TOTAL', fieldName: '연간 하수처리량(천톤/년)', header: { text: '연간\n하수처리량\n(천톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RSWTR_LAW_YN', fieldName: '법적의무대상(대상/미대상)', header: { text: '법적의무대상\n(대상/미대상)' } },
const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => {
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
/* 3. 재이용시설 현황 */
{ name: 'INST_YN', fieldName: '설치유무(유/무)', header: { text: '설치유무\n(유/무)' } },
{ name: 'RSWTR_RN_DT', fieldName: '가동개시일 yyyy-mm-dd', header: { text: '가동개시일\nyyyy-mm-dd' } },
{ name: 'RSWTR_PRMS_DT', fieldName: '설치승인일 yyyy-mm-dd', header: { text: '설치승인일\nyyyy-mm-dd' } },
{ name: 'RSWTR_CPC', fieldName: '용량(톤/일)', header: { text: '용량(톤/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RSWTR_TRT_NM', fieldName: '공법/기자재', header: { text: '공법/기자재' } },
{ name: 'RSWTR_PIPE', fieldName: '공급관로길이(km)', header: { text: '공급관로길이\n(km)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RSWTR_PUMP', fieldName: '공급펌프(톤/일)', header: { text: '공급펌프\n(톤/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, "displayYear");
if (val) uniqueValues.add(String(val));
}
/* 4. 재이용 실적 - 총계 및 장내용수 */
{ name: 'USE_AMNT_TOTAL', fieldName: '총계', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_D01', fieldName: '계(장내)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S01', fieldName: '세척수', header: { text: '세척수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S02', fieldName: '냉각수', header: { text: '냉각수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S03', fieldName: '청소수', header: { text: '청소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S04', fieldName: '조경용수(장내)', header: { text: '조경용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S05', fieldName: '희석용수', header: { text: '희석용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S06', fieldName: '장내중수도', header: { text: '장내중수도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S07', fieldName: '장내기타용수', header: { text: '장내기타용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
const divisionValues = Array.from(uniqueValues);
if (divisionValues.length <= divisionIndex) {
return withSuffix ? "합계" : "";
}
/* 5. 재이용 실적 - 장외용수 */
{ name: 'USE_AMNT_D02', fieldName: '계(장외)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S11', fieldName: '청소·화장실 용수', header: { text: '청소·화장실\n용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S12', fieldName: '세척·살수용수', header: { text: '세척·살수용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S13', fieldName: '조경용수(장외)', header: { text: '조경용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S14', fieldName: '친수용수', header: { text: '친수용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S15', fieldName: '하천등 유지용수', header: { text: '하천등 유지용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S16', fieldName: '농업용수', header: { text: '농업용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S17', fieldName: '지하수충전', header: { text: '지하수충전' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'USE_AMNT_S18', fieldName: '공업용수', header: { text: '공업용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
const label = divisionValues[divisionIndex] ?? "";
return withSuffix ? `${label}합계` : label;
};
/* 6. 지표 및 비고 */
{ name: 'REUSE_RT', fieldName: '재이용률(%)', header: { text: '재이용률\n(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN',
{
name: 'sewagePlantGroup',
header: { text: '하수처리장' },
items: ['BSC_FCT_NM', 'EQP_PRMS_DT', 'FCT_CPC', 'TPUT_TOTAL', 'RSWTR_LAW_YN']
},
{
name: 'reuseFctGroup',
header: { text: '재이용시설' },
items: [
'INST_YN',
{
name: 'installDetailGroup',
header: { text: '재이용시설 설치시' },
items: [
'RSWTR_RN_DT',
'RSWTR_PRMS_DT',
{
name: 'reProcessGroup',
header: { text: '재처리시설' },
items: ['RSWTR_CPC', 'RSWTR_TRT_NM']
},
'RSWTR_PIPE',
'RSWTR_PUMP'
]
}
]
},
{
name: 'reuseStatusGroup',
header: { text: '하수처리수 재이용현황(천톤/년)' },
items: [
'USE_AMNT_TOTAL',
{
name: 'internalUseGroup',
header: { text: '장 내 용 수' },
items: [
'USE_AMNT_D01', 'USE_AMNT_S01', 'USE_AMNT_S02', 'USE_AMNT_S03',
'USE_AMNT_S04', 'USE_AMNT_S05', 'USE_AMNT_S06', 'USE_AMNT_S07'
]
},
{
name: 'externalUseGroup',
header: { text: '장 외 용 수' },
items: [
'USE_AMNT_D02', 'USE_AMNT_S11', 'USE_AMNT_S12', 'USE_AMNT_S13',
'USE_AMNT_S14', 'USE_AMNT_S15', 'USE_AMNT_S16', 'USE_AMNT_S17', 'USE_AMNT_S18'
]
}
]
},
'REUSE_RT',
'RMK'
]
},
{
tabKey: 'b',
tabLabel: '중수도 현황',
rowStartNum: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'BILD_NM', fieldName: '건축물명/개발사업명', header: { text: '건축물명/\n개발사업명' } },
{ name: 'ADDR', fieldName: '주소', header: { text: '주소' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'BILD_TP', fieldName: '시설물용도/개발사업종류', header: { text: '시설물용도/\n개발사업종류' } },
{ name: 'LAW_YN', fieldName: '법적의무 대상여부(대상/미대상)', header: { text: '법적의무\n대상여부\n(대상/미대상)' } },
{ name: 'COMP_DT', fieldName: '설치완료일 yyyy-mm-dd', header: { text: '설치완료일\nyyyy-mm-dd' } },
{ name: 'RN_DT', fieldName: '가동개시일 yyyy-mm-dd', header: { text: '가동개시일\nyyyy-mm-dd' } },
{ name: 'TOTAR', fieldName: '건축연면적(㎡)', header: { text: '건축연면적\n(㎡)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'PROC_CPCTY', fieldName: '처리용량(㎥/일)', header: { text: '처리용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'WATER_USE_CPCTY', fieldName: '물 사용량(㎥/년)', header: { text: '물 사용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RUSWTR_USE_SM', fieldName: '중수도 이용량(㎥/년)', header: { text: '중수도\n이용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'SUB_USE_CPCTY', fieldName: '대체수 이용량(㎥/년)', header: { text: '대체수\n이용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RUSWTR_PRPOS', fieldName: '중수도용도', header: { text: '중수도용도' } },
{ name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비\n(백만원/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } }
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'BILD_NM', 'ADDR', 'YR_GBN', 'BILD_TP', 'LAW_YN', 'COMP_DT', 'RN_DT',
{
name: 'fctStatusGroup',
header: { text: '시설현황' },
items: ['TOTAR', 'PROC_CPCTY']
},
'WATER_USE_CPCTY', 'RUSWTR_USE_SM', 'SUB_USE_CPCTY', 'RUSWTR_PRPOS', 'FYER_OPER_CT', 'RMK'
]
},
{
tabKey: 'c',
tabLabel: '빗물이용시설 현황',
rowStartNum: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'RWR_REUSE_FCT_NM', fieldName: '시설명', header: { text: '시설명' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'BILD_TP', fieldName: '건축물용도', header: { text: '건축물용도' } },
{ name: 'ADDR', fieldName: '위치(주소)', header: { text: '위치\n(주소)' } },
{ name: 'LAW_YN', fieldName: '법적의무 대상여부(대상/미대상)', header: { text: '법적의무\n대상여부\n(대상/미대상)' } },
{ name: 'RN_DT', fieldName: '설치완료일 yyyy-mm-dd', header: { text: '설치완료일\nyyyy-mm-dd' } },
{ name: 'INSTL_CT', fieldName: '설치비(백만원)', header: { text: '설치비\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'CCM', fieldName: '집수면 종류', header: { text: '집수면 종류' } },
{ name: 'TRTFC_ENNC', fieldName: '여과등 처리시설 유무', header: { text: '여과등\n처리시설\n유무' } },
{ name: 'SGBS_CPCTY', fieldName: '저류조 용량(㎥)', header: { text: '저류조 용량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'FYER_USEQTY', fieldName: '연간 이용량(㎥/년)', header: { text: '연간 이용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RWR_PRCUSE_PRPPOS', fieldName: '이용용도', header: { text: '이용용도' } },
{ name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비\n(백만원/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'RWR_REUSE_FCT_NM', 'YR_GBN', 'BILD_TP', 'ADDR', 'LAW_YN', 'RN_DT', 'INSTL_CT', 'CCM', 'TRTFC_ENNC', 'SGBS_CPCTY', 'FYER_USEQTY', 'RWR_PRCUSE_PRPPOS', 'FYER_OPER_CT', 'RMK'
]
},
{
tabKey: 'd',
tabLabel: '하ㆍ폐수처리수 재이용시설 시공업 등록현황',
rowStartNum: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } },
{ name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } },
{ name: 'ADDR', fieldName: '주소', header: { text: '주소' } },
{ name: 'CMPN_RGSDE', fieldName: '등록일 yyyy-mm-dd', header: { text: '등록일\nyyyy-mm-dd' } },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
{ name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } },
{ name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN', 'CMPNM', 'RPRSNTV', 'ADDR', 'CMPN_RGSDE', 'RMK'
]
},
{
tabKey: 'e',
tabLabel: '하ㆍ폐수처리수 재이용시설 시공업 인가현황',
rowStartNum: 2,
columns: [
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } },
{ name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } },
{ name: 'ADDR', fieldName: '주소', header: { text: '주소' } },
{ name: 'CMPN_RGSDE', fieldName: '인가일(년/월/일)', header: { text: '인가일\n(년/월/일)' } },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } }
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN', 'CMPNM', 'RPRSNTV', 'ADDR', 'CMPN_RGSDE', 'RMK'
]
}
const divisionFooterCallbacks = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
];
export const WaterReuseInfoList = rawWaterReuseList.map(tab => ({
...tab,
columns: tab.columns.map(col => ({
...col,
editable: false
}))
}));
const divisionFooterLabelOnly = [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
];
const reviewFacilityAddressColumns = [
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
mergeCells: true,
sortdisable: true,
footer: divisionFooterCallbacks,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
mergeCells: true,
sortdisable: true,
},
{
name: "displayYear",
fieldName: "displayYear",
header: { text: "구분" },
sortdisable: true,
footer: divisionFooterLabelOnly,
},
];
const reviewFacilityAddressLayout = {
name: "areaGroup",
header: { text: "행정구역" },
items: [
{ column: "ctpvNm", footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] },
"sggNm",
],
};
const textCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
});
const decimal1Col = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerDecimal1ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
});
const decimal2Col = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerDecimal2ArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.00",
});
const intCol = (name: string, header: string) => ({
name,
fieldName: name,
header: { text: header },
footer: footerIntArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0",
});
// 하수도 통계 검토 10. 물 재이용
const rawWaterReuseList = [
{
tabKey: "ST00100001",
tabLabel: "공공하수처리수 재이용",
rowStartNum: 4,
fixedColumns: 2,
columns: [
...reviewFacilityAddressColumns,
textCol("swtrmtFcltNm", "시설명"),
textCol("sncnYmd", "인가일\n(년/월/일)"),
decimal1Col("prcsFcltCpct", "시설용량\n(㎥/일)"),
decimal1Col("prcsQnt", "연간\n하수처리량\n(천톤/년)"),
textCol("lglYn", "법적의무대상\n(대상/미대상)"),
textCol("instlYn", "설치유무\n(유/무)"),
textCol("oprtngStrtYmd", "가동개시일\nyyyy-mm-dd"),
textCol("instlAprvYmd", "설치승인일\nyyyy-mm-dd"),
decimal1Col("rprcsFcltCpct", "용량(톤/일)"),
textCol("mtcnsNm", "공법/기자재"),
decimal2Col("ruswtPplLen", "공급관로길이\n(km)"),
decimal1Col("ruswtPumpLen", "공급펌프\n(톤/일)"),
decimal1Col("wholUseQnt", "총계"),
decimal1Col("ithlWshwtrQnt", "계"),
decimal1Col("ithlClnQty", "세척수"),
decimal1Col("ithlFrzgQty", "냉각수"),
decimal1Col("ithlLdhcUswtrQnt", "청소수"),
decimal1Col("ithlDltnUswtrQnt", "조경용수"),
decimal1Col("ithlRuswtrQnt", "희석용수"),
decimal1Col("ithlEtcQnt", "장내중수도"),
decimal1Col("ithlUnsdQnt", "장내기타용수"),
decimal1Col("otcntrUswtrWhol", "계"),
decimal1Col("otcntrClnRstrmQnt", "청소·화장실\n용수"),
decimal1Col("otcntrWshwtrQnt", "세척·살수용수"),
decimal1Col("otcntrLdhcUswtrQnt", "조경용수"),
decimal1Col("otcntrHdrplUswtrQnt", "친수용수"),
decimal1Col("otcntrRvrKeepQnt", "하천등 유지용수"),
decimal1Col("agrcltUswtrPstnQnt", "농업용수"),
decimal1Col("ugwtrChargQnt", "지하수충전"),
decimal1Col("mdsrUswtrQnt", "공업용수"),
{
name: "rtlztnRt",
fieldName: "rtlztnRt",
header: { text: "재이용률\n(%)" },
footer: footerAvgDecimalArrayByDivision,
valueType: ValueType.NUMBER,
numberFormat: "#,##0.0",
},
textCol("rmrk", "비고"),
],
columnLayout: [
reviewFacilityAddressLayout,
"displayYear",
{
name: "seweragePlantGroup",
header: { text: "하수처리장" },
items: ["swtrmtFcltNm", "sncnYmd", "prcsFcltCpct", "prcsQnt", "lglYn"],
},
{
name: "reuseFacilityGroup",
header: { text: "재이용시설" },
items: [
"instlYn",
{
name: "installationGroup",
header: { text: "재이용시설 설치시" },
items: [
"oprtngStrtYmd",
"instlAprvYmd",
{
name: "reProcessingFacilityGroup",
header: { text: "재처리시설" },
items: ["rprcsFcltCpct", "mtcnsNm"],
},
"ruswtPplLen",
"ruswtPumpLen",
],
},
],
},
{
name: "reuseStatusGroup",
header: { text: "하수처리수 재이용현황(천톤/년)" },
items: [
"wholUseQnt",
{
name: "internalWaterGroup",
header: { text: "장 내 용 수" },
items: [
"ithlWshwtrQnt",
"ithlClnQty",
"ithlFrzgQty",
"ithlLdhcUswtrQnt",
"ithlDltnUswtrQnt",
"ithlRuswtrQnt",
"ithlEtcQnt",
"ithlUnsdQnt",
],
},
{
name: "externalWaterGroup",
header: { text: "장 외 용 수" },
items: [
"otcntrUswtrWhol",
"otcntrClnRstrmQnt",
"otcntrWshwtrQnt",
"otcntrLdhcUswtrQnt",
"otcntrHdrplUswtrQnt",
"otcntrRvrKeepQnt",
"agrcltUswtrPstnQnt",
"ugwtrChargQnt",
"mdsrUswtrQnt",
],
},
],
},
"rtlztnRt",
"rmrk",
],
},
{
tabKey: "ST00100002",
tabLabel: "중수도 현황",
rowStartNum: 2,
fixedColumns: 2,
columns: [
...reviewFacilityAddressColumns,
textCol("fcltNm", "건축물명/\n개발사업명"),
textCol("bldgSeCn", "시설물용도/\n개발사업종류"),
textCol("fcltPstnCn", "주소"),
textCol("lglTrgtYn", "법적의무\n대상여부\n(대상/미대상)"),
textCol("cmptnStrtYmd", "설치완료일\nyyyy-mm-dd"),
textCol("oprtngStrtYmd", "가동개시일\nyyyy-mm-dd"),
intCol("archWholArea", "건축연면적\n(㎡)"),
decimal1Col("prcsCpct", "처리용량\n(㎥/일)"),
intCol("wtrUseQnt", "물 사용량\n(㎥/년)"),
intCol("ruswtrUtztnQnt", "중수도\n이용량\n(㎥/년)"),
intCol("sbstCntUtztnQnt", "대체수\n이용량\n(㎥/년)"),
textCol("mnUsgCn", "중수도용도"),
decimal1Col("ruswtrOperAmt", "연간운영비\n(백만원/년)"),
textCol("rmrk", "비고"),
],
columnLayout: [
reviewFacilityAddressLayout,
"displayYear",
"fcltNm",
"bldgSeCn",
"fcltPstnCn",
"lglTrgtYn",
"cmptnStrtYmd",
"oprtngStrtYmd",
{
name: "facilityStatusGroup",
header: { text: "시설현황" },
items: ["archWholArea", "prcsCpct"],
},
"wtrUseQnt",
"ruswtrUtztnQnt",
"sbstCntUtztnQnt",
"mnUsgCn",
"ruswtrOperAmt",
"rmrk",
],
},
{
tabKey: "ST00100003",
tabLabel: "빗물이용시설 현황",
rowStartNum: 2,
fixedColumns: 2,
columns: [
...reviewFacilityAddressColumns,
textCol("fcltNm", "시설명"),
textCol("bdstTypCn", "건축물용도"),
textCol("fcltPstnCn", "위치\n(주소)"),
textCol("lglTrgtYn", "법적의무\n대상여부\n(대상/미대상)"),
textCol("instlCmptnYmd", "설치완료일\nyyyy-mm-dd"),
decimal1Col("instlAmt", "설치비\n(백만원)"),
textCol("rcptMynCn", "집수면 종류"),
textCol("filtrPrcsFcltYnExpln", "여과등\n처리시설\n유무"),
intCol("impdCpct", "저류조 용량\n(㎥)"),
intCol("anlUseQnt", "연간 이용량\n(㎥/년)"),
textCol("exlnUtlzUsgExpln", "이용용도"),
decimal1Col("anlOperAmt", "연간운영비\n(백만원/년)"),
textCol("rmrk", "비고"),
],
columnLayout: [
reviewFacilityAddressLayout,
"displayYear",
"fcltNm",
"bdstTypCn",
"fcltPstnCn",
"lglTrgtYn",
"instlCmptnYmd",
"instlAmt",
"rcptMynCn",
"filtrPrcsFcltYnExpln",
"impdCpct",
"anlUseQnt",
"exlnUtlzUsgExpln",
"anlOperAmt",
"rmrk",
],
},
{
tabKey: "ST00100004",
tabLabel: "하ㆍ폐수처리수 재이용시설 시공업 등록현황",
rowStartNum: 2,
fixedColumns: 2,
columns: [
...reviewFacilityAddressColumns,
textCol("bplcNm", "상호(명칭)"),
textCol("rprsvNm", "대표자"),
textCol("bplcPstnCn", "주소"),
textCol("regDt", "등록일\nyyyy-mm-dd"),
textCol("rmrk", "비고"),
],
columnLayout: [
reviewFacilityAddressLayout,
"displayYear",
"bplcNm",
"rprsvNm",
"bplcPstnCn",
"regDt",
"rmrk",
],
},
{
tabKey: "ST00100005",
tabLabel: "하ㆍ폐수처리수 재이용시설 시공업 인가현황",
rowStartNum: 2,
fixedColumns: 2,
columns: [
...reviewFacilityAddressColumns,
textCol("bplcNm", "상호(명칭)"),
textCol("rprsvNm", "대표자"),
textCol("bplcPstnCn", "주소"),
textCol("coRegYmd", "인가일\n(년/월/일)"),
textCol("rmrk", "비고"),
],
columnLayout: [
reviewFacilityAddressLayout,
"displayYear",
"bplcNm",
"rprsvNm",
"bplcPstnCn",
"coRegYmd",
"rmrk",
],
},
];
export const WaterReuseInfoListRvw = rawWaterReuseList.map((tab) => ({
...tab,
columns: tab.columns.map((col) => ({
...col,
editable: false,
styleName: "rg-cell-readonly",
})),
}));
/** @deprecated `WaterReuseInfoListRvw` 사용 */
export const WaterReuseInfoList = WaterReuseInfoListRvw;
@@ -1,3 +1,13 @@
import type { CellStyleCallback } from "@/components/type/DataGrid.type";
/** 검토(review) 그리드: rowType이 CURR인 행(당해 연도) 흰색 배경 */
export const reviewRowTypeCurrCellStyleCallback: CellStyleCallback = ({ row }) => {
if (String(row.rowType ?? "") === "CURR") {
return "rg-cell-rowtype-curr";
}
return undefined;
};
/**
* 객체 배열을 그리드 row 형식으로 변환 (null/undefined → "").
* 어떤 엔티티 타입이든 공통 사용 가능.
@@ -29,6 +29,8 @@ import {
type TreatWaterReuseDesignConstStatusSaveResponse,
type TreatWaterReuseDesignConstStatusSubmitRequest,
type TreatWaterReuseDesignConstStatusSubmitResponse,
type WaterReuseOperationDataUpdateRequest,
type WaterReuseOperationDataUpdateResponse,
type WaterReuseRequestParams,
} from "../../model/input/statistics-input-water-reuse-interface";
@@ -199,6 +201,17 @@ export class StatisticsInputWaterReuseApiService implements StatisticsInputWater
);
return response.data;
}
/** 공공하수처리수 재이용 운영 데이터 업데이트 — ST00100001 */
public async updateWaterReuseOperationData(
request: WaterReuseOperationDataUpdateRequest,
): Promise<WaterReuseOperationDataUpdateResponse> {
const response = await stClient.put<WaterReuseOperationDataUpdateResponse>(
`${BASE}/operation-data/update`,
request,
);
return response.data;
}
}
export const statisticsInputWaterReuseApi = new StatisticsInputWaterReuseApiService();
@@ -9,6 +9,8 @@ import type {
PublicSewageFacilitySaveResponse,
PublicSewageFacilitySubmitRequest,
PublicSewageFacilitySubmitResponse,
PublicSewageFacilityOperationDataUpdateRequest,
PublicSewageFacilityOperationDataUpdateResponse,
PublicSewageMainFacilityMappingSaveRequest,
PublicSewageMainFacilityMappingSaveResponse,
PublicSewageMainFacilityResponse,
@@ -73,6 +75,16 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics
);
return response.data;
}
public async updatePublicSewageFacilityOperationData(
param: PublicSewageFacilityOperationDataUpdateRequest,
): Promise<PublicSewageFacilityOperationDataUpdateResponse> {
const response = await stClient.put<PublicSewageFacilityOperationDataUpdateResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/operation-data/update`,
param,
);
return response.data;
}
}
export const statisticsInputPublicSewageFacilityApiService = new StatisticsInputPublicSewageFacilityApiService();
@@ -35,6 +35,34 @@ import {
type PublicSewageFacilityReviewApiResponse,
type PublicSewageFacilityReviewRequestParams,
} from "../model/statistics-public-sewage-facility-review-interface";
import {
type ExcretaTreatmentFacilityReviewApiResponse,
type ExcretaTreatmentFacilityReviewRequestParams,
} from "../model/statistics-excreta-treatment-facility-review-interface";
import {
type PrivateSewageFacilityReviewApiResponse,
type PrivateSewageFacilityReviewRequestParams,
} from "../model/statistics-private-sewage-facility-review-interface";
import {
type SewerageFinanceReviewApiResponse,
type SewerageFinanceReviewRequestParams,
} from "../model/statistics-sewerage-finance-review-interface";
import {
type WaterReuseReviewApiResponse,
type WaterReuseReviewRequestParams,
} from "../model/statistics-water-reuse-review-interface";
import {
type SewageExcretaSlurgeReviewApiResponse,
type SewageExcretaSlurgeReviewRequestParams,
} from "../model/statistics-sewage-excreta-slurge-review-interface";
import {
type SewageExcretaRelatedReviewApiResponse,
type SewageExcretaRelatedReviewRequestParams,
} from "../model/statistics-sewage-excreta-related-review-interface";
import {
type SewerageServiceLevelReviewApiResponse,
type SewerageServiceLevelReviewRequestParams,
} from "../model/statistics-sewerage-service-level-review-interface";
export class StatisticsManagementApiService {
@@ -80,6 +108,20 @@ export class StatisticsManagementApiService {
return response.data;
}
/**
* 07. 분뇨처리시설 검토 목록 조회
* @param params (sido, gugun, yr, bsnmCd, fctName)
*/
public async getExcretaTreatmentFacilityReviewList(
params: ExcretaTreatmentFacilityReviewRequestParams,
): Promise<ExcretaTreatmentFacilityReviewApiResponse> {
const response = await stClient.get<ExcretaTreatmentFacilityReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/excreta-treatment`,
{ params },
);
return response.data;
}
/**
* 02. 하수관로 지역별 시설현황 목록 조회 (Tab1)
* @param params (sido, gugun, yr, bsnmCd)
@@ -226,6 +268,292 @@ export class StatisticsManagementApiService {
return response.data;
}
/**
* 08. 개인하수처리시설 tab1. 처리용량별 오수처리시설 현황 검토 조회 — ST00080001
*/
public async getPrivateSewageTreatFactStatusReviewList(
params: PrivateSewageFacilityReviewRequestParams,
): Promise<PrivateSewageFacilityReviewApiResponse> {
const response = await stClient.get<PrivateSewageFacilityReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/private-sevage`,
{ params },
);
return response.data;
}
/**
* 08. 개인하수처리시설 tab2. 정화조 현황 검토 조회 — ST00080002
*/
public async getSepticTankStatusReviewList(
params: PrivateSewageFacilityReviewRequestParams,
): Promise<PrivateSewageFacilityReviewApiResponse> {
const response = await stClient.get<PrivateSewageFacilityReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/private-tank-status`,
{ params },
);
return response.data;
}
/**
* 08. 개인하수처리시설 tab3. 개인하수도 관리지역 지정공고 검토 조회 — ST00080003
*/
public async getPrivateSewageDesigStatusReviewList(
params: PrivateSewageFacilityReviewRequestParams,
): Promise<PrivateSewageFacilityReviewApiResponse> {
const response = await stClient.get<PrivateSewageFacilityReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/private-mgmt-area`,
{ params },
);
return response.data;
}
/**
* 09. 하수도 재정 tab1. 하수도재정 결산 검토 조회 — ST00090001
*/
public async getSewerageFinanceSettleReviewList(
params: SewerageFinanceReviewRequestParams,
): Promise<SewerageFinanceReviewApiResponse> {
const response = await stClient.get<SewerageFinanceReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-financial`,
{ params },
);
return response.data;
}
/**
* 09. 하수도 재정 tab2. 하수도 요금 검토 조회 — ST00090002
*/
public async getSewerageChargeReviewList(
params: SewerageFinanceReviewRequestParams,
): Promise<SewerageFinanceReviewApiResponse> {
const response = await stClient.get<SewerageFinanceReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-fee`,
{ params },
);
return response.data;
}
/**
* 09. 하수도 재정 tab3. 하수도 사용료 검토 조회 — ST00090003
*/
public async getSewerageUsageFeeReviewList(
params: SewerageFinanceReviewRequestParams,
): Promise<SewerageFinanceReviewApiResponse> {
const response = await stClient.get<SewerageFinanceReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-usage`,
{ params },
);
return response.data;
}
/**
* 09. 하수도 재정 tab4. 원인자부담금 검토 조회 — ST00090004
*/
public async getCauserBurdenReviewList(
params: SewerageFinanceReviewRequestParams,
): Promise<SewerageFinanceReviewApiResponse> {
const response = await stClient.get<SewerageFinanceReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-org-cause`,
{ params },
);
return response.data;
}
/**
* 10. 물 재이용 tab1. 공공하수처리수 재이용 검토 조회 — ST00100001
*/
public async getWaterReuseReuseStatusReviewList(
params: WaterReuseReviewRequestParams,
): Promise<WaterReuseReviewApiResponse> {
const response = await stClient.get<WaterReuseReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/pbl-swage-ruswt`,
{ params },
);
return response.data;
}
/**
* 10. 물 재이용 tab2. 중수도 현황 검토 조회 — ST00100002
*/
public async getWaterReuseGrayWaterReviewList(
params: WaterReuseReviewRequestParams,
): Promise<WaterReuseReviewApiResponse> {
const response = await stClient.get<WaterReuseReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/gray-water-status`,
{ params },
);
return response.data;
}
/**
* 10. 물 재이용 tab3. 빗물이용시설 현황 검토 조회 — ST00100003
*/
public async getWaterReuseRainwaterReviewList(
params: WaterReuseReviewRequestParams,
): Promise<WaterReuseReviewApiResponse> {
const response = await stClient.get<WaterReuseReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/water-reuse`,
{ params },
);
return response.data;
}
/**
* 10. 물 재이용 tab4. 하·폐수처리수 재이용시설 시공업 등록현황 검토 조회 — ST00100004
*/
public async getWaterReuseRegStatusReviewList(
params: WaterReuseReviewRequestParams,
): Promise<WaterReuseReviewApiResponse> {
const response = await stClient.get<WaterReuseReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/wwt-rtlztn-reg`,
{ params },
);
return response.data;
}
/**
* 10. 물 재이용 tab5. 하·폐수처리수 재이용시설 시공업 인가현황 검토 조회 — ST00100005
*/
public async getWaterReuseSncnStatusReviewList(
params: WaterReuseReviewRequestParams,
): Promise<WaterReuseReviewApiResponse> {
const response = await stClient.get<WaterReuseReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/wwt-rtlztn-aprv`,
{ params },
);
return response.data;
}
/**
* 11. 하수 및 분뇨 찌꺼기 tab1. 찌꺼기(슬러지)처리시설현황 검토 조회 — ST00110001
*/
public async getSludgeProcessingReviewList(
params: SewageExcretaSlurgeReviewRequestParams,
): Promise<SewageExcretaSlurgeReviewApiResponse> {
const response = await stClient.get<SewageExcretaSlurgeReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sludge-processing`,
{ params },
);
return response.data;
}
/**
* 11. 하수 및 분뇨 찌꺼기 tab2. 소화조 운영현황 검토 조회 — ST00110002
*/
public async getSludgeDigestionTankReviewList(
params: SewageExcretaSlurgeReviewRequestParams,
): Promise<SewageExcretaSlurgeReviewApiResponse> {
const response = await stClient.get<SewageExcretaSlurgeReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sludge-digestion-tank`,
{ params },
);
return response.data;
}
/**
* 11. 하수 및 분뇨 찌꺼기 tab3. 하수찌꺼기 처리시설 운영현황 검토 조회 — ST00110003
*/
public async getSludgeFacilityOperReviewList(
params: SewageExcretaSlurgeReviewRequestParams,
): Promise<SewageExcretaSlurgeReviewApiResponse> {
const response = await stClient.get<SewageExcretaSlurgeReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sludge-facility-oper`,
{ params },
);
return response.data;
}
/**
* 11. 하수 및 분뇨 찌꺼기 tab4. 분뇨찌꺼기(슬러지)처리현황 검토 조회 — ST00110004
*/
public async getHumanWasteSludgeReviewList(
params: SewageExcretaSlurgeReviewRequestParams,
): Promise<SewageExcretaSlurgeReviewApiResponse> {
const response = await stClient.get<SewageExcretaSlurgeReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sludge-human-waste`,
{ params },
);
return response.data;
}
/**
* 12. 하수 및 분뇨 관련 산업 tab1. 공공기관 하수분야 검토 조회 — ST00120001
*/
public async getPublicSewageStaffReviewList(
params: SewageExcretaRelatedReviewRequestParams,
): Promise<SewageExcretaRelatedReviewApiResponse> {
const response = await stClient.get<SewageExcretaRelatedReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/public-sewage-staff`,
{ params },
);
return response.data;
}
/**
* 12. 하수 및 분뇨 관련 산업 tab2. 공공기관 분뇨분야 검토 조회 — ST00120002
*/
public async getPublicHumanWasteStaffReviewList(
params: SewageExcretaRelatedReviewRequestParams,
): Promise<SewageExcretaRelatedReviewApiResponse> {
const response = await stClient.get<SewageExcretaRelatedReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/public-human-waste-staff`,
{ params },
);
return response.data;
}
/**
* 12. 하수 및 분뇨 관련 산업 tab3. 분뇨수집·운반업 검토 조회 — ST00120003
*/
public async getHumanWasteCollectionReviewList(
params: SewageExcretaRelatedReviewRequestParams,
): Promise<SewageExcretaRelatedReviewApiResponse> {
const response = await stClient.get<SewageExcretaRelatedReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/human-waste-collection`,
{ params },
);
return response.data;
}
/**
* 12. 하수 및 분뇨 관련 산업 tab4. 개인하수처리관련업 검토 조회 — ST00120004
*/
public async getIndvSwtrmtRelBsnsReviewList(
params: SewageExcretaRelatedReviewRequestParams,
): Promise<SewageExcretaRelatedReviewApiResponse> {
const response = await stClient.get<SewageExcretaRelatedReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/indv-swtrmt-rel-bsns`,
{ params },
);
return response.data;
}
/**
* 13. 하수도 서비스 수준 tab1. 민원 검토 조회 — ST00130001
*/
public async getSewageComplaintReviewList(
params: SewerageServiceLevelReviewRequestParams,
): Promise<SewerageServiceLevelReviewApiResponse> {
const response = await stClient.get<SewerageServiceLevelReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sewage-complaint`,
{ params },
);
return response.data;
}
/**
* 13. 하수도 서비스 수준 tab2. 주민친화시설 검토 조회 — ST00130002
*/
public async getResidentFriendlyFacilityReviewList(
params: SewerageServiceLevelReviewRequestParams,
): Promise<SewerageServiceLevelReviewApiResponse> {
const response = await stClient.get<SewerageServiceLevelReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/resident-friendly-facility`,
{ params },
);
return response.data;
}
public async getReviewHistory(params: ReviewHistoryRequest): Promise<ReviewHistoryResponse> {
const response = await stClient.get<ReviewHistoryResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/history`,
@@ -1,4 +1,5 @@
import { client, stClient } from "@/shared/api/client";
import { withAuthHeader } from "@/shared/api/with-auth-header";
import { ENDPOINT } from "@/shared/config";
import {
@@ -310,7 +311,7 @@ export class StatisticsApiService implements StatisticsService {
public async getReviewerInfo(param: ReviewerInfoRequest): Promise<ReviewerInfoResponse> {
const response = await stClient.get<ReviewerInfoResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/reviewer-info`,
{ params: param },
withAuthHeader({ params: param }),
);
return response.data;
}
@@ -46,15 +46,15 @@ export interface SewageReservoirItem {
wtrStrgZoneArea: number;
impdFcltCpct: number;
grssPpqty: number;
pumpFcltN1DmtrExpln: string;
pumpFcltN1DmtrExpln: number;
pumpFcltN1PerPpqty: number;
pumpFcltN1PumpCnt: number;
pumpFcltN1RsrvPumpCnt: number;
pumpFcltN2DmtrExpln: string;
pumpFcltN2DmtrExpln: number;
pumpFcltN2PerPpqty: number;
pumpFcltN2PumpCnt: number;
pumpFcltN2RsrvPumpCnt: number;
pumpFcltN3DmtrExpln: string;
pumpFcltN3DmtrExpln: number;
pumpFcltN3PerPpqty: number;
pumpFcltN3PumpCnt: number;
pumpFcltN3RsrvPumpCnt: number;
@@ -114,17 +114,17 @@ export interface RainfallPumpStationItem {
abstnRgnImpdQnt: number;
grssPpqty: number;
pumpFcltN1DmtrExpln: string;
pumpFcltN1DmtrExpln: number;
pumpFcltN1PerPpqty: number;
pumpFcltN1PumpCnt: number;
pumpFcltN1RsrvPumpCnt: number;
pumpFcltN2DmtrExpln: string;
pumpFcltN2DmtrExpln: number;
pumpFcltN2PerPpqty: number;
pumpFcltN2PumpCnt: number;
pumpFcltN2RsrvPumpCnt: number;
pumpFcltN3DmtrExpln: string;
pumpFcltN3DmtrExpln: number;
pumpFcltN3PerPpqty: number;
pumpFcltN3PumpCnt: number;
pumpFcltN3RsrvPumpCnt: number;
@@ -520,6 +520,20 @@ export interface TreatWaterReuseBusinessApprStatusSubmitResponse {
};
}
export interface WaterReuseOperationDataUpdateRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
}
export interface WaterReuseOperationDataUpdateResponse {
data?: string;
message?: string;
status: string;
}
export interface StatisticsInputWaterReuseService {
/** 공공하수처리수 재이용 조회 — ST00100001 */
getPublicSewageTreatWaterReuseList(
@@ -581,4 +595,8 @@ export interface StatisticsInputWaterReuseService {
submitTreatWaterReuseBusinessApprStatus(
request: TreatWaterReuseBusinessApprStatusSubmitRequest,
): Promise<TreatWaterReuseBusinessApprStatusSubmitResponse>;
/** 공공하수처리수 재이용 운영 데이터 업데이트 — ST00100001 */
updateWaterReuseOperationData(
request: WaterReuseOperationDataUpdateRequest,
): Promise<WaterReuseOperationDataUpdateResponse>;
}
@@ -281,6 +281,20 @@ export interface PublicSewageFacilitySubmitResponse {
status: string;
}
export interface PublicSewageFacilityOperationDataUpdateRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
}
export interface PublicSewageFacilityOperationDataUpdateResponse {
data?: string;
message?: string;
status: string;
}
export interface StatisticsInputPublicSewageFacilityService {
getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise<PublicSewageFacilityResponse>;
getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise<PublicSewageMainFacilityResponse>;
@@ -288,4 +302,7 @@ export interface StatisticsInputPublicSewageFacilityService {
saveMainFacilityMapping(param: PublicSewageMainFacilityMappingSaveRequest): Promise<PublicSewageMainFacilityMappingSaveResponse>;
savePublicSewageFacility(param: PublicSewageFacilitySaveRequest): Promise<PublicSewageFacilitySaveResponse>;
submitPublicSewageFacility(param: PublicSewageFacilitySubmitRequest): Promise<PublicSewageFacilitySubmitResponse>;
updatePublicSewageFacilityOperationData(
param: PublicSewageFacilityOperationDataUpdateRequest,
): Promise<PublicSewageFacilityOperationDataUpdateResponse>;
}
@@ -0,0 +1,76 @@
import type { ApiResponse } from "./statistics-interface";
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
export interface ExcretaTreatmentFacilityReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
fcltNm: string;
rowType: string;
fcltPstnCn: string | null;
telNo: string | null;
cmptnStrtYmd: string | null;
oprtngStrtYmd: string | null;
wholPjtcoAmt: number;
prcsFcltCpct: number;
cpct: number;
oprtngDayCnt: number;
prcsMthdExpln: string | null;
linkTmplntCn: string | null;
linkPrcsQnt: number;
inflwBodWqVl: number;
inflwTocWqVl: number;
inflwSsWqVl: number;
inflwTnWqVl: number;
inflwTpWqVl: number;
inflwClfgCnt: number;
dschgBodWqVl: number;
dschgTocWqVl: number;
dschgSsWqVl: number;
dschgTnWqVl: number;
dschgTpWqVl: number;
dschgClfgCnt: number;
wtsysNm: string | null;
trbtrNm: string | null;
msirbCn: string | null;
ssirbCn: string | null;
operMthdCdCn: string | null;
wholNope: number;
sftyAcdntEvntcsCnt: number;
rmrk: string | null;
}
export interface ExcretaTreatmentFacilityReviewResponseData {
excretaTreatmentFacilityList: ExcretaTreatmentFacilityReviewItem[];
statsStatusInfo?: StatsStatusInfo;
}
export interface ExcretaTreatmentFacilityReviewRequestParams {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
swtrmtFcltNm?: string;
sewerBzmnCd?: string;
}
export type ExcretaTreatmentFacilityReviewApiResponse = ApiResponse<
ExcretaTreatmentFacilityReviewItem[] | ExcretaTreatmentFacilityReviewResponseData
>;
export function extractExcretaTreatmentFacilityReviewList(
data: ExcretaTreatmentFacilityReviewItem[] | ExcretaTreatmentFacilityReviewResponseData | undefined,
): ExcretaTreatmentFacilityReviewItem[] {
if (!data) return [];
if (Array.isArray(data)) return data;
return data.excretaTreatmentFacilityList ?? [];
}
export function extractExcretaTreatmentFacilityReviewStatsStatusInfo(
data: ExcretaTreatmentFacilityReviewItem[] | ExcretaTreatmentFacilityReviewResponseData | undefined,
): StatsStatusInfo {
if (!data || Array.isArray(data)) return {};
return data.statsStatusInfo ?? {};
}
@@ -0,0 +1,152 @@
import type { ApiResponse } from "./statistics-interface";
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
export interface PrivateSewageFacilityReviewRequestParams {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
}
/** ST00080001 검토 `data.privateSewageList[]` 한 행 */
export interface PrivateSewageTreatFactStatusReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstCd: string;
admdstNm: string | null;
sewerBzmnCd: string;
displayYear: string;
rowType: string;
wholDrtwPrcsFcltCnt: number;
wholStaaCnt: number;
step1StaaPrcsQnt: number;
step2StaaPrcsQnt: number;
step3StaaPrcsQnt: number;
step4StaaPrcsQnt: number;
step5StaaPrcsQnt: number;
step6StaaPrcsQnt: number;
step7StaaPrcsQnt: number;
staaExcpWhol: number;
step1StaaExcpPrcsQnt: number;
step2StaaExcpPrcsQnt: number;
step3StaaExcpPrcsQnt: number;
step4StaaExcpPrcsQnt: number;
step5StaaExcpPrcsQnt: number;
step6StaaExcpPrcsQnt: number;
step7StaaExcpPrcsQnt: number;
rmrk: string | null;
}
/** ST00080002 검토 `data.septicTankList[]` 한 행 */
export interface SepticTankReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstCd: string;
admdstNm: string | null;
sewerBzmnCd: string;
displayYear: string;
rowType: string;
grssFcltCnt: number;
staaInsdWhol: number;
staaInsdSttkNprsflCnt: number;
step1StaaInsdSttkFrcCnt: number;
staaInsdSprktpFltrtbCnt: number;
staaInsdAertMthdCnt: number;
staaInsdCntcAertCnt: number;
staaInsdEtcCnt: number;
staaInsdPrcsCpctByWhol: number;
step1StaaInsdNope: number;
step2StaaInsdNope: number;
step3StaaInsdNope: number;
step4StaaInsdNope: number;
step5StaaInsdNope: number;
step6StaaInsdNope: number;
step7StaaInsdNope: number;
step8StaaInsdNope: number;
staaExcpWhol: number;
staaExcpSttkNprsflCnt: number;
step1StaaExcpSttkFrcCnt: number;
staaExcpSprktpFltrtbCnt: number;
staaExcpAertMthdCnt: number;
staaExcpCntcAertCnt: number;
staaExcpEtcCnt: number;
staaExcpPrcsCpctByWhol: number;
step1StaaExcpNope: number;
step2StaaExcpNope: number;
step3StaaExcpNope: number;
step4StaaExcpNope: number;
step5StaaExcpNope: number;
step6StaaExcpNope: number;
step7StaaExcpNope: number;
step8StaaExcpNope: number;
rmrk: string | null;
}
/** ST00080003 검토 `data.mgmtAreaList[]` 한 행 */
export interface MgmtAreaReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstCd: string;
admdstNm: string | null;
sewerBzmnCd: string;
displayYear: string;
rowType: string;
indvSwagePbancRgnCnt: number;
indvSwageDsgnScpLen: number;
spctnkGrssFcltCnt: number;
spctnkQnt: number;
drtwPrcsFcltGrssFcltCnt: number;
drtwPrcsFcltQnt: number;
rmrk: string | null;
}
export interface PrivateSewageFacilityReviewResponseData {
privateSewageList?: PrivateSewageTreatFactStatusReviewItem[];
septicTankList?: SepticTankReviewItem[];
mgmtAreaList?: MgmtAreaReviewItem[];
statsStatusInfo?: StatsStatusInfo;
}
export type PrivateSewageFacilityReviewApiResponse = ApiResponse<
unknown[] | PrivateSewageFacilityReviewResponseData
>;
export function extractPrivateSewageFacilityReviewList(
tabKey: string,
data: unknown[] | PrivateSewageFacilityReviewResponseData | undefined,
): unknown[] {
if (!data) return [];
if (Array.isArray(data)) return data;
if (tabKey === "ST00080001") {
return data.privateSewageList ?? [];
}
if (tabKey === "ST00080002") {
return data.septicTankList ?? [];
}
if (tabKey === "ST00080003") {
return data.mgmtAreaList ?? [];
}
return [];
}
export function extractPrivateSewageFacilityReviewStatsStatusInfo(
data: unknown[] | PrivateSewageFacilityReviewResponseData | undefined,
): StatsStatusInfo {
if (!data || Array.isArray(data)) return {};
return data.statsStatusInfo ?? {};
}
/** 검토 목록 null → 그리드 표시용 빈 문자열 */
export function mapPrivateSewageFacilityReviewRows(list: unknown[]): object[] {
return list.map((item) => {
const row = item as Record<string, unknown>;
return {
...row,
admdstNm: row.admdstNm ?? "",
rmrk: row.rmrk ?? "",
};
});
}
@@ -1,77 +1,82 @@
import type { ApiResponse } from "./statistics-interface";
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
/**
* 05.중계펌프장 검토 아이템 인터페이스
*/
export interface MediationPumpStationItem {
// 기본 정보
ctpvNm: string;
sggNm: string;
admdstCd: string;
statsYr: string;
displayYear: string;
rowType: 'OLD' | 'CURR';
pumpstNm: string;
impdFcltPstnCn: string;
oprtngStrtYmd: string;
pumpSeNm: string;
swtrmtFcltNm: string;
// 제원 및 계획량
planMltvArea: number;
sndyArea: number;
rainArea: number;
exlnQnt: number;
// 오수펌프 (DRTW)
drtwPumpGrssPpqty: number;
drtwPumpN1DmtrArea: number;
drtwPumpN1Ppqty: number;
drtwPumpN1Cnt: number;
drtwPumpN1RsrvCnt: number;
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
fcltNm: string;
pumpSeNm: string | null;
swtrmtFcltNm: string | null;
fcltPstnCn: string | null;
oprtngStrtYmd: string | null;
planMltvArea: number;
sndyArea: number;
rainArea: number;
exlnQnt: number;
drtwPumpGrssPpqty: number;
drtwPumpN1DmtrArea: number;
drtwPumpN1Ppqty: number;
drtwPumpN1Cnt: number;
drtwPumpN1RsrvCnt: number;
drtwPumpN2DmtrArea: number;
drtwPumpN2Ppqty: number;
drtwPumpN2Cnt: number;
drtwPumpN2RsrvCnt: number;
drtwPumpN3DmtrArea: number;
drtwPumpN3Ppqty: number;
drtwPumpN3Cnt: number;
drtwPumpN3RsrvCnt: number;
// 우수펌프 (EXLN)
exlnPumpGrssPpqty: number;
exlnPumpGrssPpqty: number;
exlnPumpN1DmtrArea: number;
exlnPumpN1Ppqty: number;
exlnPumpN1Cnt: number;
exlnPumpN4RsrvCnt: number;
exlnPumpN1RsrvCnt: number;
exlnPumpN2DmtrArea: number;
exlnPumpN2Ppqty: number;
exlnPumpN2Cnt: number;
exlnPumpN2RsrvCnt: number;
exlnPumpN3DmtrArea: number;
exlnPumpN3Ppqty: number;
exlnPumpN3Cnt: number;
exlnPumpN3RsrvCnt: number;
// 기타
anlElpwrUseQnt: number;
rmrk: string;
anlElpwrUseQnt: number;
rmrk: string | null;
}
export interface MediationPumpStationResponseData {
pumpStationList: MediationPumpStationItem[];
pumpingStationList?: MediationPumpStationItem[];
pumpStationList?: MediationPumpStationItem[];
statsStatusInfo?: StatsStatusInfo;
}
export type MediationPumpStationApiResponse = ApiResponse<
MediationPumpStationItem[] | MediationPumpStationResponseData
>;
export type MediationPumpStationApiResponse = ApiResponse<MediationPumpStationItem[]> & {
message: string;
};
export function extractMediationPumpStationList(
data: MediationPumpStationItem[] | MediationPumpStationResponseData | undefined,
): MediationPumpStationItem[] {
if (!data) return [];
if (Array.isArray(data)) return data;
const list = data.pumpingStationList ?? data.pumpStationList;
return Array.isArray(list) ? list : [];
}
export function extractMediationPumpStationStatsStatusInfo(
data: MediationPumpStationItem[] | MediationPumpStationResponseData | undefined,
): StatsStatusInfo {
if (!data || Array.isArray(data)) return {};
return data.statsStatusInfo ?? {};
}
export interface MediationPumpStationService {
getMediationPumpStationList(params: {
@@ -81,4 +86,4 @@ export interface MediationPumpStationService {
sggNm?: string;
pumpstNm?: string;
}): Promise<MediationPumpStationApiResponse>;
}
}
@@ -0,0 +1,118 @@
import type { ApiResponse } from "./statistics-interface";
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
export interface SewageExcretaRelatedReviewRequestParams {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
}
/** ST00120001·ST00120002 검토 `data.staffList[]` 한 행 */
export interface StaffReviewItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
displayYear: string;
admdstCd: string;
admdstNm: string | null;
rowType: string;
ognzNm: string | null;
empGrssCnt: number;
pbadmsNope: number;
techWholNope: number;
engrkCnt: number;
mchnCnt: number;
prvtmCnt: number;
chmengCnt: number;
envCnt: number;
enjbEtcCnt: number;
etcCnt: number;
rmrk: string | null;
}
/** ST00120003 검토 `data.collectionList[]` 한 행 */
export interface CollectionReviewItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
admdstCd: string;
admdstNm: string | null;
displayYear: string;
rowType: string;
bzentyCnt: number;
empNope: number;
fcltSecrPrstCnt: number;
n3TonUdrCnt: number;
n4TonUdrCnt: number;
n8TonUdrCnt: number;
etcCnt: number;
clctCnvncPrfmncQnt: number;
rmrk: string | null;
}
/** ST00120004 검토 `data.businessList[]` 한 행 */
export interface BusinessReviewItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
admdstCd: string;
admdstNm: string | null;
displayYear: string;
rowType: string;
designMdsrCnt: number;
designMdsrNope: number;
mfbizCnt: number;
mfbizMdsrNope: number;
mngCnt: number;
mngMdsrNope: number;
rmrk: string | null;
}
export interface SewageExcretaRelatedReviewResponseData {
staffList?: StaffReviewItem[];
collectionList?: CollectionReviewItem[];
businessList?: BusinessReviewItem[];
statsStatusInfo?: StatsStatusInfo;
}
export type SewageExcretaRelatedReviewApiResponse = ApiResponse<
unknown[] | SewageExcretaRelatedReviewResponseData
>;
export function extractSewageExcretaRelatedReviewList(
tabKey: string,
data: unknown[] | SewageExcretaRelatedReviewResponseData | undefined,
): unknown[] {
if (!data) return [];
if (Array.isArray(data)) return data;
if (tabKey === "ST00120001" || tabKey === "ST00120002") {
return data.staffList ?? [];
}
if (tabKey === "ST00120003") {
return data.collectionList ?? [];
}
if (tabKey === "ST00120004") {
return data.businessList ?? [];
}
return [];
}
export function extractSewageExcretaRelatedReviewStatsStatusInfo(
data: unknown[] | SewageExcretaRelatedReviewResponseData | undefined,
): StatsStatusInfo {
if (!data || Array.isArray(data)) return {};
return data.statsStatusInfo ?? {};
}
export function mapSewageExcretaRelatedReviewRows(list: unknown[]): object[] {
return list.map((item) => {
const row = item as Record<string, unknown>;
return {
...row,
admdstNm: row.admdstNm ?? "",
ognzNm: row.ognzNm ?? "",
rmrk: row.rmrk ?? "",
};
});
}
@@ -0,0 +1,229 @@
import type { ApiResponse } from "./statistics-interface";
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
export interface SewageExcretaSlurgeReviewRequestParams {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
swtrmtFcltNm?: string;
}
/** ST00110001 검토 `data.sludgeProcessingList[]` 한 행 */
export interface SludgeProcessingReviewItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
swtrmtFcltNm: string | null;
gnamWhol: number;
itslfQnt: number;
otsdInflwQnt: number;
transfTmplntNm: string | null;
transfPrcsQnt: number;
srpFncRt: number;
wholSrpPrcsQnt: number;
itslfTs1Whol: number;
itslfTs1RclmQnt: number;
itslfTs1IncnrQnt: number;
incnrPrcsWhol: number;
incnrPrcsLfvlm: number;
incnrPrcsPrdctRmtrlQnt: number;
incnrPrcsFmlyrQnt: number;
incnrPrcsFcltNm: string | null;
itslfTs1AltdQnt: number;
stbznPrcsWhol: number;
stbznPrcsLfvlm: number;
stbznPrcsFmlyrQnt: number;
stbznPrcsFcltNm: string | null;
itslfTs1DryQnt: number;
dryPrcsWhol: number;
dryPrcsLfvlm: number;
dryPrcsIncnrQnt: number;
dryPrcsFuelQnt: number;
dryPrcsPrdctRmtrlQnt: number;
dryPrcsFmlyrQnt: number;
dryPrcsCmpstgQnt: number;
dryPrcsFcltNm: string | null;
itslfTs1FuelQnt: number;
fuelPrcsWhol: number;
fuelPrcsIncnrQnt: number;
fuelPrcsFuelQnt: number;
fuelPrcsFcltNm: string | null;
itslfTs1CmpstgQnt: number;
cmpsPrcsFcltBzentyNm: string | null;
otsdCnsgnWhol: number;
otsdCnsgnRclmQnt: number;
otsdCnsgnIncnrQnt: number;
otsdCnsgnFuelPwplQnt: number;
otsdCnsgnPrdctRmtrlQnt: number;
otsdCnsgnFmlyrQnt: number;
otsdCnsgnCmpstgQnt: number;
otsdCnsgnFcltNm: string | null;
cparTs2Whol: number;
rmrk: string | null;
}
/** ST00110002 검토 `data.digestionTankOperList[]` 한 행 */
export interface DigestionTankOperReviewItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
swtrmtFcltNm: string | null;
digstnEnYn: string | null;
digstnCpct: number;
actlDrvnCpct: number;
digstnGasInputQnt: number;
digstnGasInputSldDnst: number;
digstnTnkOprtngDayCnt: number;
ocrnGasQnt: number;
digstnGasUtztnWholPrst: number;
digstnGasUtztnNtslQnt: number;
digstnGasUtztnEtgtQnt: number;
digstnGasUtztnItslfQnt: number;
digstnGasUtztnEtcQnt: number;
surplsGasQnt: number;
rmrk: string | null;
}
/** ST00110003 검토 `data.sewageSludgeOperList[]` 한 행 */
export interface SewageSludgeOperReviewItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
fcltNm: string | null;
prcsMthExpln: string | null;
fcltCpct: number | null;
anlPrcsQnt: number | null;
dayAvgOprtngHr: number | null;
anlOprtngDayCnt: number | null;
oprtngRt: number | null;
rmrk: string | null;
}
/** ST00110004 검토 `data.humanWasteSludgeList[]` 한 행 */
export interface HumanWasteSludgeReviewItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
fcltNm: string | null;
gnam: number | null;
wholDspsQnt: number | null;
rcycQnt: number | null;
olandLfvlm: number | null;
incnrQnt: number | null;
oceanInputQnt: number | null;
fuelQnt: number | null;
etcQnt: number | null;
nondspsQnt: number | null;
swtrmtFcltNm: string | null;
rmrk: string | null;
}
export interface SewageExcretaSlurgeReviewResponseData {
sludgeProcessingList?: SludgeProcessingReviewItem[];
digestionTankOperList?: DigestionTankOperReviewItem[];
sewageSludgeOperList?: SewageSludgeOperReviewItem[];
humanWasteSludgeList?: HumanWasteSludgeReviewItem[];
statsStatusInfo?: StatsStatusInfo;
}
export type SewageExcretaSlurgeReviewApiResponse = ApiResponse<
unknown[] | SewageExcretaSlurgeReviewResponseData
>;
export function extractSewageExcretaSlurgeReviewList(
tabKey: string,
data: unknown[] | SewageExcretaSlurgeReviewResponseData | undefined,
): unknown[] {
if (!data) return [];
if (Array.isArray(data)) return data;
if (tabKey === "ST00110001") {
return data.sludgeProcessingList ?? [];
}
if (tabKey === "ST00110002") {
return data.digestionTankOperList ?? [];
}
if (tabKey === "ST00110003") {
return data.sewageSludgeOperList ?? [];
}
if (tabKey === "ST00110004") {
return data.humanWasteSludgeList ?? [];
}
return [];
}
export function extractSewageExcretaSlurgeReviewStatsStatusInfo(
data: unknown[] | SewageExcretaSlurgeReviewResponseData | undefined,
): StatsStatusInfo {
if (!data || Array.isArray(data)) return {};
return data.statsStatusInfo ?? {};
}
export function mapSewageExcretaSlurgeReviewRows(list: unknown[]): object[] {
return list.map((item) => {
const row = item as Record<string, unknown>;
const isHumanWasteSludgeRow = "gnam" in row;
return {
...row,
admdstNm: row.admdstNm ?? "",
fcltNm: isHumanWasteSludgeRow
? (row.fcltNm ?? "")
: (row.fcltNm ?? row.swtrmtFcltNm ?? ""),
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
transfTmplntNm: row.transfTmplntNm ?? "",
incnrPrcsFcltNm: row.incnrPrcsFcltNm ?? "",
stbznPrcsFcltNm: row.stbznPrcsFcltNm ?? "",
dryPrcsFcltNm: row.dryPrcsFcltNm ?? "",
fuelPrcsFcltNm: row.fuelPrcsFcltNm ?? "",
cmpsPrcsFcltBzentyNm: row.cmpsPrcsFcltBzentyNm ?? "",
otsdCnsgnFcltNm: row.otsdCnsgnFcltNm ?? "",
// ST00110002 — API 필드 → review 그리드 컬럼 fieldName
digesterYn: row.digesterYn ?? row.digstnEnYn ?? "",
dsgnCpct: row.dsgnCpct ?? row.digstnCpct ?? 0,
actlOperCpct: row.actlOperCpct ?? row.actlDrvnCpct ?? 0,
inputQnt: row.inputQnt ?? row.digstnGasInputQnt ?? 0,
tsConc: row.tsConc ?? row.digstnGasInputSldDnst ?? 0,
oprtngDays: row.oprtngDays ?? row.digstnTnkOprtngDayCnt ?? 0,
gnrtGasQnt: row.gnrtGasQnt ?? row.ocrnGasQnt ?? 0,
gasTotal: row.gasTotal ?? row.digstnGasUtztnWholPrst ?? 0,
gasSale: row.gasSale ?? row.digstnGasUtztnNtslQnt ?? 0,
gasGen: row.gasGen ?? row.digstnGasUtztnEtgtQnt ?? 0,
gasSelf: row.gasSelf ?? row.digstnGasUtztnItslfQnt ?? 0,
gasEtc: row.gasEtc ?? row.digstnGasUtztnEtcQnt ?? 0,
surplusGas: row.surplusGas ?? row.surplsGasQnt ?? 0,
prcsMthExpln: row.prcsMthExpln ?? "",
fcltCpct: row.fcltCpct ?? 0,
anlPrcsQnt: row.anlPrcsQnt ?? 0,
dayAvgOprtngHr: row.dayAvgOprtngHr ?? 0,
anlOprtngDayCnt: row.anlOprtngDayCnt ?? 0,
oprtngRt: row.oprtngRt ?? 0,
// ST00110004
gnam: row.gnam ?? 0,
wholDspsQnt: row.wholDspsQnt ?? 0,
rcycQnt: row.rcycQnt ?? 0,
olandLfvlm: row.olandLfvlm ?? 0,
incnrQnt: row.incnrQnt ?? 0,
oceanInputQnt: row.oceanInputQnt ?? 0,
fuelQnt: row.fuelQnt ?? 0,
etcQnt: row.etcQnt ?? 0,
nondspsQnt: row.nondspsQnt ?? 0,
rmrk: row.rmrk ?? "",
};
});
}
@@ -0,0 +1,174 @@
import type { ApiResponse } from "./statistics-interface";
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
export interface SewerageFinanceReviewRequestParams {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
}
/** ST00090001 검토 `data.financialSettlementList[]` 한 행 */
export interface FinancialSettlementReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
fnnByWholPjtcoAmt: number;
cnterGovSprtAmt: number;
mctpvLcexpAmt: number;
wareaSggLclAmt: number;
sewerUseAmt: number;
orgprBrdnAmt: number;
etcIncmAmt: number;
crfwAmt: number;
lbltAmt: number;
usgByWholPjtcoAmt: number;
tmplntWholAmt: number;
tmplntFcltAmt: number;
tmplntMendngAmt: number;
tmplntOperKeepAmt: number;
tmplntEtcAmt: number;
wholSwagePplAmt: number;
swagePplFcltAmt: number;
swagePplMendngAmt: number;
swagePplOperKeepAmt: number;
swagePplEtcAmt: number;
wholSwageImpdFcltAmt: number;
swageImpdFcltAmt: number;
swageImpdMendngAmt: number;
swageImpdOperKeepAmt: number;
swageImpdFcltEtcAmt: number;
etcWholAmt: number;
etcAcdntCrfwAmt: number;
etcNrcpAmt: number;
etcAmt: number;
rmrk: string | null;
}
/** ST00090002 검토 `data.sewerageFeeList[]` 한 행 */
export interface SewerageFeeReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
anlLevyQnt: number;
levyAmt: number;
avgUntprcAmt: number;
grssPrmcoAmt: number;
grssUnitPrmcoAmt: number;
rlztnRt: number;
rmrk: string | null;
}
/** ST00090003 검토 `data.usageFeeList[]` 한 행 */
export interface UsageFeeReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
levyAmt: number;
levyQnt: number;
hshusLevyAmt: number;
hshusLevyQnt: number;
hshusUntprcAmt: number;
taskFrLevyAmt: number;
taskFrLevyQnt: number;
taskFrUntprcAmt: number;
bsnGnrlLevyAmt: number;
bsnGnrlLevyQnt: number;
bsnGnrlUntprcAmt: number;
massBthhsFrLevyAmt: number;
massBthhsFrLevyQnt: number;
massBthhsFrUntprcAmt: number;
bthhsFrSpcs2LevyAmt: number;
bthhsFrSpcs2LevyQnt: number;
bthhsFrSpcs2UntprcAmt: number;
induMdsrLevyAmt: number;
induMdsrLevyQnt: number;
induMdsrUntprcAmt: number;
etcLevyAmt: number;
etcLevyQnt: number;
etcLevyUntprcAmt: number;
rmrk: string | null;
}
/** ST00090004 검토 `data.orgCauseList[]` 한 행 */
export interface OrgCauseReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
levyEvntcsCnt: number;
levyAmt: number;
trgtByWholClctnEvntcsCnt: number;
trgtByWholClctnAmt: number;
mltvEqpmEvntcsCnt: number;
mltvEqpmAmt: number;
othrCstrnOthrActrCnt: number;
othrCstrnOthrActrAmt: number;
rmrk: string | null;
}
export interface SewerageFinanceReviewResponseData {
financialSettlementList?: FinancialSettlementReviewItem[];
sewerageFeeList?: SewerageFeeReviewItem[];
usageFeeList?: UsageFeeReviewItem[];
orgCauseList?: OrgCauseReviewItem[];
statsStatusInfo?: StatsStatusInfo;
}
export type SewerageFinanceReviewApiResponse = ApiResponse<
unknown[] | SewerageFinanceReviewResponseData
>;
export function extractSewerageFinanceReviewList(
tabKey: string,
data: unknown[] | SewerageFinanceReviewResponseData | undefined,
): unknown[] {
if (!data) return [];
if (Array.isArray(data)) return data;
if (tabKey === "ST00090001") {
return data.financialSettlementList ?? [];
}
if (tabKey === "ST00090002") {
return data.sewerageFeeList ?? [];
}
if (tabKey === "ST00090003") {
return data.usageFeeList ?? [];
}
if (tabKey === "ST00090004") {
return data.orgCauseList ?? [];
}
return [];
}
export function extractSewerageFinanceReviewStatsStatusInfo(
data: unknown[] | SewerageFinanceReviewResponseData | undefined,
): StatsStatusInfo {
if (!data || Array.isArray(data)) return {};
return data.statsStatusInfo ?? {};
}
export function mapSewerageFinanceReviewRows(list: unknown[]): object[] {
return list.map((item) => {
const row = item as Record<string, unknown>;
return {
...row,
admdstNm: row.admdstNm ?? "",
rmrk: row.rmrk ?? "",
};
});
}
@@ -0,0 +1,101 @@
import type { ApiResponse } from "./statistics-interface";
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
export interface SewerageServiceLevelReviewRequestParams {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
swtrmtFcltNm?: string;
}
/** ST00130001 검토 `data.complaintList[]` 한 행 */
export interface ComplaintReviewItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
admdstCd: string;
admdstNm: string | null;
displayYear: string;
rowType: string;
swtrmtPpltnCnt: number;
whol: number;
odorCnt: number;
swagePplCnt: number;
dschgWqCnt: number;
ldsCnt: number;
plltnAcdntCnt: number;
sewerCrgAmt: number;
etcCnt: number;
crtrPpltnCvlcptEvntcsCnt: number;
rmrk: string | null;
}
/** ST00130002 검토 `data.facilityList[]` 한 행 */
export interface ResidentFriendlyFacilityReviewItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
admdstCd: string;
admdstNm: string | null;
displayYear: string;
rowType: string;
swtrmtFcltNm: string | null;
fcltCpct: number;
tmplntArea: number;
whol: number;
brkFcltArea: number;
childFrshpSpacArea: number;
bdySportsFcltArea: number;
sportsFcltArea: number;
pkltArea: number;
eduMultiFcltArea: number;
wtlndMakeArea: number;
etcCnt: number;
rmrk: string | null;
}
export interface SewerageServiceLevelReviewResponseData {
complaintList?: ComplaintReviewItem[];
facilityList?: ResidentFriendlyFacilityReviewItem[];
statsStatusInfo?: StatsStatusInfo;
}
export type SewerageServiceLevelReviewApiResponse = ApiResponse<
unknown[] | SewerageServiceLevelReviewResponseData
>;
export function extractSewerageServiceLevelReviewList(
tabKey: string,
data: unknown[] | SewerageServiceLevelReviewResponseData | undefined,
): unknown[] {
if (!data) return [];
if (Array.isArray(data)) return data;
if (tabKey === "ST00130001") {
return data.complaintList ?? [];
}
if (tabKey === "ST00130002") {
return data.facilityList ?? [];
}
return [];
}
export function extractSewerageServiceLevelReviewStatsStatusInfo(
data: unknown[] | SewerageServiceLevelReviewResponseData | undefined,
): StatsStatusInfo {
if (!data || Array.isArray(data)) return {};
return data.statsStatusInfo ?? {};
}
export function mapSewerageServiceLevelReviewRows(list: unknown[]): object[] {
return list.map((item) => {
const row = item as Record<string, unknown>;
return {
...row,
rowType: row.rowType ?? "",
admdstNm: row.admdstNm ?? "",
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
rmrk: row.rmrk ?? "",
};
});
}
@@ -0,0 +1,212 @@
import type { ApiResponse } from "./statistics-interface";
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
export interface WaterReuseReviewRequestParams {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
swtrmtFcltNm?: string;
}
/** ST00100001 검토 `data.reuseStatusList[]` 한 행 */
export interface ReuseStatusReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
swtrmtFcltNm: string;
sncnYmd: string | null;
prcsFcltCpct: number | string;
prcsQnt: number;
lglYn: string | null;
instlYn: string | null;
oprtngStrtYmd: string | null;
instlAprvYmd: string | null;
rprcsFcltCpct: number;
mtcnsNm: string | null;
ruswtPplLen: number;
ruswtPumpLen: number;
wholUseQnt: number;
ithlWshwtrQnt: number;
ithlClnQty: number;
ithlFrzgQty: number;
ithlLdhcUswtrQnt: number;
ithlDltnUswtrQnt: number;
ithlRuswtrQnt: number;
ithlEtcQnt: number;
ithlUnsdQnt: number;
otcntrUswtrWhol: number;
otcntrClnRstrmQnt: number;
otcntrWshwtrQnt: number;
otcntrLdhcUswtrQnt: number;
otcntrHdrplUswtrQnt: number;
otcntrRvrKeepQnt: number;
agrcltUswtrPstnQnt: number;
ugwtrChargQnt: number;
mdsrUswtrQnt: number;
rtlztnRt: number;
rmrk: string | null;
}
/** ST00100002 검토 `data.grayWaterList[]` 한 행 */
export interface GrayWaterReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
fcltNm: string;
fcltPstnCn: string | null;
bldgSeCn: string | null;
lglTrgtYn: string | null;
cmptnStrtYmd: string | null;
oprtngStrtYmd: string | null;
archWholArea: number;
prcsCpct: number;
wtrUseQnt: number;
ruswtrUtztnQnt: number;
sbstCntUtztnQnt: number;
mnUsgCn: string | null;
ruswtrOperAmt: number;
rmrk: string | null;
}
/** ST00100003 검토 `data.rainwaterList[]` 한 행 */
export interface RainwaterReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
fcltNm: string;
bdstTypCn: string | null;
fcltPstnCn: string | null;
lglTrgtYn: string | null;
instlCmptnYmd: string | null;
instlAmt: number;
rcptMynCn: string | null;
filtrPrcsFcltYnExpln: string | null;
impdCpct: number;
anlUseQnt: number;
exlnUtlzUsgExpln: string | null;
anlOperAmt: number;
rmrk: string | null;
}
/** ST00100004 검토 `data.regStatusList[]` 한 행 */
export interface RegStatusReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
bplcNm: string | null;
rprsvNm: string | null;
bplcPstnCn: string | null;
regDt: string | null;
rmrk: string | null;
}
/** ST00100005 검토 `data.sncnStatusList[]` 한 행 */
export interface SncnStatusReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: string;
bplcNm: string | null;
rprsvNm: string | null;
bplcPstnCn: string | null;
coRegYmd: string | null;
rmrk: string | null;
}
export interface WaterReuseReviewResponseData {
reuseStatusList?: ReuseStatusReviewItem[];
grayWaterList?: GrayWaterReviewItem[];
rainwaterList?: RainwaterReviewItem[];
regStatusList?: RegStatusReviewItem[];
sncnStatusList?: SncnStatusReviewItem[];
statsStatusInfo?: StatsStatusInfo;
}
export type WaterReuseReviewApiResponse = ApiResponse<
unknown[] | WaterReuseReviewResponseData
>;
export function extractWaterReuseReviewList(
tabKey: string,
data: unknown[] | WaterReuseReviewResponseData | undefined,
): unknown[] {
if (!data) return [];
if (Array.isArray(data)) return data;
if (tabKey === "ST00100001") {
return data.reuseStatusList ?? [];
}
if (tabKey === "ST00100002") {
return data.grayWaterList ?? [];
}
if (tabKey === "ST00100003") {
return data.rainwaterList ?? [];
}
if (tabKey === "ST00100004") {
return data.regStatusList ?? [];
}
if (tabKey === "ST00100005") {
return data.sncnStatusList ?? [];
}
return [];
}
export function extractWaterReuseReviewStatsStatusInfo(
data: unknown[] | WaterReuseReviewResponseData | undefined,
): StatsStatusInfo {
if (!data || Array.isArray(data)) return {};
return data.statsStatusInfo ?? {};
}
export function mapWaterReuseReviewRows(list: unknown[]): object[] {
return list.map((item) => {
const row = item as Record<string, unknown>;
return {
...row,
admdstNm: row.admdstNm ?? "",
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
sncnYmd: row.sncnYmd ?? "",
lglYn: row.lglYn ?? "",
instlYn: row.instlYn ?? "",
oprtngStrtYmd: row.oprtngStrtYmd ?? "",
instlAprvYmd: row.instlAprvYmd ?? "",
mtcnsNm: row.mtcnsNm ?? "",
fcltNm: row.fcltNm ?? "",
fcltPstnCn: row.fcltPstnCn ?? "",
bldgSeCn: row.bldgSeCn ?? "",
lglTrgtYn: row.lglTrgtYn ?? "",
cmptnStrtYmd: row.cmptnStrtYmd ?? "",
bdstTypCn: row.bdstTypCn ?? "",
instlCmptnYmd: row.instlCmptnYmd ?? "",
rcptMynCn: row.rcptMynCn ?? "",
filtrPrcsFcltYnExpln: row.filtrPrcsFcltYnExpln ?? "",
exlnUtlzUsgExpln: row.exlnUtlzUsgExpln ?? "",
bplcNm: row.bplcNm ?? "",
rprsvNm: row.rprsvNm ?? "",
bplcPstnCn: row.bplcPstnCn ?? "",
regDt: row.regDt ?? "",
coRegYmd: row.coRegYmd ?? "",
mnUsgCn: row.mnUsgCn ?? "",
rmrk: row.rmrk ?? "",
};
});
}
@@ -73,9 +73,11 @@ const StatisticsPage = () => {
const DynamicComponent = dynamic(
() =>
import(
`@/components/statistics/${category}/${resolvedComponentName}`
),
category
? import(
`@/components/statistics/${category}/${resolvedComponentName}`
)
: import(`@/components/statistics/${resolvedComponentName}`),
{ ssr: false, loading: () => <SkeletonPage /> },
);
@@ -55,7 +55,7 @@ export function resolveStatisticsTabId(urlPath: string): string {
return componentName || rawUrlPath;
}
/** `@/components/statistics/{category}/{Component}` 경로 */
/** `@/components/statistics/{category}/{Component}` 또는 루트 컴포넌트 경로 */
export function buildStatisticsComponentImportPath(
urlPath: string,
parentEngNm: string,
@@ -63,6 +63,9 @@ export function buildStatisticsComponentImportPath(
const { componentName } = parseStatisticsMenuUrlPath(urlPath);
const category = resolveStatisticsCategory(urlPath, parentEngNm);
const name = componentName || urlPath;
if (!category) {
return `@/components/statistics/${name}`;
}
return `@/components/statistics/${category}/${name}`;
}
+168 -29
View File
@@ -1,10 +1,57 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTabStore } from "@/store/tab-store";
import { Tabs, TabsList, TabsTrigger } from "@common/components";
type TabContextMenuState = {
x: number;
y: number;
tabId: string;
};
const TabBar = () => {
const { tabs, activeTabId, setActiveTab, removeTab } = useTabStore();
const {
tabs,
activeTabId,
setActiveTab,
removeTab,
closeAllTabs,
closeOtherTabs,
closeTabsToLeft,
closeTabsToRight,
} = useTabStore();
const [contextMenu, setContextMenu] = useState<TabContextMenuState | null>(null);
const contextMenuRef = useRef<HTMLDivElement | null>(null);
const closeContextMenu = useCallback(() => {
setContextMenu(null);
}, []);
useEffect(() => {
if (!contextMenu) return;
const onPointerDown = (event: PointerEvent) => {
if (contextMenuRef.current?.contains(event.target as Node)) return;
closeContextMenu();
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") closeContextMenu();
};
const onScroll = () => closeContextMenu();
window.addEventListener("pointerdown", onPointerDown);
window.addEventListener("keydown", onKeyDown);
window.addEventListener("scroll", onScroll, true);
return () => {
window.removeEventListener("pointerdown", onPointerDown);
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("scroll", onScroll, true);
};
}, [closeContextMenu, contextMenu]);
if (tabs.length === 0) {
return null;
@@ -18,36 +65,128 @@ const TabBar = () => {
removeTab(tabId);
};
const handleTabContextMenu = (
e: React.MouseEvent<HTMLButtonElement>,
tabId: string,
) => {
e.preventDefault();
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, tabId });
};
const handleCloseOtherTabs = () => {
if (!contextMenu || tabs.length <= 1) return;
closeOtherTabs(contextMenu.tabId);
closeContextMenu();
};
const handleCloseAllTabs = () => {
closeAllTabs();
closeContextMenu();
};
const contextTabIndex = contextMenu
? tabs.findIndex((tab) => tab.id === contextMenu.tabId)
: -1;
const canCloseTabsToLeft = contextTabIndex > 0;
const canCloseTabsToRight =
contextTabIndex >= 0 && contextTabIndex < tabs.length - 1;
const canCloseOtherTabs = tabs.length > 1;
const handleCloseTabsToLeft = () => {
if (!contextMenu || !canCloseTabsToLeft) return;
closeTabsToLeft(contextMenu.tabId);
closeContextMenu();
};
const handleCloseTabsToRight = () => {
if (!contextMenu || !canCloseTabsToRight) return;
closeTabsToRight(contextMenu.tabId);
closeContextMenu();
};
return (
<Tabs
value={activeTabId || undefined}
onValueChange={setActiveTab}
className="w-full border-b border-border"
variant="outline"
>
<TabsList className="w-full !justify-start !p-4 !pb-0">
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
className="!flex !w-[140px] !items-center !justify-start !gap-2 !px-4 [&::after]:hidden"
title={tab.label}
>
<span className="flex-1 truncate text-xl leading-none">
{tab.label}
</span>
<span
role="button"
onClick={(e) => handleCloseTab(e, tab.id)}
aria-label={`${tab.label} 탭 닫기`}
className="flex flex-shrink-0 items-center justify-center leading-none hover:text-[#2094fa]"
<>
<Tabs
value={activeTabId || undefined}
onValueChange={setActiveTab}
className="w-full border-b border-border"
variant="outline"
>
<TabsList className="w-full !justify-start !p-4 !pb-0">
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
className="!flex !w-[140px] !items-center !justify-start !gap-2 !px-4 [&::after]:hidden"
title={tab.label}
onContextMenu={(e) => handleTabContextMenu(e, tab.id)}
>
<i className="ri-close-line" />
</span>
</TabsTrigger>
))}
</TabsList>
</Tabs>
<span className="flex-1 truncate text-xl leading-none">
{tab.label}
</span>
<span
role="button"
onClick={(e) => handleCloseTab(e, tab.id)}
aria-label={`${tab.label} 탭 닫기`}
className="flex flex-shrink-0 items-center justify-center leading-none hover:text-[#2094fa]"
>
<i className="ri-close-line" />
</span>
</TabsTrigger>
))}
</TabsList>
</Tabs>
{contextMenu &&
typeof document !== "undefined" &&
createPortal(
<div
ref={contextMenuRef}
role="menu"
className="fixed z-[100] min-w-[20rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md"
style={{ left: contextMenu.x, top: contextMenu.y }}
onContextMenu={(e) => e.preventDefault()}
>
<button
type="button"
role="menuitem"
disabled={!canCloseTabsToLeft}
className="flex w-full cursor-default select-none items-center rounded-sm px-4 py-3 text-left text-[1.2rem] outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
onClick={handleCloseTabsToLeft}
>
왼쪽 탭 모두 닫기
</button>
<button
type="button"
role="menuitem"
disabled={!canCloseTabsToRight}
className="flex w-full cursor-default select-none items-center rounded-sm px-4 py-3 text-left text-[1.2rem] outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
onClick={handleCloseTabsToRight}
>
오른쪽 탭 모두 닫기
</button>
<button
type="button"
role="menuitem"
disabled={!canCloseOtherTabs}
className="flex w-full cursor-default select-none items-center rounded-sm px-4 py-3 text-left text-[1.2rem] outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
onClick={handleCloseOtherTabs}
>
해당 탭 제외 모두 닫기
</button>
<button
type="button"
role="menuitem"
className="flex w-full cursor-default select-none items-center rounded-sm px-4 py-3 text-left text-[1.2rem] outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
onClick={handleCloseAllTabs}
>
모든 탭 닫기
</button>
</div>,
document.body,
)}
</>
);
};
+42
View File
@@ -89,6 +89,8 @@ interface TabStore {
setMenuList: (menuList: MenuItem[] | null) => void;
closeAllTabs: () => void;
closeOtherTabs: (tabId: string) => void;
closeTabsToLeft: (tabId: string) => void;
closeTabsToRight: (tabId: string) => void;
}
export const TAB_STORE_PERSIST_KEY = "st-tab-store";
@@ -240,6 +242,46 @@ const useTabStoreHook = create<TabStore>()(
});
}
},
closeTabsToLeft: (tabId: string) => {
const { tabs, activeTabId } = get();
const index = tabs.findIndex((tab) => tab.id === tabId);
if (index <= 0) return;
const newTabs = tabs.slice(index);
let newActiveTabId = activeTabId;
if (activeTabId && !newTabs.some((tab) => tab.id === activeTabId)) {
newActiveTabId = tabId;
}
set({
tabs: newTabs,
activeTabId: newActiveTabId,
});
if (newActiveTabId) {
const nextActive = get().tabs.find((t) => t.id === newActiveTabId);
if (nextActive) ensureTabPageMenuAuthority(get, set, nextActive);
}
},
closeTabsToRight: (tabId: string) => {
const { tabs, activeTabId } = get();
const index = tabs.findIndex((tab) => tab.id === tabId);
if (index === -1 || index >= tabs.length - 1) return;
const newTabs = tabs.slice(0, index + 1);
let newActiveTabId = activeTabId;
if (activeTabId && !newTabs.some((tab) => tab.id === activeTabId)) {
newActiveTabId = tabId;
}
set({
tabs: newTabs,
activeTabId: newActiveTabId,
});
if (newActiveTabId) {
const nextActive = get().tabs.find((t) => t.id === newActiveTabId);
if (nextActive) ensureTabPageMenuAuthority(get, set, nextActive);
}
},
}),
{
name: TAB_STORE_PERSIST_KEY,