Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 3m57s

This commit is contained in:
2026-06-30 16:02:29 +09:00
83 changed files with 10273 additions and 1633 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) 최우선 적용.
+129 -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 {
@@ -662,6 +720,7 @@ const DataGrid = ({
onCellEdited,
onRowClick,
onDataCellClick,
onCellDblClick,
onItemChecked,
onItemAllChecked,
onScrollToBottom,
@@ -670,6 +729,7 @@ const DataGrid = ({
idFieldName = "id",
enableSort = false,
gridHeight = 55, // vh 단위
onRowDblClick,
headerHeight = 60,
fitStyle = GridFitStyle.EVEN,
}: DataGridProps) => {
@@ -708,6 +768,7 @@ const DataGrid = ({
onCellEditedRef.current = onCellEdited;
onDataCellClickRef.current = onDataCellClick;
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
useEffect(() => {
if (!gridRef.current?.gridView || highlightedRowId == null) return;
@@ -723,11 +784,38 @@ const DataGrid = ({
gridView.setFocus();
}
}, [highlightedRowId, rowData, idFieldName]);
// 행 더블클릭 이벤트
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (!gridView || !onCellDblClick) return;
(gridView as any).onCellDblClicked = (_grid: unknown, clickData: { cellType?: string; itemIndex?: number }) => {
if (clickData.cellType !== "data" || clickData.itemIndex === undefined) return;
const row = rowDataRef.current[clickData.itemIndex];
const cellIndex = (gridView as any).getCurrent?.();
const fieldName = cellIndex?.fieldName ?? cellIndex?.column ?? "";
if (row) onCellDblClick({ // ← 여기는 Props 콜백 호출이라 그대로
row: row as Record<string, string | number>,
fieldName,
itemIndex: clickData.itemIndex
});
};
return () => {
(gridView as any).onCellDblClicked = undefined;
};
}, [onCellDblClick]);
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 +1137,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 +1281,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 +1402,7 @@ const DataGrid = ({
};
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
setTimeout(() => {
scheduleFooterTask(() => {
if (gridRef.current?.gridView && columnLayout) {
const safeLayout = sanitizeColumnLayout(columnLayout as unknown[]);
if (safeLayout.length === 0) return;
@@ -1327,10 +1421,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 +1478,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 +1531,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 +1603,7 @@ const DataGrid = ({
}
return () => {
footerTimerIds.forEach(clearTimeout);
if (getGridDataRef) getGridDataRef.current = null;
};
}, [
@@ -1939,19 +2048,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 +2089,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);
}}
+2 -2
View File
@@ -184,7 +184,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
const handleSidoChange = (value: string) => {
const selectedBsnm =
sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || "";
sidoList?.data?.sidoList?.find((v) => v.sido === value)?.bsnmCd || "";
setValues((prev) => ({
...prev,
sido: value,
@@ -197,7 +197,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
const listForBsnm = sggList?.data ?? [];
const selectedBsnm =
value === "ALL"
? sidoList?.data.sidoList.find((v) => v.sido === values.sido)
? sidoList?.data?.sidoList?.find((v) => v.sido === values.sido)
?.bsnmCd || ""
: listForBsnm.find((v) => v.gugun === value)?.bsnmCd || "";
@@ -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 });
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"
>
@@ -38,7 +38,7 @@ import type {
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { formatCompactYmdForGrid, toCompactYmd } from "@/components/util/grid/ymdFormat";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -199,8 +199,8 @@ function toGridNumber(value: string | number | null | undefined): number {
return Number.isFinite(n) ? n : 0;
}
function toApiYmd(value: string | number | Date | null | undefined): string {
return toCompactYmd(value);
function toApiYmd(value: string | number | Date | null | undefined): string | null {
return toCompactYmdOrNull(value);
}
function resolveExcretaTreatmentFacilityAdmdst(
@@ -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,
@@ -38,7 +38,7 @@ import type {
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { formatCompactYmdForGrid, toCompactYmd } from "@/components/util/grid/ymdFormat";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { statisticsInputPumpingStationApi } from "@/entities/statistics/api/input/statistics-input-pumping-station-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -230,7 +230,7 @@ function buildPumpingStationSaveItem(
fcltNm: String(row.fcltNm ?? ""),
pumpSeNm: String(row.pumpSeNm ?? ""),
fcltPstnCn: String(row.fcltPstnCn ?? ""),
oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
planMltvArea: toGridNumber(row.planMltvArea),
sndyArea: toGridNumber(row.sndyArea),
rainArea: toGridNumber(row.rainArea),
@@ -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";
@@ -46,7 +45,7 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
import {
formatCompactYmdForGrid,
normalizeOxYnValue,
toCompactYmd,
toCompactYmdOrNull,
} from "@/components/util/grid/ymdFormat";
import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
@@ -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,
@@ -313,7 +308,7 @@ function buildSewageReservoirSaveItem(
fcltSn: fcltSn,
fcltNm: String(row.fcltNm ?? ""),
fcltPstnCn: String(row.fcltPstnCn ?? ""),
oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt),
fldngPrvtnYn: normalizeOxYnValue(row.fldngPrvtnYn),
wqPlltnMtgtYn: normalizeOxYnValue(row.wqPlltnMtgtYn),
@@ -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),
@@ -368,22 +363,22 @@ function buildRainfallPumpStationSaveItem(
fcltNm: String(row.fcltNm ?? ""),
fcltPstnCn: String(row.fcltPstnCn ?? ""),
fcltyKndCn: String(row.fcltyKndCn ?? ""),
oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt),
wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea),
rsvrYn: String(row.rsvrYn ? (row.rsvrYn === "무" ? "N" : "Y") : ""),
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),
@@ -38,7 +38,7 @@ import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/wat
import { TreatWaterReuseBusinessApprStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn";
import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { formatCompactYmdForGrid, toCompactYmd } from "@/components/util/grid/ymdFormat";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { statisticsInputWaterReuseApi } from "@/entities/statistics/api/input/statistics-input-water-reuse-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -297,8 +297,8 @@ function buildRecycledWaterStatusSaveItem(
bldgSeCn: String(row.bldgSeCn ?? "").trim(),
fcltPstnCn: String(row.fcltPstnCn ?? "").trim(),
lglTrgtYn: String(row.lglTrgtYn ?? "").trim(),
cmptnStrtYmd: toCompactYmd(row.cmptnStrtYmd),
oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd),
cmptnStrtYmd: toCompactYmdOrNull(row.cmptnStrtYmd),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
archWholArea: toGridNumber(row.archWholArea),
prcsCpct: toGridNumber(row.prcsCpct),
wtrUseQnt: toGridNumber(row.wtrUseQnt),
@@ -323,7 +323,7 @@ function buildRainFallUtilFactStatusSaveItem(
bdstTypCn: String(row.bdstTypCn ?? "").trim(),
fcltPstnCn: String(row.fcltPstnCn ?? "").trim(),
lglTrgtYn: String(row.lglTrgtYn ?? "").trim(),
cmptYmd: toCompactYmd(row.cmptYmd),
cmptYmd: toCompactYmdOrNull(row.cmptYmd),
instlAmt: toGridNumber(row.instlAmt),
rcptMynCn: String(row.rcptMynCn ?? "").trim(),
filtrPrcsFcltYnExpln: String(row.filtrPrcsFcltYnExpln ?? "").trim(),
@@ -347,7 +347,7 @@ function buildTreatWaterReuseDesignConstStatusSaveItem(
bplcNm: String(row.bplcNm ?? "").trim(),
rprsvNm: String(row.rprsvNm ?? "").trim(),
bplcPstnCn: String(row.bplcPstnCn ?? "").trim(),
coRegYmd: toCompactYmd(row.coRegYmd),
coRegYmd: toCompactYmdOrNull(row.coRegYmd),
rmrk: nullableTrimmedString(row.rmrk),
};
}
@@ -364,7 +364,7 @@ function buildTreatWaterReuseBusinessApprStatusSaveItem(
bplcNm: String(row.bplcNm ?? "").trim(),
rprsvNm: String(row.rprsvNm ?? "").trim(),
bplcPstnCn: String(row.bplcPstnCn ?? "").trim(),
coRegYmd: toCompactYmd(row.coRegYmd),
coRegYmd: toCompactYmdOrNull(row.coRegYmd),
rmrk: nullableTrimmedString(row.rmrk),
};
}
@@ -381,8 +381,8 @@ function buildPublicSewageTreatWaterReuseSaveItem(
wholPrcsQnt: toGridNumber(row.wholPrcsQnt),
lglYn: String(row.lglYn ?? "").trim(),
instlYn: String(row.instlYn ?? "").trim(),
oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd),
instlAprvYmd: toCompactYmd(row.instlAprvYmd),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
instlAprvYmd: toCompactYmdOrNull(row.instlAprvYmd),
rprcsFcltCpct: toGridNumber(row.rprcsFcltCpct),
mtcnsNm: String(row.mtcnsNm ?? "").trim(),
ruswtPplLen: toGridNumber(row.ruswtPplLen),
@@ -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 () => {
@@ -0,0 +1,103 @@
import { Button } from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import {
AdmdstRenewalReducedColumnConfig,
AdmdstRenewalAddedColumnConfig,
} from "@/components/util/column/management/AdmdstRenewalColumn";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import type { AdmdstRenewalAddedItem } from "@/components/statistics/management/types/population-tab-types";
const reducedColumnInfo: ColumnInfo = {
columns: AdmdstRenewalReducedColumnConfig.columns ?? [],
columnLayout: AdmdstRenewalReducedColumnConfig.columnLayout ?? [],
};
const addedColumnInfo: ColumnInfo = {
columns: AdmdstRenewalAddedColumnConfig.columns ?? [],
columnLayout: AdmdstRenewalAddedColumnConfig.columnLayout ?? [],
};
type Props = {
tab: ReturnType<typeof useAdmdstRenewalTab>;
statsYr: string;
};
const AdmdstRenewalTab = ({ tab, statsYr }: Props) => {
return (
<div className="flex gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[1.1rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
감축된 행정구역(범위:동읍면) 자료
</span>
</div>
<div className="h-[3.4rem]" />
<DataGrid
columnInfo={reducedColumnInfo}
rowStartNum={AdmdstRenewalReducedColumnConfig.rowStartNum}
excelExportFileSuffix="감축된행정구역"
rowData={asGridRowData(tab.reducedRowData)}
setRowData={asGridRowSetter(tab.setReducedRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={AdmdstRenewalReducedColumnConfig.fixedColumns}
enableSort={false}
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[0.4rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
추가된 행정구역 자료
</span>
</div>
<div className="flex justify-end pr-[1rem] mb-[0.4rem]">
<Button
type="button"
color="primary"
disabled={tab.isLoading || (tab.reducedRowData.length === 0 && tab.addedRowData.length === 0)}
onClick={() => void tab.handleFinal(statsYr)}
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
>
반영
</Button>
</div>
<DataGrid
columnInfo={addedColumnInfo}
rowStartNum={AdmdstRenewalAddedColumnConfig.rowStartNum}
excelExportFileSuffix="추가된행정구역"
rowData={asGridRowData(tab.addedRowData)}
setRowData={asGridRowSetter(tab.setAddedRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={AdmdstRenewalAddedColumnConfig.fixedColumns}
enableSort={false}
onCellDblClick={({ row, fieldName }) => {
if (fieldName !== "lawCd") return; // lawCd 셀만!
void tab.handleRowClick(fromGridRow<AdmdstRenewalAddedItem>(row), statsYr);
}}
/>
</div>
<UpperCodeSelectDialog
open={tab.dialogOpen}
onOpenChange={tab.setDialogOpen}
candidates={tab.candidates}
codeField="lawCd"
nameField="lawNm"
extraFields={[
{ field: "rgnCd", header: "지역코드" },
{ field: "lv", header: "레벨" },
]}
onSelect={tab.handleCodeSelect}
/>
</div>
);
};
export default AdmdstRenewalTab;
@@ -0,0 +1,134 @@
import { Button } from "@common/components";
import type { RealGridReact } from "realgrid-react";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import {
DomesticPopulationLeftColumnConfig,
DomesticPopulationRightColumnConfig,
} from "@/components/util/column/management/DomesticPopulationColumn";
import { useRef } from "react";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
const leftColumnInfo: ColumnInfo = {
columns: DomesticPopulationLeftColumnConfig.columns ?? [],
columnLayout: DomesticPopulationLeftColumnConfig.columnLayout ?? [],
};
const rightColumnInfo: ColumnInfo = {
columns: DomesticPopulationRightColumnConfig.columns ?? [],
columnLayout: DomesticPopulationRightColumnConfig.columnLayout ?? [],
};
type DomesticTab = ReturnType<typeof usePopulationMappingTab<DomesticPopulationItem>>;
type Props = {
tab: DomesticTab;
statsYr: string;
onFileButtonClick: () => void;
};
const DomesticPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
const rightGridRef = useRef<RealGridReact | null>(null);
return (
<div className="flex gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[0.4rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
엑셀 업로드 자료 (내국인 전체 리스트)
</span>
</div>
<div className="flex justify-end pr-2 mb-[0.4rem]">
<Button
type="button"
onClick={onFileButtonClick}
className="mt-[0.5rem] px-6 py-[0.8rem] text-[1.4rem] bg-gradient-green hover:bg-gradient-green-hover"
>
<i className="diveicon di-file-excel" />
엑셀 업로드
</Button>
</div>
<DataGrid
columnInfo={leftColumnInfo}
rowStartNum={DomesticPopulationLeftColumnConfig.rowStartNum}
excelExportFileSuffix="내국인전체리스트"
rowData={asGridRowData(tab.leftRowData)}
setRowData={asGridRowSetter(tab.setLeftRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={DomesticPopulationLeftColumnConfig.fixedColumns}
enableSort={false}
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[0.4rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
미매핑 리스트 (작년 대비)
</span>
</div>
<div className="flex justify-end pr-[1rem] mb-[0.4rem]">
<Button
type="button"
color="primary"
disabled={tab.isLoading || tab.rightRowData.length === 0}
onClick={() => void tab.handleFinal(statsYr)}
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
>
반영
</Button>
</div>
<DataGrid
columnInfo={rightColumnInfo}
rowStartNum={DomesticPopulationRightColumnConfig.rowStartNum}
excelExportFileSuffix="미매핑리스트"
rowData={asGridRowData(tab.rightRowData)}
setRowData={asGridRowSetter(tab.setRightRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={DomesticPopulationRightColumnConfig.fixedColumns}
enableSort={false}
getGridRef={rightGridRef} // ← 추가
onDataCellClick={({ row, fieldName }) => {
if (fieldName === "mpinCd") return;
if (fieldName === "admdstCd") {
(rightGridRef.current?.gridView as any)?.commit?.(); // ← commit 먼저
setTimeout(() => {
tab.setRightRowData((prev) =>
prev.map((r) =>
r.admdstCd === row.admdstCd
? { ...r, mpinCd: String(row.admdstCd ?? "") }
: r,
),
);
}, 0);
return;
}
}}
onCellDblClick={({ row, fieldName }) => {
if (fieldName !== "mpinCd") return;
void tab.handleRightRowClick(fromGridRow<DomesticPopulationItem>(row), statsYr);
}}
/>
</div>
<UpperCodeSelectDialog
open={tab.dialogOpen}
onOpenChange={tab.setDialogOpen}
candidates={tab.candidates}
codeField="admdstCd"
nameField="admdstNm"
onSelect={tab.handleCodeSelect}
/>
</div>
);
};
export default DomesticPopulationTab;
@@ -0,0 +1,132 @@
import { Button } from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import { useRef } from "react";
import type { RealGridReact } from "realgrid-react";
import {
ForeignPopulationLeftColumnConfig,
ForeignPopulationRightColumnConfig,
} from "@/components/util/column/management/ForeignPopulationColumn";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types";
const leftColumnInfo: ColumnInfo = {
columns: ForeignPopulationLeftColumnConfig.columns ?? [],
columnLayout: ForeignPopulationLeftColumnConfig.columnLayout ?? [],
};
const rightColumnInfo: ColumnInfo = {
columns: ForeignPopulationRightColumnConfig.columns ?? [],
columnLayout: ForeignPopulationRightColumnConfig.columnLayout ?? [],
};
type ForeignTab = ReturnType<typeof usePopulationMappingTab<ForeignPopulationItem>>;
type Props = {
tab: ForeignTab;
statsYr: string;
onFileButtonClick: () => void;
};
const ForeignPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
const rightGridRef = useRef<RealGridReact | null>(null);
return (
<div className="flex gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[0.4rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
엑셀 업로드 자료 (외국인 전체 리스트)
</span>
</div>
<div className="flex justify-end pr-2 mb-[0.4rem]">
<Button
type="button"
onClick={onFileButtonClick}
className="mt-[0.5rem] px-6 py-[0.8rem] text-[1.4rem] bg-gradient-green hover:bg-gradient-green-hover"
>
<i className="diveicon di-file-excel" />
엑셀 업로드
</Button>
</div>
<DataGrid
columnInfo={leftColumnInfo}
rowStartNum={ForeignPopulationLeftColumnConfig.rowStartNum}
excelExportFileSuffix="외국인전체리스트"
rowData={asGridRowData(tab.leftRowData)}
setRowData={asGridRowSetter(tab.setLeftRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={ForeignPopulationLeftColumnConfig.fixedColumns}
enableSort={false}
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[0.4rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
미매핑 리스트 (작년 대비)
</span>
</div>
<div className="flex justify-end pr-[1rem] mb-[0.4rem]">
<Button
type="button"
color="primary"
disabled={tab.isLoading || tab.rightRowData.length === 0}
onClick={() => void tab.handleFinal(statsYr)}
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
>
반영
</Button>
</div>
<DataGrid
columnInfo={rightColumnInfo}
rowStartNum={ForeignPopulationRightColumnConfig.rowStartNum}
excelExportFileSuffix="외국인미매핑리스트"
rowData={asGridRowData(tab.rightRowData)}
setRowData={asGridRowSetter(tab.setRightRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={ForeignPopulationRightColumnConfig.fixedColumns}
enableSort={false}
getGridRef={rightGridRef}
onDataCellClick={({ row, fieldName }) => {
if (fieldName === "mpinCd") return;
if (fieldName === "admdstCd") {
(rightGridRef.current?.gridView as any)?.commit?.();
setTimeout(() => {
tab.setRightRowData((prev) =>
prev.map((r) =>
r.admdstCd === row.admdstCd
? { ...r, mpinCd: String(row.admdstCd ?? "") }
: r,
),
);
}, 0);
return;
}
}}
onCellDblClick={({ row, fieldName }) => {
if (fieldName !== "mpinCd") return;
void tab.handleRightRowClick(fromGridRow<ForeignPopulationItem>(row), statsYr);
}}
/>
</div>
<UpperCodeSelectDialog
open={tab.dialogOpen}
onOpenChange={tab.setDialogOpen}
candidates={tab.candidates}
codeField="admdstCd"
nameField="admdstNm"
onSelect={tab.handleCodeSelect}
/>
</div>
);
};
export default ForeignPopulationTab;
@@ -119,7 +119,7 @@ const LogHistory = () => {
const handleSidoChange = (value: string) => {
const selectedBsnm =
sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd ?? "";
sidoList?.data?.sidoList.find((v) => v.sido === value)?.bsnmCd ?? "";
setSido(value);
setGugun("ALL");
setBsnmCd(selectedBsnm);
@@ -129,7 +129,7 @@ const LogHistory = () => {
const listForBsnm = sggList?.data ?? [];
const selectedBsnm =
value === "ALL"
? (sidoList?.data.sidoList.find((v) => v.sido === sido)
? (sidoList?.data?.sidoList.find((v) => v.sido === sido)
?.bsnmCd ?? "")
: (listForBsnm.find((v) => v.gugun === value)?.bsnmCd ?? "");
setGugun(value);
@@ -0,0 +1,106 @@
import { Chart } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import type { ChartOptions } from "chart.js";
ChartJS.register(CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend);
// 차트 설정 외부 정의 (Mixed 타입 명시)
const options: ChartOptions<"bar" | "line"> = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { stacked: true },
y: {
stacked: true,
beginAtZero: true,
title: { display: true, text: "인구수(명)" },
},
y1: {
position: "right",
beginAtZero: true,
grid: { drawOnChartArea: false },
title: { display: true, text: "총 인구(명)" },
},
},
plugins: {
legend: { position: "bottom" },
},
};
export type PopulationChartData = {
X_AXIS: string[];
TOT: number[];
ORIGIN_MALE: (number | null)[];
ORIGIN_FEMALE: (number | null)[];
FOREIGN_MALE: (number | null)[];
FOREIGN_FEMALE: (number | null)[];
};
type Props = {
chartData: PopulationChartData | null;
};
const SERIES_META = [
{ key: "ORIGIN_MALE", name: "내국인 남성", color: "#3B82F6" },
{ key: "ORIGIN_FEMALE", name: "내국인 여성", color: "#EC4899" },
{ key: "FOREIGN_MALE", name: "외국인 남성", color: "#10B981" },
{ key: "FOREIGN_FEMALE", name: "외국인 여성", color: "#F59E0B" },
] as const;
const PopulationStatusChart = ({ chartData }: Props) => {
if (!chartData || chartData.X_AXIS.length === 0) {
return (
<div className="flex items-center justify-center h-full min-h-[40rem] w-full text-gray-400 text-[1.4rem] border border-gray-200 rounded">
조회된 데이터가 없습니다. 조회 버튼을 눌러주세요.
</div>
);
}
const data = {
labels: chartData.X_AXIS.map((yr) => `${yr}년`),
datasets: [
...SERIES_META
.filter((s) => chartData[s.key]?.some((v) => v !== null))
.map((s) => ({
type: "bar" as const,
label: s.name,
data: chartData[s.key].map((v) => v ?? 0),
backgroundColor: s.color,
stack: "stack1",
})),
{
type: "line" as const,
label: "총 인구",
data: chartData.TOT,
borderColor: "#1F2937",
backgroundColor: "#1F2937",
borderDash: [5, 5],
tension: 0.1,
yAxisID: "y1",
},
],
};
return (
<div className="h-full w-full border border-gray-200 rounded p-4">
{/* 제네릭 타입을 통해 Mixed Chart 지원, 부모 컨테이너 크기에 맞춰 반응형으로 리사이즈됨 */}
<Chart<"bar" | "line", (string | number)[], string>
type="bar"
data={data}
options={options}
/>
</div>
);
};
export default PopulationStatusChart;
@@ -0,0 +1,42 @@
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import { PopulationStatusColumnConfig } from "@/components/util/column/management/PopulationStatusColumn";
import type { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab";
import { asGridRowData, asGridRowSetter } from "@/components/statistics/management/lib/gridCast";
const columnInfo: ColumnInfo = {
columns: PopulationStatusColumnConfig.columns ?? [],
columnLayout: PopulationStatusColumnConfig.columnLayout ?? [],
};
type Props = {
tab: ReturnType<typeof usePopulationStatusTab>;
};
const PopulationStatusTab = ({ tab }: Props) => {
return (
<>
<div className="flex items-center mb-[1rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
행정 인구 현황
</span>
</div>
<DataGrid
columnInfo={columnInfo}
rowStartNum={PopulationStatusColumnConfig.rowStartNum}
excelExportFileSuffix="행정인구현황"
rowData={asGridRowData(tab.rowData)}
setRowData={asGridRowSetter(tab.setRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={PopulationStatusColumnConfig.fixedColumns}
enableSort={false}
/>
</>
);
};
export default PopulationStatusTab;
@@ -0,0 +1,22 @@
import PopulationStatusChart from "./PopulationStatusChart";
import type { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab";
type Props = {
tab: ReturnType<typeof usePopulationVizTab>;
};
const PopulationVizTab = ({ tab }: Props) => {
return (
<>
<span className="inline-block text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500 mb-[1.5rem]">
행정인구현황 시각화 (기준연도 ~ 과거 자료(최대 5년) / *단위: 명)
</span>
<div className="w-full h-[77rem]">
<PopulationStatusChart chartData={tab.chartData} />
</div>
</>
);
};
export default PopulationVizTab;
@@ -1,10 +1,284 @@
import { useRef, useState } from "react";
import { Button, LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
import { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
import { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab";
import { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab";
import { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab";
import DomesticPopulationTab from "./DomesticPopulationTab";
import ForeignPopulationTab from "./ForeignPopulationTab";
import AdmdstRenewalTab from "./AdmdstRenewalTab";
import PopulationStatusTab from "./PopulationStatusTab";
import PopulationVizTab from "./PopulationVizTab";
import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types";
const TAB_LIST = [
{ key: "domestic", label: "행정인구등록" },
{ key: "foreign", label: "외국인인구등록" },
{ key: "renewal", label: "행정동갱신" },
{ key: "status", label: "행정인구현황" },
{ key: "statusViz", label: "행정인구현황 시각화" },
];
const StatisticsPopulationManagement = () => {
const [activeTab, setActiveTab] = useState("domestic");
/** 검색폼은 모든 탭이 공유. 마지막 조회 조건만 ref로 기억해뒀다가 탭 전환 시 재사용 */
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
/* ── 탭별 상태/로직은 전부 훅에서 가져온다. 탭이 화면에 보이지 않아도 훅은 항상 호출되므로
탭을 왔다갔다 해도 데이터가 유지된다 (원본 컴포넌트와 동일한 동작) ── */
const domesticTab = usePopulationMappingTab<DomesticPopulationItem>({
kind: "domestic",
getList: statisticsPpltnApi.getDomesticPopulationList,
uploadExcel: statisticsPpltnApi.uploadDomesticPopulationExcel,
saveFinal: statisticsPpltnApi.saveDomesticPopulationFinal,
getUpperCodesByName: statisticsPpltnApi.getUpperCodesByName,
requiredHeaderKeywords: ["행정기관코드", "행정기관"],
invalidFileMessage: "행정인구(내국인) 전용 파일만 업로드 가능합니다.\n올바른 파일을 선택해주세요.",
});
const foreignTab = usePopulationMappingTab<ForeignPopulationItem>({
kind: "foreign",
getList: async (statsYr, opt1) => {
const res = await statisticsPpltnApi.getForeignPopulationList(statsYr, opt1);
// res.data를 확실하게 ForeignPopulationItem[]로 캐스팅
return { ...res, data: res.data as ForeignPopulationItem[] };
},
uploadExcel: statisticsPpltnApi.uploadForeignPopulationExcel,
saveFinal: statisticsPpltnApi.saveForeignPopulationFinal,
getUpperCodesByName: statisticsPpltnApi.getUpperCodesByName,
requiredHeaderKeywords: ["행정동코드", "행정동명", "외국인수"],
invalidFileMessage: "외국인 인구 전용 파일만 업로드 가능합니다.\n올바른 파일을 선택해주세요.",
});
const renewalTab = useAdmdstRenewalTab();
const statusTab = usePopulationStatusTab();
const vizTab = usePopulationVizTab();
const statsYr = lastSearchedValuesRef.current?.year ?? "";
const isLoading =
(activeTab === "domestic" && domesticTab.isLoading) ||
(activeTab === "foreign" && foreignTab.isLoading) ||
(activeTab === "renewal" && renewalTab.isLoading) ||
(activeTab === "status" && statusTab.isLoading) ||
(activeTab === "statusViz" && vizTab.isLoading);
/* ════════════════ 공통 ════════════════ */
const handleSearch = async (values: SearchFormValues) => {
lastSearchedValuesRef.current = values;
const yr = values.year ?? "";
if (activeTab === "domestic") await domesticTab.search(yr);
else if (activeTab === "foreign") await foreignTab.search(yr);
else if (activeTab === "renewal") await renewalTab.loadAll(yr);
else if (activeTab === "status") await statusTab.search(yr);
else if (activeTab === "statusViz") await vizTab.search(yr);
};
const handleReset = () => {
domesticTab.reset();
foreignTab.reset();
renewalTab.reset();
statusTab.reset();
vizTab.reset();
setActiveTab("domestic");
lastSearchedValuesRef.current = null;
};
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const values = lastSearchedValuesRef.current;
if (!values) return;
const yr = values.year ?? "";
if (tabKey === "domestic") await domesticTab.loadOnActivate(yr);
else if (tabKey === "foreign") await foreignTab.loadOnActivate(yr);
else if (tabKey === "renewal") await renewalTab.loadOnActivate(yr);
// status / statusViz는 원본에서도 탭 전환 시 자동 재조회하지 않음 (그대로 유지)
};
return (
<>
<SearchForm sido={false} />
</>
<div className="relative">
<input
ref={domesticTab.fileInputRef}
type="file"
accept=".xlsx,.xls,.csv"
className="hidden"
onChange={(e) => void domesticTab.handleFileChange(e, statsYr)}
/>
<input
ref={foreignTab.fileInputRef}
type="file"
accept=".xlsx,.xls,.csv"
className="hidden"
onChange={(e) => void foreignTab.handleFileChange(e, statsYr)}
/>
<div className="relative">
<SearchForm sido={false} gugun={false} onSubmit={handleSearch} onReset={handleReset} />
{activeTab === "status" && (
<div className="absolute top-0 left-[16rem] h-full flex items-center gap-6 px-4">
<label className="text-[1.3rem] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-label cursor-pointer flex items-center gap-1.5">
<input
type="checkbox"
checked={statusTab.includeOriginMale}
onChange={(e) => statusTab.setIncludeOriginMale(e.target.checked)}
className="size-4"
/>
내국인 남성 포함
</label>
<label className="text-[1.3rem] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-label cursor-pointer flex items-center gap-1.5">
<input
type="checkbox"
checked={statusTab.includeOriginFemale}
onChange={(e) => statusTab.setIncludeOriginFemale(e.target.checked)}
className="size-4"
/>
내국인 여성 포함
</label>
<label className="text-[1.3rem] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-label cursor-pointer flex items-center gap-1.5">
<input
type="checkbox"
checked={statusTab.includeForeignMale}
onChange={(e) => statusTab.setIncludeForeignMale(e.target.checked)}
className="size-4"
/>
외국인 남성 포함
</label>
<label className="text-[1.3rem] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-label cursor-pointer flex items-center gap-1.5">
<input
type="checkbox"
checked={statusTab.includeForeignFemale}
onChange={(e) => statusTab.setIncludeForeignFemale(e.target.checked)}
className="size-4"
/>
외국인 여성 포함
</label>
</div>
)}
{activeTab === "statusViz" && (
<div className="absolute top-0 left-[16rem] h-full flex items-center gap-5 px-4 flex-nowrap">
<label className="text-[1.3rem] font-medium leading-none text-label cursor-pointer flex items-center gap-1.5 shrink-0 whitespace-nowrap">
<input
type="checkbox"
checked={vizTab.includeOriginMale}
onChange={(e) => vizTab.setIncludeOriginMale(e.target.checked)}
className="size-4"
/>
내국인 남성
</label>
<label className="text-[1.3rem] font-medium leading-none text-label cursor-pointer flex items-center gap-1.5 shrink-0 whitespace-nowrap">
<input
type="checkbox"
checked={vizTab.includeOriginFemale}
onChange={(e) => vizTab.setIncludeOriginFemale(e.target.checked)}
className="size-4"
/>
내국인 여성
</label>
<label className="text-[1.3rem] font-medium leading-none text-label cursor-pointer flex items-center gap-1.5 shrink-0 whitespace-nowrap">
<input
type="checkbox"
checked={vizTab.includeForeignMale}
onChange={(e) => vizTab.setIncludeForeignMale(e.target.checked)}
className="size-4"
/>
외국인 남성
</label>
<label className="text-[1.3rem] font-medium leading-none text-label cursor-pointer flex items-center gap-1.5 shrink-0 whitespace-nowrap">
<input
type="checkbox"
checked={vizTab.includeForeignFemale}
onChange={(e) => vizTab.setIncludeForeignFemale(e.target.checked)}
className="size-4"
/>
외국인 여성
</label>
<Button
type="button"
color="primary"
onClick={vizTab.selectAll}
className="px-[1rem] py-[0.5rem] text-[1.2rem] shrink-0 whitespace-nowrap"
>
전체선택
</Button>
<Button
type="button"
color="primary"
onClick={vizTab.deselectAll}
className="px-[1rem] py-[0.5rem] text-[1.2rem] shrink-0 whitespace-nowrap"
>
전체해제(총 인구만 조회)
</Button>
</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>
{TAB_LIST.map((tab) => (
<TabsTrigger key={tab.key} value={tab.key}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
<TabsContent value="domestic">
<DomesticPopulationTab
tab={domesticTab}
statsYr={statsYr}
onFileButtonClick={() => domesticTab.fileInputRef.current?.click()}
/>
</TabsContent>
<TabsContent value="foreign">
<ForeignPopulationTab
tab={foreignTab}
statsYr={statsYr}
onFileButtonClick={() => foreignTab.fileInputRef.current?.click()}
/>
</TabsContent>
<TabsContent value="renewal">
<AdmdstRenewalTab tab={renewalTab} statsYr={statsYr} />
</TabsContent>
<TabsContent value="status">
<PopulationStatusTab tab={statusTab} />
</TabsContent>
<TabsContent value="statusViz">
<PopulationVizTab tab={vizTab} />
</TabsContent>
</Tabs>
</div>
</div>
);
};
@@ -0,0 +1,129 @@
import { useState } from "react";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@common/components";
interface UpperCodeSelectDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
candidates: Record<string, string>[];
codeField: string;
nameField: string;
onSelect: (item: Record<string, string>) => void;
extraFields?: { field: string; header: string }[];
}
export const UpperCodeSelectDialog = ({
open,
onOpenChange,
candidates,
codeField,
nameField,
extraFields,
onSelect,
}: UpperCodeSelectDialogProps) => {
const [selectedCode, setSelectedCode] = useState<string | null>(null);
const handleSelect = () => {
const item = candidates.find((c) => c[codeField] === selectedCode);
if (!item) return;
onSelect(item);
onOpenChange(false);
setSelectedCode(null);
};
const handleClose = () => {
onOpenChange(false);
setSelectedCode(null);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-[50rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>상위 코드 리스트</DialogTitle>
</DialogHeader>
<div className="border border-gray-300 rounded overflow-hidden">
<table className="w-full text-[1.3rem]">
<thead className="sticky top-0 z-10">
<tr className="bg-blue-600 text-white">
<th className="py-[0.8rem] px-[1rem] text-center font-semibold border-r border-blue-500 w-[40%]">
코드
</th>
<th className="py-[0.8rem] px-[1rem] text-center font-semibold">
동명
</th>
{extraFields?.map((f) => (
<th key={f.field} className="py-[0.8rem] px-[1rem] text-center font-semibold border-l border-blue-500">
{f.header}
</th>
))}
</tr>
</thead>
</table>
<div className="max-h-[40rem] overflow-y-auto">
<table className="w-full text-[1.3rem]">
<tbody>
{candidates.length === 0 ? (
<tr>
<td colSpan={2} className="py-[1rem] text-center text-gray-400">
후보 코드가 없습니다.
</td>
</tr>
) : (
candidates.map((item,index) => (
<tr
key={`${item[codeField]}-${index}`} // ← index 추가
onClick={() => setSelectedCode(item[codeField] ?? "")}
className={`cursor-pointer border-b border-gray-200 transition-colors
${selectedCode === item[codeField]
? "bg-blue-100"
: "hover:bg-gray-50"
}`}
>
<td className="py-[0.7rem] px-[1rem] text-center border-r border-gray-200 w-[40%]">
{item[codeField]}
</td>
<td className="py-[0.7rem] px-[1rem] text-center">
{item[nameField]}
</td>
{extraFields?.map((f) => (
<td key={f.field} className="py-[0.7rem] px-[1rem] text-center border-l border-gray-200">
{item[f.field]}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
<DialogFooter className="flex flex-col gap-2">
<Button
type="button"
color="primary"
disabled={!selectedCode}
onClick={handleSelect}
className="w-full py-[1rem] text-[1.4rem]"
>
선택
</Button>
<Button
type="button"
color="gray"
onClick={handleClose}
className="w-full py-[1rem] text-[1.4rem]"
>
닫기
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,161 @@
import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
import { toGridRows } from "@/components/util/grid/gridFunction";
// TODO: 원본 파일은 `../../../../../../packages/common/components/src/api` (6단계 상위) 였음.
// 이 훅 파일은 원본 컴포넌트보다 한 단계 더 깊은 폴더(hooks/)에 위치하므로
// 실제 프로젝트에 옮길 때 상대경로 깊이를 다시 맞춰줘야 함 (혹은 @common/components에 getUserInfo가
// re-export 되어 있다면 그쪽으로 교체).
import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
import { useAsyncAction } from "./useAsyncAction";
import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
import type {
AdmdstRenewalAddedItem,
AdmdstRenewalAddedSaveItem,
AdmdstRenewalReducedItem,
AdmdstRenewalReducedSaveItem,
} from "@/components/statistics/management/types/population-tab-types";
export function useAdmdstRenewalTab() {
const { setMessage: alert } = useAlertStore((state) => state);
const { isLoading, run } = useAsyncAction(alert);
const [reducedRowData, setReducedRowData] = useState<AdmdstRenewalReducedItem[]>([]);
const [addedRowData, setAddedRowData] = useState<AdmdstRenewalAddedItem[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [candidates, setCandidates] = useState<UpperCodeCandidateItem[]>([]);
const selectedRowIndexRef = useRef<number | null>(null);
/** OPT_1: "1" = 추가된 행정구역, "2" = 감축된 행정구역 (원본 그대로) */
const loadAll = async (statsYr: string): Promise<boolean> => {
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
const result = await run(async () => {
const [addedRes, reducedRes] = await Promise.all([
statisticsPpltnApi.searchChangedArea(statsYr, "1"),
statisticsPpltnApi.searchChangedArea(statsYr, "2"),
]);
if (addedRes.status === "success") {
setAddedRowData(toGridRows((addedRes.data as object[]) ?? []) as unknown as AdmdstRenewalAddedItem[]);
}
if (reducedRes.status === "success") {
setReducedRowData(toGridRows((reducedRes.data as object[]) ?? []) as unknown as AdmdstRenewalReducedItem[]);
}
return true;
});
return result ?? false;
};
/** 탭 전환 시 - 아직 한 번도 로드되지 않았을 때만 로드 */
const loadOnActivate = async (statsYr: string) => {
if (reducedRowData.length === 0 && addedRowData.length === 0) await loadAll(statsYr);
};
const handleFinal = async (statsYr: string) => {
if (!statsYr) { alert("먼저 조회를 진행해주세요."); return; }
if (reducedRowData.length === 0 && addedRowData.length === 0) {
alert("반영할 데이터가 없습니다.");
return;
}
await run(async () => {
const userInfo = await getUserInfo();
const addedList: AdmdstRenewalAddedSaveItem[] = addedRowData
.filter((row) => {
const lawCd = String(row.lawCd ?? "").trim();
return lawCd !== "" && lawCd !== "null";
})
.map((row) => ({
adstCd: String(row.adstCd ?? ""),
lawCd: String(row.lawCd ?? ""),
adstDstrNm: String(row.adstDstrNm ?? ""),
rgnCd: String(row.rgnCd ?? ""),
lv: String(row.lv ?? ""),
sido: String(row.sido ?? ""),
gugun: String(row.gugun ?? ""),
adstDong: String(row.adstDong ?? ""),
userId: userInfo.userUnqId,
}));
const reducedList: AdmdstRenewalReducedSaveItem[] = reducedRowData.map((row) => ({
adstCd: String(row.adstCd ?? ""),
}));
console.log("reducedList:", reducedList);
console.log("addedList:", addedList);
console.log("reducedList 이상한 것:", reducedList.filter(r => r.adstCd.length !== 10));
const res = await statisticsPpltnApi.saveChangedArea(reducedList, addedList);
if (res.status === "success") {
await loadAll(statsYr);
alert("행정구역 감축 및 추가사항이 성공적으로 반영되었습니다.");
} else {
alert(res.message || "반영 중 오류가 발생했습니다.");
}
});
};
const handleRowClick = async (rowData: AdmdstRenewalAddedItem, statsYr: string) => {
const adstCd = String(rowData.adstCd ?? "");
if (!adstCd) { alert("행정동코드가 없습니다."); return; }
const parts = adstCd.trim().split(/\s+/);
const dongNm = parts[parts.length - 1] ?? adstCd;
await run(
async () => {
const res = await statisticsPpltnApi.getLawCdsByName(dongNm);
if (res.status === "success" && Array.isArray(res.data) && res.data.length > 0) {
setCandidates(res.data);
const rowIndex = addedRowData.findIndex((r) => r.adstCd === rowData.adstCd);
console.log("rowData.adstCd:", rowData.adstCd); // ← 여기
console.log("rowIndex:", rowIndex); // ← 여기
selectedRowIndexRef.current = rowIndex;
setDialogOpen(true);
} else {
alert("해당 지역명에 대한 법정동코드 후보가 없습니다.");
}
},
{ errorMessage: "법정동코드 조회 중 오류가 발생했습니다." },
);
};
const handleCodeSelect = (item: Record<string, string>) => {
const rowIndex = selectedRowIndexRef.current;
if (rowIndex === null) return;
setAddedRowData((prev) =>
prev.map((row, idx) =>
idx === rowIndex
? { ...row, lawCd: item["lawCd"] ?? "", lawNm: item["lawNm"] ?? "", rgnCd: item["rgnCd"] ?? "", lv: item["lv"] ?? "" }
: row,
),
);
selectedRowIndexRef.current = null;
};
const reset = () => {
setReducedRowData([]);
setAddedRowData([]);
setDialogOpen(false);
setCandidates([]);
selectedRowIndexRef.current = null;
};
return {
isLoading,
reducedRowData,
addedRowData,
setReducedRowData,
setAddedRowData,
dialogOpen,
setDialogOpen,
candidates,
loadAll,
loadOnActivate,
handleFinal,
handleRowClick,
handleCodeSelect,
reset,
};
}
@@ -0,0 +1,41 @@
import { useCallback, useState } from "react";
type AsyncActionOptions = {
/** onError가 없을 때 사용할 기본 에러 메시지 */
errorMessage?: string;
/** 커스텀 에러 처리가 필요할 때 - 지정하면 errorMessage 대신 이 콜백이 호출됨 */
onError?: (error: unknown) => void;
};
/**
* 탭별 hook(usePopulationMappingTab, useAdmdstRenewalTab 등)에서
* 반복되던 `setIsLoading(true) → try → catch(alert) → finally(setIsLoading(false))`
* 패턴을 공통화한 훅.
*
* notify: 실패 시 사용자에게 보여줄 알럿 함수 (useAlertStore의 setMessage를 그대로 넘기면 됨)
*/
export function useAsyncAction(notify: (message: string) => void) {
const [isLoading, setIsLoading] = useState(false);
const run = useCallback(
async <T>(action: () => Promise<T>, options: AsyncActionOptions = {}): Promise<T | undefined> => {
const { errorMessage = "서버 연결 중 오류가 발생했습니다.", onError } = options;
try {
setIsLoading(true);
return await action();
} catch (error) {
if (onError) {
onError(error);
} else {
notify(errorMessage);
}
return undefined;
} finally {
setIsLoading(false);
}
},
[notify],
);
return { isLoading, run };
}
@@ -0,0 +1,200 @@
import { useRef, useState } from "react";
import * as XLSX from "xlsx";
import { useAlertStore } from "@/shared/lib";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { useAsyncAction } from "./useAsyncAction";
import type {
PopulationMappingApiConfig,
PopulationMappingRow,
UpperCodeCandidateItem,
} from "@/components/statistics/management/types/population-tab-types";
/* ── 엑셀 헤더 검증 (원본 컴포넌트의 validateExcelHeader를 그대로 이동) ── */
const validateExcelHeader = (file: File, requiredKeywords: string[]): Promise<boolean> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = e.target?.result;
const workbook = XLSX.read(data, { type: "binary" });
const sheetName = workbook.SheetNames[0];
if (!sheetName) { resolve(false); return; }
const sheet = workbook.Sheets[sheetName];
if (!sheet) { resolve(false); return; }
const rows = XLSX.utils.sheet_to_json<string[]>(sheet, { header: 1 });
const headerRow = (rows[0] as string[]).map((h) => String(h ?? ""));
const isValid = requiredKeywords.every((kw) => headerRow.some((h) => h.includes(kw)));
resolve(isValid);
} catch {
resolve(false);
}
};
reader.readAsBinaryString(file);
});
};
/**
* 내국인/외국인 탭이 공유하는 "좌측 전체 리스트 + 우측 미매핑 리스트" 패턴 훅.
* 두 탭은 API 함수와 필드명만 다를 뿐 동작이 완전히 동일해서 config로 주입받는다.
*/
export function usePopulationMappingTab<TRow extends PopulationMappingRow>(
config: PopulationMappingApiConfig<TRow>,
) {
const { setMessage: alert } = useAlertStore((state) => state);
const { isLoading, run } = useAsyncAction(alert);
const [leftRowData, setLeftRowData] = useState<TRow[]>([]);
const [rightRowData, setRightRowData] = useState<TRow[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [candidates, setCandidates] = useState<UpperCodeCandidateItem[]>([]);
const selectedRowIndexRef = useRef<number | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const loadLeft = async (statsYr: string) => {
const res = await config.getList(statsYr, "1");
if (res.status === "success") setLeftRowData(toGridRows((res.data as object[]) ?? []) as unknown as TRow[]);
};
const loadRight = async (statsYr: string) => {
const res = await config.getList(statsYr, "2");
if (res.status === "success") setRightRowData(toGridRows((res.data as object[]) ?? []) as unknown as TRow[]);
};
/** 탭 최초 진입/활성화 시 - 좌/우 모두 갱신 (업로드 직후에도 사용) */
const loadAll = async (statsYr: string): Promise<boolean> => {
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
const result = await run(async () => {
await loadLeft(statsYr);
await loadRight(statsYr);
return true;
});
return result ?? false;
};
/**
* 검색 버튼 클릭 시 - 좌측은 항상 갱신하고, 우측은 비어있을 때만 갱신한다.
* (사용자가 우측에서 수동으로 매핑한 값을 재검색 시 날려버리지 않기 위함 - 원본 동작 그대로 유지)
*/
const search = async (statsYr: string): Promise<boolean> => {
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
const result = await run(async () => {
await loadLeft(statsYr);
if (rightRowData.length === 0) await loadRight(statsYr);
return true;
});
return result ?? false;
};
/** 탭 전환 시 - 아직 한 번도 로드되지 않았을 때만 로드 */
const loadOnActivate = async (statsYr: string) => {
if (leftRowData.length === 0) await loadAll(statsYr);
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>, statsYr: string) => {
const file = e.target.files?.[0];
if (!file) return;
const isValidExt = [".xlsx", ".xls", ".csv"].some((ext) => file.name.toLowerCase().endsWith(ext));
if (!isValidExt) { alert("xlsx, xls, csv 파일만 업로드 가능합니다."); e.target.value = ""; return; }
const isValidHeader = await validateExcelHeader(file, config.requiredHeaderKeywords);
if (!isValidHeader) { alert(config.invalidFileMessage); e.target.value = ""; return; }
if (!statsYr) { alert("연도를 선택해주세요."); e.target.value = ""; return; }
await run(async () => {
const res = await config.uploadExcel(file, statsYr);
if (res.status === "success") {
await loadAll(statsYr);
alert("업로드가 완료되었습니다.");
} else {
alert(res.message || "업로드 중 오류가 발생했습니다.");
}
});
e.target.value = "";
};
const handleFinal = async (statsYr: string) => {
if (!statsYr) { alert("먼저 조회를 진행해주세요."); return; }
if (rightRowData.length === 0) { alert("반영할 데이터가 없습니다."); return; }
await run(async () => {
const regList = rightRowData
.filter((row) => String(row.mpinCd ?? "").trim() !== "")
.map((row) => ({
statsYr,
admdstCd: String(row.admdstCd ?? ""),
admdstNm: String(row.admdstNm ?? ""),
subAdmdstCd: String(row.mpinCd ?? ""),
}));
const res = await config.saveFinal(statsYr, regList);
if (res.status === "success") {
await loadLeft(statsYr);
alert("반영이 완료되었습니다.");
} else {
alert(res.message || "반영 중 오류가 발생했습니다.");
}
});
};
const handleRightRowClick = async (rowData: TRow, statsYr: string) => {
const admdstCd = String(rowData.admdstCd ?? "");
if (!admdstCd) { alert("행정구역코드가 없습니다."); return; }
await run(
async () => {
const res = await config.getUpperCodesByName(statsYr, "", admdstCd); // ← admdstCd 추가
if (res.status === "success" && Array.isArray(res.data) && res.data.length > 0) {
setCandidates(res.data);
const rowIndex = rightRowData.findIndex((r) => r.admdstCd === rowData.admdstCd);
selectedRowIndexRef.current = rowIndex;
setDialogOpen(true);
} else {
alert("해당 지역명에 대한 상위 코드 후보가 없습니다.");
}
},
{ errorMessage: "상위 코드 조회 중 오류가 발생했습니다." },
);
};
const handleCodeSelect = (item: Record<string, string>) => {
const rowIndex = selectedRowIndexRef.current;
if (rowIndex === null) return;
setRightRowData((prev) =>
prev.map((row, idx) => (idx === rowIndex ? { ...row, mpinCd: item["admdstCd"] ?? "" } : row)),
);
selectedRowIndexRef.current = null;
};
const reset = () => {
setLeftRowData([]);
setRightRowData([]);
setDialogOpen(false);
setCandidates([]);
selectedRowIndexRef.current = null;
};
return {
isLoading,
leftRowData,
rightRowData,
setLeftRowData,
setRightRowData,
fileInputRef,
dialogOpen,
setDialogOpen,
candidates,
loadAll,
search,
loadOnActivate,
handleFileChange,
handleFinal,
handleRightRowClick,
handleCodeSelect,
reset,
};
}
@@ -0,0 +1,71 @@
import { useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { useAsyncAction } from "./useAsyncAction";
import type { PopulationStatusItem } from "../types/population-tab-types";
export function usePopulationStatusTab() {
const { setMessage: alert } = useAlertStore((state) => state);
const { isLoading, run } = useAsyncAction(alert);
const [rowData, setRowData] = useState<PopulationStatusItem[]>([]);
const [includeOriginMale, setIncludeOriginMale] = useState(true);
const [includeOriginFemale, setIncludeOriginFemale] = useState(true);
const [includeForeignMale, setIncludeForeignMale] = useState(true);
const [includeForeignFemale, setIncludeForeignFemale] = useState(true);
const search = async (statsYr: string): Promise<boolean> => {
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
if (!includeOriginMale && !includeOriginFemale && !includeForeignMale && !includeForeignFemale) {
alert(
"체크 박스를 모두 해제하신 상태에서는\n조회가 불가능합니다.\n(최소 1개 이상의 체크 박스 선택 후\n조회를 진행해주세요.)",
);
return false;
}
const result = await run(async () => {
const res = await statisticsPpltnApi.searchPopByEachCondition(
statsYr,
includeOriginMale,
includeOriginFemale,
includeForeignMale,
includeForeignFemale,
);
if (res.status === "success") {
setRowData(toGridRows((res.data as object[]) ?? []) as unknown as PopulationStatusItem[]);
return true;
}
alert(res.message || "조회 중 오류가 발생했습니다.");
return false;
});
return result ?? false;
};
const reset = () => {
setRowData([]);
setIncludeOriginMale(true);
setIncludeOriginFemale(true);
setIncludeForeignMale(true);
setIncludeForeignFemale(true);
};
return {
isLoading,
rowData,
setRowData,
includeOriginMale,
setIncludeOriginMale,
includeOriginFemale,
setIncludeOriginFemale,
includeForeignMale,
setIncludeForeignMale,
includeForeignFemale,
setIncludeForeignFemale,
search,
reset,
};
}
@@ -0,0 +1,84 @@
import { useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
import { useAsyncAction } from "./useAsyncAction";
import type { PopulationChartData } from "../PopulationStatusChart";
export function usePopulationVizTab() {
const { setMessage: alert } = useAlertStore((state) => state);
const { isLoading, run } = useAsyncAction(alert);
const [year, setYear] = useState(String(new Date().getFullYear() - 1));
const [includeOriginMale, setIncludeOriginMale] = useState(true);
const [includeOriginFemale, setIncludeOriginFemale] = useState(true);
const [includeForeignMale, setIncludeForeignMale] = useState(true);
const [includeForeignFemale, setIncludeForeignFemale] = useState(true);
const [chartData, setChartData] = useState<PopulationChartData | null>(null);
const search = async (statsYr: string): Promise<boolean> => {
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
setYear(statsYr);
const result = await run(async () => {
const res = await statisticsPpltnApi.getChartDatas1(
statsYr,
includeOriginMale,
includeOriginFemale,
includeForeignMale,
includeForeignFemale,
);
if (res.status === "success") {
setChartData(res.data as PopulationChartData);
return true;
}
alert(res.message || "조회 중 오류가 발생했습니다.");
return false;
});
return result ?? false;
};
const selectAll = () => {
setIncludeOriginMale(true);
setIncludeOriginFemale(true);
setIncludeForeignMale(true);
setIncludeForeignFemale(true);
};
const deselectAll = () => {
setIncludeOriginMale(false);
setIncludeOriginFemale(false);
setIncludeForeignMale(false);
setIncludeForeignFemale(false);
};
const reset = () => {
setYear(String(new Date().getFullYear() - 1));
setIncludeOriginMale(true);
setIncludeOriginFemale(true);
setIncludeForeignMale(true);
setIncludeForeignFemale(true);
setChartData(null);
};
return {
isLoading,
year,
chartData,
includeOriginMale,
setIncludeOriginMale,
includeOriginFemale,
setIncludeOriginFemale,
includeForeignMale,
setIncludeForeignMale,
includeForeignFemale,
setIncludeForeignFemale,
search,
selectAll,
deselectAll,
reset,
};
}
@@ -0,0 +1,27 @@
import type { Dispatch, SetStateAction } from "react";
export type GridRow = Record<string, string | number>;
/**
* DataGrid가 제네릭이 아니라 rowData: Record<string, string|number>[] 를 그대로 요구함
* (실제 타입 에러로 확인됨: ts(2322), "missing properties from PopulationStatusItem").
*
* 훅/탭 내부에서는 명시적 도메인 타입(PopulationStatusItem 등)을 그대로 쓰고,
* DataGrid에 넘기는 이 경계에서만 캐스팅한다. 내부 로직의 타입 안정성은 유지하면서
* 외부 라이브러리(DataGrid)의 느슨한 타입과의 경계만 명시적으로 표시하는 용도.
*/
export function asGridRowData<T>(data: T[]): GridRow[] {
return data as unknown as GridRow[];
}
export function asGridRowSetter<T>(setter: Dispatch<SetStateAction<T[]>>): Dispatch<SetStateAction<GridRow[]>> {
return setter as unknown as Dispatch<SetStateAction<GridRow[]>>;
}
/**
* DataGrid의 onRowClick 등에서 넘어오는 GridRow(Record<string,string|number>)를
* 도메인 타입으로 되돌릴 때 사용. `as unknown as T`를 매번 직접 쓰지 않도록 모아둠.
*/
export function fromGridRow<T>(row: GridRow): T {
return row as unknown as T;
}
@@ -0,0 +1,116 @@
import type { ApiResponse } from "@/entities/statistics/model/statistics-interface";
import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
/* ══════════════════════════════════════════════════
외국인 인구 관련 타입
(내국인 쪽 DomesticPopulationItem과 필드명만 다르고 구조는 동일)
══════════════════════════════════════════════════ */
export interface ForeignPopulationItem {
statsYr: string;
admdstCd: string;
admdstNm: string;
frgnrWholPpltnCnt: number;
frgnrMlPpltnCnt: number;
frgnrFmlPpltnCnt: number;
/** 미매핑 리스트(우측) 전용 - 매핑 대상 행정구역코드(상위코드) */
mpinCd: string;
}
export type ForeignPopulationListResponse = ApiResponse<ForeignPopulationItem[]>;
/* ══════════════════════════════════════════════════
행정동갱신 관련 타입
══════════════════════════════════════════════════ */
export interface AdmdstRenewalReducedItem {
adstDstrNm: string;
adstCd: string;
}
export interface AdmdstRenewalAddedItem {
adstDstrNm: string;
adstCd: string;
lawCd: string;
lawNm: string;
rgnCd?: string;
lv?: string;
sido?: string; // ← 추가
gugun?: string; // ← 추가
adstDong?: string; // ← 추가
}
/** 추가된 행정구역 반영 시 서버로 보내는 단건 페이로드 */
export interface AdmdstRenewalAddedSaveItem {
adstCd: string;
lawCd: string;
adstDstrNm: string;
sido: string;
gugun: string;
adstDong: string;
userId: string;
rgnCd: string;
lv: string;
}
/** 감축된 행정구역 반영 시 서버로 보내는 단건 페이로드 */
export interface AdmdstRenewalReducedSaveItem {
adstCd: string;
}
/* ══════════════════════════════════════════════════
행정인구현황 관련 타입
══════════════════════════════════════════════════ */
export interface PopulationStatusItem {
statsYr: string;
admdstCd: string;
admdstNm: string;
wholPpltnCnt: number;
domplWholPpltnCnt: number;
domplMlPpltnCnt: number;
domplFmlPpltnCnt: number;
frgnrWholPpltnCnt: number;
frgnrMlPpltnCnt: number;
frgnrFmlPpltnCnt: number;
}
export type PopulationStatusListResponse = ApiResponse<PopulationStatusItem[]>;
/* ══════════════════════════════════════════════════
내국인/외국인 공용 매핑 탭(좌:전체 리스트, 우:미매핑 리스트) 공통 타입
══════════════════════════════════════════════════ */
export type MappingTabKind = "domestic" | "foreign";
/** 반영(최종 매핑 저장) 요청 시 서버로 보내는 단건 페이로드 - 내국인/외국인 공통 */
export interface PopulationMappingRegItem {
statsYr: string;
admdstCd: string;
admdstNm: string;
subAdmdstCd: string;
}
/** 좌/우 그리드 행이 공통으로 가져야 하는 최소 필드 */
export interface PopulationMappingRow {
admdstCd: string;
admdstNm: string;
/** 우측(미매핑 리스트) 전용 필드. 좌측 데이터에는 존재하지 않을 수 있음 */
mpinCd?: string;
}
/** usePopulationMappingTab에 주입하는 API/검증 설정 - 내국인/외국인 탭이 이 설정만 다르게 넘김 */
export interface PopulationMappingApiConfig<TRow extends PopulationMappingRow> {
kind: MappingTabKind;
/** opt1: "1" = 전체 리스트(좌측), "2" = 미매핑 리스트(우측) */
getList: (statsYr: string, opt1: "1" | "2") => Promise<ApiResponse<TRow[]>>;
uploadExcel: (file: File, statsYr: string) => Promise<ApiResponse<unknown>>;
saveFinal: (statsYr: string, regList: PopulationMappingRegItem[]) => Promise<ApiResponse<unknown>>;
getUpperCodesByName: (statsYr: string, admdstNm: string, admdstCd: string) => Promise<ApiResponse<UpperCodeCandidateItem[]>>;
/** 업로드된 엑셀 헤더에 반드시 포함되어야 하는 키워드 목록 */
requiredHeaderKeywords: string[];
/** 헤더 검증 실패 시 노출할 안내 메시지 */
invalidFileMessage: string;
}
export type { UpperCodeCandidateItem };
@@ -1,37 +1,314 @@
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="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} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && <SubmitButton submitFunc={submitFunc} />}
</div>
</div>
<TabDataGrid columnInfo={ExcretaTreatmentFacilityInfoList} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
)}
{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>
);
};
@@ -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,
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
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 returnFunc = () => {
console.log('returnFunc');
// 반려
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 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="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} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
<TabDataGrid columnInfo={PrivateSewageFacilityInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
)}
{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>
<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[];
if (response.status === "success" && response.data) {
const rawList = extractMediationPumpStationList(response.data);
const statsInfo = extractMediationPumpStationStatsStatusInfo(response.data);
const convertedData = rawList.map(item => ({
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} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
)}
{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">
@@ -104,13 +258,45 @@ const PumpingStationRvw = () => {
</div>
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<DataGrid
columnInfo={columnInfo}
isExcelImport={false}
fixedColumns={columnInfo.fixedColumns}
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>
);
@@ -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,
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
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 returnFunc = () => {
console.log('returnFunc');
// 반려
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 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="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} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
<TabDataGrid columnInfo={SewageExcretaRelatedInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
)}
{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>
<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,51 +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 {
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,
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
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 returnFunc = () => {
console.log('returnFunc');
// 반려
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 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="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} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
<TabDataGrid columnInfo={SewageExcretaSludgeInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
)}
{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>
<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>
);
};
@@ -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} />
{showActionBar && (
<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={() => { }} />
{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">
@@ -228,12 +409,37 @@ 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>
);
};
@@ -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 = {
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) {
const canLazyLoad =
searchParamRef.current &&
!hasActiveTabData &&
!lazyTabLoggedRef.current.has(tabKey) &&
searchDoneRef.current;
if (!canLazyLoad) return;
const searchParam = searchParamRef.current;
if (!searchParam) return;
try {
setIsLoading(true);
let response;
const response = await fetchSewageReservoirPumpReviewTab(tabKey, searchParam);
lazyTabLoggedRef.current.add(tabKey);
if (tabKey === 'ST00040001') {
response = await statisticsManagementApi.getSewerReservoirList(searchParamRef.current);
} else if (tabKey === 'ST00040002') {
response = await statisticsManagementApi.getPumpStationList(searchParamRef.current);
}
if (response?.status === 'success' && response.data) {
setRowDataMap(prev => {
if (response?.status === "success" && response.data) {
const ssi = extractStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, toGridRows(response.data as object[]));
next.set(tabKey, ssi ?? {});
return next;
});
} else if (response) {
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('데이터 조회 중 오류가 발생했습니다.');
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} />
{showActionBar && (
<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={() => { }} />
{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">
@@ -141,13 +367,84 @@ const SewageReservoirPumpRvw = () => {
</div>
)}
<TabDataGrid
columnInfo={columnInfo}
<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}
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
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>
);
@@ -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,
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
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 returnFunc = () => {
console.log('returnFunc');
// 반려
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 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="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} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
<TabDataGrid columnInfo={SewerageFinanceInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
)}
{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>
<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;
@@ -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,
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
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 returnFunc = () => {
console.log('returnFunc');
// 반려
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 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="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} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
<TabDataGrid columnInfo={SewerageServiceLevelInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
)}
{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>
<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,50 +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 {
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,
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
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 returnFunc = () => {
console.log('returnFunc');
// 반려
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 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="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} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
<TabDataGrid columnInfo={WaterReuseInfoList} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
)}
{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>
<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>
);
};
@@ -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";
@@ -395,6 +395,8 @@ export type DataGridProps = {
/** 외부에서 gridRef에 직접 접근하기 위한 ref (setRows/addRows/setValue 직접 호출용) */
getGridRef?: React.MutableRefObject<RealGridReact | null>;
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
onRowDblClick?: (rowData: Record<string, string | number>) => void; // ← 추가
onCellDblClick?: DataCellClickCallback;
/** 데이터 셀 클릭 시 행 + 클릭한 필드명 전달 (`onRowClick`과 함께 쓸 수 있음) */
onDataCellClick?: DataCellClickCallback;
/** CheckBar 행 체크 변경 */
@@ -0,0 +1,75 @@
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
/* ══════════════════════════════════════════════════
16. 통계인구관리 > 행정동갱신 탭 컬럼 설정
══════════════════════════════════════════════════ */
/** 좌측: 감축된 행정구역(범위:동읍면) 자료 */
export const AdmdstRenewalReducedColumnConfig: ColumnInfoList = {
tabKey: "RENEWAL_REDUCED",
tabLabel: "감축된 행정구역(범위:동읍면) 자료",
rowStartNum: 1,
fixedColumns: 1,
columns: [
{
name: "adstDstrNm",
fieldName: "adstDstrNm",
header: { text: "행정동명" },
width: 200,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "adstCd",
fieldName: "adstCd",
header: { text: "행정동코드" },
width: 150,
editable: false,
styleName: "rg-cell-readonly",
},
],
columnLayout: ["adstDstrNm", "adstCd"],
};
/** 우측: 추가된 행정구역 자료 */
export const AdmdstRenewalAddedColumnConfig: ColumnInfoList = {
tabKey: "RENEWAL_ADDED",
tabLabel: "추가된 행정구역 자료",
rowStartNum: 1,
fixedColumns: 1,
columns: [
{
name: "adstDstrNm",
fieldName: "adstDstrNm",
header: { text: "행정동명" },
width: 200,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "adstCd",
fieldName: "adstCd",
header: { text: "행정동코드" },
width: 150,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "lawCd",
fieldName: "lawCd",
header: { text: "법정동코드" },
width: 150,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "lawNm",
fieldName: "lawNm",
header: { text: "법정동명" },
width: 200,
editable: false,
styleName: "rg-cell-readonly",
},
],
columnLayout: ["adstDstrNm", "adstCd", "lawCd", "lawNm"],
};
@@ -0,0 +1,143 @@
import { ValueType } from "realgrid";
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
/* ══════════════════════════════════════════════════
16. 통계인구관리 > 내국인 인구 탭 컬럼 설정
══════════════════════════════════════════════════ */
/** 좌측: 엑셀 업로드 자료 (내국인 전체 리스트) */
export const DomesticPopulationLeftColumnConfig: ColumnInfoList = {
tabKey: "DOMESTIC_LEFT",
tabLabel: "엑셀 업로드 자료 (내국인 전체 리스트)",
rowStartNum: 1,
fixedColumns: 2,
columns: [
{
name: "admdstCd",
fieldName: "admdstCd",
header: { text: "코드" },
width: 120,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "admdstNm",
fieldName: "admdstNm",
header: { text: "동명" },
width: 200,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "domplWholPpltnCnt",
fieldName: "domplWholPpltnCnt",
header: { text: "합계" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "domplMlPpltnCnt",
fieldName: "domplMlPpltnCnt",
header: { text: "남성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "domplFmlPpltnCnt",
fieldName: "domplFmlPpltnCnt",
header: { text: "여성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
],
columnLayout: [
"admdstCd",
"admdstNm",
{
name: "populationGroup",
header: { text: "인구수" },
items: ["domplWholPpltnCnt", "domplMlPpltnCnt", "domplFmlPpltnCnt"],
},
],
};
/** 우측: 미매핑 리스트 (작년 대비) */
export const DomesticPopulationRightColumnConfig: ColumnInfoList = {
tabKey: "DOMESTIC_RIGHT",
tabLabel: "미매핑 리스트 (작년 대비)",
rowStartNum: 1,
columns: [
{
name: "admdstCd",
fieldName: "admdstCd",
header: { text: "업로드코드" },
width: 130,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "admdstNm",
fieldName: "admdstNm",
header: { text: "동명" },
width: 200,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "mpinCd",
fieldName: "mpinCd",
header: { text: "맵핑코드" },
width: 130,
editable: true
},
{
name: "domplWholPpltnCnt",
fieldName: "domplWholPpltnCnt",
header: { text: "합계" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "domplMlPpltnCnt",
fieldName: "domplMlPpltnCnt",
header: { text: "남성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "domplFmlPpltnCnt",
fieldName: "domplFmlPpltnCnt",
header: { text: "여성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
],
columnLayout: [
"admdstCd",
"admdstNm",
"mpinCd",
{
name: "populationGroup",
header: { text: "인구수" },
items: ["domplWholPpltnCnt", "domplMlPpltnCnt", "domplFmlPpltnCnt"],
},
],
};
@@ -0,0 +1,162 @@
import { ValueType } from "realgrid";
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
/* ══════════════════════════════════════════════════
16. 통계인구관리 > 외국인 인구 탭 컬럼 설정
══════════════════════════════════════════════════ */
/** 좌측: 엑셀 업로드 자료 (외국인 전체 리스트) */
export const ForeignPopulationLeftColumnConfig: ColumnInfoList = {
tabKey: "FOREIGN_LEFT",
tabLabel: "엑셀 업로드 자료 (외국인 전체 리스트)",
rowStartNum: 1,
fixedColumns: 2,
columns: [
{
name: "admdstCd",
fieldName: "admdstCd",
header: { text: "코드" },
width: 120,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "admdstNm",
fieldName: "admdstNm",
header: { text: "동명" },
width: 200,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "frgnrWholPpltnCnt",
fieldName: "frgnrWholPpltnCnt",
header: { text: "합계" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "frgnrMlPpltnCnt",
fieldName: "frgnrMlPpltnCnt",
header: { text: "남성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "frgnrFmlPpltnCnt",
fieldName: "frgnrFmlPpltnCnt",
header: { text: "여성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
],
columnLayout: [
"admdstCd",
"admdstNm",
{
name: "populationGroup",
header: { text: "외국인수" },
items: ["frgnrWholPpltnCnt", "frgnrMlPpltnCnt", "frgnrFmlPpltnCnt"],
},
],
};
/** 우측: 미매핑 리스트 (작년 대비) */
export const ForeignPopulationRightColumnConfig: ColumnInfoList = {
tabKey: "FOREIGN_RIGHT",
tabLabel: "미매핑 리스트 (작년 대비)",
rowStartNum: 1,
fixedColumns: 3,
columns: [
{
name: "admdstCd",
fieldName: "admdstCd",
header: { text: "업로드코드" },
width: 130,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "sido",
fieldName: "sido",
header: { text: "시도" },
width: 150,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "gugun",
fieldName: "gugun",
header: { text: "구군" },
width: 150,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "admdstNm",
fieldName: "admdstNm",
header: { text: "동명" },
width: 200,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "mpinCd",
fieldName: "mpinCd",
header: { text: "맵핑코드" },
width: 130,
editable: true
},
{
name: "frgnrWholPpltnCnt",
fieldName: "frgnrWholPpltnCnt",
header: { text: "합계" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "frgnrMlPpltnCnt",
fieldName: "frgnrMlPpltnCnt",
header: { text: "남성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "frgnrFmlPpltnCnt",
fieldName: "frgnrFmlPpltnCnt",
header: { text: "여성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
],
columnLayout: [
"admdstCd",
"sido",
"gugun",
"admdstNm",
"mpinCd",
{
name: "populationGroup",
header: { text: "외국인수" },
items: ["frgnrWholPpltnCnt", "frgnrMlPpltnCnt", "frgnrFmlPpltnCnt"],
},
],
};
@@ -0,0 +1,121 @@
import { ValueType } from "realgrid";
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
/* ══════════════════════════════════════════════════
16. 통계인구관리 > 행정인구현황 탭 컬럼 설정
══════════════════════════════════════════════════ */
export const PopulationStatusColumnConfig: ColumnInfoList = {
tabKey: "POPULATION_STATUS",
tabLabel: "행정 인구 현황",
rowStartNum: 1,
fixedColumns: 3,
columns: [
{
name: "statsYr",
fieldName: "statsYr",
header: { text: "년도" },
width: 90,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "admdstCd",
fieldName: "admdstCd",
header: { text: "행정구역코드" },
width: 140,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "admdstNm",
fieldName: "admdstNm",
header: { text: "행정구역명" },
width: 200,
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "wholPpltnCnt",
fieldName: "wholPpltnCnt",
header: { text: "총인구" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "domplWholPpltnCnt",
fieldName: "domplWholPpltnCnt",
header: { text: "내국인 소계" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "domplMlPpltnCnt",
fieldName: "domplMlPpltnCnt",
header: { text: "내국인 남성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "domplFmlPpltnCnt",
fieldName: "domplFmlPpltnCnt",
header: { text: "내국인 여성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "frgnrWholPpltnCnt",
fieldName: "frgnrWholPpltnCnt",
header: { text: "외국인 소계" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "frgnrMlPpltnCnt",
fieldName: "frgnrMlPpltnCnt",
header: { text: "외국인 남성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
{
name: "frgnrFmlPpltnCnt",
fieldName: "frgnrFmlPpltnCnt",
header: { text: "외국인 여성" },
width: 110,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
styleName: "rg-cell-readonly",
},
],
columnLayout: [
"statsYr",
"admdstCd",
"admdstNm",
"wholPpltnCnt",
"domplWholPpltnCnt",
"domplMlPpltnCnt",
"domplFmlPpltnCnt",
"frgnrWholPpltnCnt",
"frgnrMlPpltnCnt",
"frgnrFmlPpltnCnt",
],
};
@@ -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'
]
],
},
{
tabKey: 'c',
tabLabel: '개인하수도 관리지역 지정공고 현황',
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: "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"],
},
],
},
"rmrk",
],
},
'RMK'
]
}
];
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 => ({
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 => ({
columns: tab.columns.map((col) => ({
...col,
editable: false
}))
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"],
},
'RMK'
]
"surplusGas",
],
},
"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. 하수도 재정
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 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: '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: '구분' } },
/* 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' },
/* 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: '원인자부담금',
tabKey: "ST00090001",
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: '비고' } },
...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: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN',
{ name: 'levyMainGroup', header: { text: '부과액' }, items: ['LEVY_CO', 'LEVY_AMOUNT'] },
reviewAddressLayout,
"displayYear",
{
name: 'collectGroup', header: { text: '대상별 징수현황' },
name: "financeGroup",
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'] }
]
"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",
],
},
'RMK'
]
}
];
export const SewerageFinanceInfoListRvw = rawFinanceList.map(tab => ({
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",
],
},
"rmrk",
],
},
'RMK'
]
}
];
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. 물 재이용
import type { GridView } from "@/components/type/DataGrid.type";
import {
footerAvgDecimalArrayByDivision,
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 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: 'a',
tabLabel: '공공하수처리수 재이용',
tabKey: "ST00100001",
tabLabel: "공공하수처리수 재이용",
rowStartNum: 4,
fixedColumns: 2,
columns: [
/* 1. 기본 및 행정 정보 */
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
/* 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(대상/미대상)' } },
/* 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' },
/* 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' },
/* 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' },
/* 6. 지표 및 비고 */
{ name: 'REUSE_RT', fieldName: '재이용률(%)', header: { text: '재이용률\n(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
...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: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN',
reviewFacilityAddressLayout,
"displayYear",
{
name: 'sewagePlantGroup',
header: { text: '하수처리장' },
items: ['BSC_FCT_NM', 'EQP_PRMS_DT', 'FCT_CPC', 'TPUT_TOTAL', 'RSWTR_LAW_YN']
name: "seweragePlantGroup",
header: { text: "하수처리장" },
items: ["swtrmtFcltNm", "sncnYmd", "prcsFcltCpct", "prcsQnt", "lglYn"],
},
{
name: 'reuseFctGroup',
header: { text: '재이용시설' },
name: "reuseFacilityGroup",
header: { text: "재이용시설" },
items: [
'INST_YN',
"instlYn",
{
name: 'installDetailGroup',
header: { text: '재이용시설 설치시' },
name: "installationGroup",
header: { text: "재이용시설 설치시" },
items: [
'RSWTR_RN_DT',
'RSWTR_PRMS_DT',
"oprtngStrtYmd",
"instlAprvYmd",
{
name: 'reProcessGroup',
header: { text: '재처리시설' },
items: ['RSWTR_CPC', 'RSWTR_TRT_NM']
name: "reProcessingFacilityGroup",
header: { text: "재처리시설" },
items: ["rprcsFcltCpct", "mtcnsNm"],
},
'RSWTR_PIPE',
'RSWTR_PUMP'
]
}
]
"ruswtPplLen",
"ruswtPumpLen",
],
},
],
},
{
name: 'reuseStatusGroup',
header: { text: '하수처리수 재이용현황(천톤/년)' },
name: "reuseStatusGroup",
header: { text: "하수처리수 재이용현황(천톤/년)" },
items: [
'USE_AMNT_TOTAL',
"wholUseQnt",
{
name: 'internalUseGroup',
header: { text: '장 내 용 수' },
name: "internalWaterGroup",
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'
]
"ithlWshwtrQnt",
"ithlClnQty",
"ithlFrzgQty",
"ithlLdhcUswtrQnt",
"ithlDltnUswtrQnt",
"ithlRuswtrQnt",
"ithlEtcQnt",
"ithlUnsdQnt",
],
},
{
name: 'externalUseGroup',
header: { text: '장 외 용 수' },
name: "externalWaterGroup",
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'
]
}
]
"otcntrUswtrWhol",
"otcntrClnRstrmQnt",
"otcntrWshwtrQnt",
"otcntrLdhcUswtrQnt",
"otcntrHdrplUswtrQnt",
"otcntrRvrKeepQnt",
"agrcltUswtrPstnQnt",
"ugwtrChargQnt",
"mdsrUswtrQnt",
],
},
'REUSE_RT',
'RMK'
]
],
},
"rtlztnRt",
"rmrk",
],
},
{
tabKey: 'b',
tabLabel: '중수도 현황',
tabKey: "ST00100002",
tabLabel: "중수도 현황",
rowStartNum: 2,
fixedColumns: 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: '비고' } }
...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: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'BILD_NM', 'ADDR', 'YR_GBN', 'BILD_TP', 'LAW_YN', 'COMP_DT', 'RN_DT',
reviewFacilityAddressLayout,
"displayYear",
"fcltNm",
"bldgSeCn",
"fcltPstnCn",
"lglTrgtYn",
"cmptnStrtYmd",
"oprtngStrtYmd",
{
name: 'fctStatusGroup',
header: { text: '시설현황' },
items: ['TOTAR', 'PROC_CPCTY']
name: "facilityStatusGroup",
header: { text: "시설현황" },
items: ["archWholArea", "prcsCpct"],
},
'WATER_USE_CPCTY', 'RUSWTR_USE_SM', 'SUB_USE_CPCTY', 'RUSWTR_PRPOS', 'FYER_OPER_CT', 'RMK'
]
"wtrUseQnt",
"ruswtrUtztnQnt",
"sbstCntUtztnQnt",
"mnUsgCn",
"ruswtrOperAmt",
"rmrk",
],
},
{
tabKey: 'c',
tabLabel: '빗물이용시설 현황',
tabKey: "ST00100003",
tabLabel: "빗물이용시설 현황",
rowStartNum: 2,
fixedColumns: 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: '비고' } },
...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: [
{ 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'
]
reviewFacilityAddressLayout,
"displayYear",
"fcltNm",
"bdstTypCn",
"fcltPstnCn",
"lglTrgtYn",
"instlCmptnYmd",
"instlAmt",
"rcptMynCn",
"filtrPrcsFcltYnExpln",
"impdCpct",
"anlUseQnt",
"exlnUtlzUsgExpln",
"anlOperAmt",
"rmrk",
],
},
{
tabKey: 'd',
tabLabel: '하ㆍ폐수처리수 재이용시설 시공업 등록현황',
tabKey: "ST00100004",
tabLabel: "하ㆍ폐수처리수 재이용시설 시공업 등록현황",
rowStartNum: 2,
fixedColumns: 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: '사용자의견' } },
...reviewFacilityAddressColumns,
textCol("bplcNm", "상호(명칭)"),
textCol("rprsvNm", "대표자"),
textCol("bplcPstnCn", "주소"),
textCol("regDt", "등록일\nyyyy-mm-dd"),
textCol("rmrk", "비고"),
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN', 'CMPNM', 'RPRSNTV', 'ADDR', 'CMPN_RGSDE', 'RMK'
]
reviewFacilityAddressLayout,
"displayYear",
"bplcNm",
"rprsvNm",
"bplcPstnCn",
"regDt",
"rmrk",
],
},
{
tabKey: 'e',
tabLabel: '하ㆍ폐수처리수 재이용시설 시공업 인가현황',
tabKey: "ST00100005",
tabLabel: "하ㆍ폐수처리수 재이용시설 시공업 인가현황",
rowStartNum: 2,
fixedColumns: 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: '비고' } }
...reviewFacilityAddressColumns,
textCol("bplcNm", "상호(명칭)"),
textCol("rprsvNm", "대표자"),
textCol("bplcPstnCn", "주소"),
textCol("coRegYmd", "인가일\n(년/월/일)"),
textCol("rmrk", "비고"),
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN', 'CMPNM', 'RPRSNTV', 'ADDR', 'CMPN_RGSDE', 'RMK'
]
}
reviewFacilityAddressLayout,
"displayYear",
"bplcNm",
"rprsvNm",
"bplcPstnCn",
"coRegYmd",
"rmrk",
],
},
];
export const WaterReuseInfoList = rawWaterReuseList.map(tab => ({
export const WaterReuseInfoListRvw = rawWaterReuseList.map((tab) => ({
...tab,
columns: tab.columns.map(col => ({
columns: tab.columns.map((col) => ({
...col,
editable: false
}))
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 → "").
* 어떤 엔티티 타입이든 공통 사용 가능.
@@ -62,6 +62,14 @@ export function toCompactYmd(
return "";
}
/** 그리드·API 값 → 8자리 일자(yyyyMMdd). 없거나 유효하지 않으면 null */
export function toCompactYmdOrNull(
value: string | number | Date | null | undefined,
): string | null {
const compact = toCompactYmd(value);
return compact === "" ? null : compact;
}
/** 설치목적 등 O/X 표기 → 대문자 O 또는 X (그 외는 원문 유지) */
export function normalizeOxYnValue(
value: string | number | 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`,
@@ -0,0 +1,242 @@
import { ENDPOINT } from "@/shared/config";
import { stClient } from "@/shared/api/client";
import type {
DomesticPopulationListResponse,
DomesticPopulationFinalResponse,
UpperCodesByNameResponse,
DomesticPopulationMappingItem,
} from "../model/ppltn/statistics-domestic-population-interface";
// 1. 응답 데이터의 타입을 정의합니다 (이미 정의된 것이 있다면 그것을 사용하세요)
interface ChartDataResponse {
status: string;
data: {
X_AXIS: string[];
TOT: number[];
ORIGIN_MALE: (number | null)[];
ORIGIN_FEMALE: (number | null)[];
FOREIGN_MALE: (number | null)[];
FOREIGN_FEMALE: (number | null)[];
};
message?: string;
}
export class StatisticsPpltnApiService {
/** 내국인 엑셀 임시 업로드 */
public async uploadDomesticPopulationExcel(
file: File,
statsYr: string,
): Promise<{ status: string; message?: string }> {
const formData = new FormData();
formData.append("file", file);
formData.append("statsYr", statsYr);
const response = await stClient.post<{ status: string; message?: string }>(
`${ENDPOINT.ST_SERVICE.PPLTN}/domestic-population-excel`,
formData,
{ headers: { "Content-Type": "multipart/form-data" } },
);
return response.data;
}
/** 내국인 임시 적재 리스트 조회
* @param opt1 "1" = 전체 리스트(좌측), "2" = 미매핑 리스트(우측)
*/
public async getDomesticPopulationList(
statsYr: string,
opt1: "1" | "2",
): Promise<DomesticPopulationListResponse> {
const response = await stClient.get<DomesticPopulationListResponse>(
`${ENDPOINT.ST_SERVICE.PPLTN}/domestic-population-list`,
{ params: { statsYr, opt1 } },
);
return response.data;
}
/** 내국인 최종 마스터 반영 */
public async saveDomesticPopulationFinal(
statsYr: string,
regList: DomesticPopulationMappingItem[],
): Promise<DomesticPopulationFinalResponse> {
const response = await stClient.post<DomesticPopulationFinalResponse>(
`${ENDPOINT.ST_SERVICE.PPLTN}/domestic-population-final`,
{ YR: statsYr, regList },
);
return response.data;
}
/** 미매핑 행 클릭 시 상위 코드 후보 조회 */
public async getUpperCodesByName(
statsYr: string,
admdstNm: string,
admdstCd: string
): Promise<UpperCodesByNameResponse> {
// GET 요청으로 변경하고 params 객체를 추가합니다.
const response = await stClient.get<UpperCodesByNameResponse>(
`${ENDPOINT.ST_SERVICE.PPLTN}/upper-codes-by-name`,
{
params: { statsYr, admdstNm,admdstCd }
},
);
return response.data;
}
/* ════════════════════════════════════
외국인
════════════════════════════════════ */
/** 외국인 엑셀 임시 업로드 */
public async uploadForeignPopulationExcel(
file: File,
statsYr: string,
): Promise<{ status: string; message?: string }> {
const formData = new FormData();
formData.append("file", file);
formData.append("statsYr", statsYr);
const response = await stClient.post<{ status: string; message?: string }>(
`${ENDPOINT.ST_SERVICE.PPLTN}/foreign-population-excel`,
formData,
{ headers: { "Content-Type": "multipart/form-data" } },
);
return response.data;
}
/** 외국인 임시 적재 리스트 조회
* @param opt1 "1" = 전체 리스트(좌측), "2" = 미매핑 리스트(우측)
*/
public async getForeignPopulationList(
statsYr: string,
opt1: "1" | "2",
): Promise<{ status: string; data: object[]; message?: string }> {
const response = await stClient.get<{ status: string; data: object[]; message?: string }>(
`${ENDPOINT.ST_SERVICE.PPLTN}/foreign-population-list`,
{ params: { statsYr, opt1 } },
);
return response.data;
}
/** 외국인 최종 마스터 반영 */
public async saveForeignPopulationFinal(
statsYr: string,
regList: DomesticPopulationMappingItem[],
): Promise<{ status: string; message?: string }> {
const response = await stClient.post<{ status: string; message?: string }>(
`${ENDPOINT.ST_SERVICE.PPLTN}/foreign-population-final`,
{ statsYr, regList },
);
return response.data;
}
/* ════════════════════════════════════
행정동갱신
════════════════════════════════════ */
/** 행정동 갱신 — 감축/추가 행정구역 조회
* @param opt1 "1" = 추가된 행정구역, "2" = 감축된 행정구역
*/
public async searchChangedArea(
statsYr: string,
opt1: "1" | "2",
): Promise<{ status: string; data: object[]; message?: string }> {
const response = await stClient.get<{ status: string; data: object[]; message?: string }>(
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/search`,
{ params: { statsYr, opt1 } },
);
return response.data;
}
/** 행정동 갱신 — 감축/추가 반영 */
public async saveChangedArea(
reducedList: object[],
addedList: object[],
): Promise<{ status: string; message?: string }> {
const response = await stClient.post<{ status: string; message?: string }>(
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/save`,
{ reducedList, addedList },
);
return response.data;
}
// /** 행정동 갱신 — 법정동코드 후보 조회 (행정동명 클릭 시) */
// public async getRenewalUpperCodesByName(
// statsYr: string,
// admdstNm: string,
// ): Promise<UpperCodesByNameResponse> {
// const response = await stClient.get<UpperCodesByNameResponse>(
// `${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/upper-codes-by-name`,
// {
// params: { statsYr, admdstNm }
// },
// );
// return response.data;
// }
/** 행정동 갱신 — 법정동코드 후보 조회 */
public async getLawCdsByName(
adstCd : string,
): Promise<UpperCodesByNameResponse> {
const response = await stClient.get<UpperCodesByNameResponse>(
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/law-codes-by-name`,
{
params: { adstCd }
},
);
return response.data;
}
/* ════════════════════════════════════
행정인구현황
════════════════════════════════════ */
/** 행정인구현황 — 체크박스 조건에 따른 인구 현황 조회 */
public async searchPopByEachCondition(
statsYr: string,
oMale: boolean,
oFemale: boolean,
fMale: boolean,
fFemale: boolean,
): Promise<{ status: string; data: object[]; message?: string }> {
const response = await stClient.get<{ status: string; data: object[]; message?: string }>(
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/pop-by-condition`,
{
params: {
statsYr,
oMale: oMale ? "TRUE" : "FALSE",
oFemale: oFemale ? "TRUE" : "FALSE",
fMale: fMale ? "TRUE" : "FALSE",
fFemale: fFemale ? "TRUE" : "FALSE",
}
},
);
return response.data;
}
/** 행정인구현황 시각화 — 기준연도 기준 최대 5년치 차트 데이터 조회 */
public async getChartDatas1(
statsYr: string,
oMale: boolean,
oFemale: boolean,
fMale: boolean,
fFemale: boolean,
): Promise<ChartDataResponse> {
const response = await stClient.get<ChartDataResponse>(
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/chart-datas`,
{
params: {
statsYr,
oMale: oMale ? "TRUE" : "FALSE",
oFemale: oFemale ? "TRUE" : "FALSE",
fMale: fMale ? "TRUE" : "FALSE",
fFemale: fFemale ? "TRUE" : "FALSE",
}
},
);
return response.data;
}
}
export const statisticsPpltnApi = new StatisticsPpltnApiService();
@@ -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;
}
@@ -97,8 +97,11 @@ export interface ExcretaTreatmentFacilityRequestParams {
*/
export type ExcretaTreatmentFacilitySaveItem = Omit<
ExcretaTreatmentFacilityItem,
"sggNm" | "verificationResult"
>;
"sggNm" | "verificationResult" | "cmptnStrtYmd" | "oprtngStrtYmd"
> & {
cmptnStrtYmd: string | null;
oprtngStrtYmd: string | null;
};
/** 분뇨처리시설(ST00070001) 저장 API 요청 */
export interface ExcretaTreatmentFacilitySaveRequest {
@@ -106,7 +106,10 @@ export type PumpingStationSaveItem = Omit<
| "exlnQnt"
| "drtwPumpGrssPpqty"
| "exlnPumpGrssPpqty"
>;
| "oprtngStrtYmd"
> & {
oprtngStrtYmd: string | null;
};
/** 펌프장(ST00050001) 저장 API 요청 */
export interface PumpingStationSaveRequest {
@@ -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;
@@ -172,8 +172,10 @@ export interface SewageReservoirRequestParams {
*/
export type SewageReservoirSaveItem = Omit<
SewageReservoirItem,
"sggNm" | "verificationResult"
>;
"sggNm" | "verificationResult" | "oprtngStrtYmd"
> & {
oprtngStrtYmd: string | null;
};
/** 하수저류시설(ST00040001) 저장 API 요청 */
export interface SewageReservoirSaveRequest {
@@ -229,8 +231,10 @@ export interface SewageReservoirSubmitResponse {
*/
export type RainfallPumpStationSaveItem = Omit<
RainfallPumpStationItem,
"sggNm" | "verificationResult"
>;
"sggNm" | "verificationResult" | "oprtngStrtYmd"
> & {
oprtngStrtYmd: string | null;
};
/**
* 빗물펌프장(ST00040002) 저장 API 요청
@@ -113,8 +113,8 @@ export interface PublicSewageTreatWaterReuseSaveItem {
wholPrcsQnt: number;
lglYn: string;
instlYn: string;
oprtngStrtYmd: string;
instlAprvYmd: string;
oprtngStrtYmd: string | null;
instlAprvYmd: string | null;
rprcsFcltCpct: number;
mtcnsNm: string;
ruswtPplLen: number;
@@ -209,8 +209,11 @@ export type RecycledWaterStatusApiResponse = ApiResponse<RecycledWaterStatusResp
/** ST00100002 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */
export type RecycledWaterStatusSaveItem = Omit<
RecycledWaterStatusItem,
"sggNm" | "verificationResult"
>;
"sggNm" | "verificationResult" | "cmptnStrtYmd" | "oprtngStrtYmd"
> & {
cmptnStrtYmd: string | null;
oprtngStrtYmd: string | null;
};
/** ST00100002 저장 API 요청 */
export interface RecycledWaterStatusSaveRequest {
@@ -300,8 +303,10 @@ export type RainFallUtilFactStatusApiResponse =
/** ST00100003 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */
export type RainFallUtilFactStatusSaveItem = Omit<
RainFallUtilFactStatusItem,
"sggNm" | "verificationResult"
>;
"sggNm" | "verificationResult" | "cmptYmd"
> & {
cmptYmd: string | null;
};
/** ST00100003 저장 API 요청 */
export interface RainFallUtilFactStatusSaveRequest {
@@ -383,8 +388,10 @@ export type TreatWaterReuseDesignConstStatusApiResponse =
/** ST00100004 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */
export type TreatWaterReuseDesignConstStatusSaveItem = Omit<
TreatWaterReuseDesignConstStatusItem,
"sggNm" | "verificationResult"
>;
"sggNm" | "verificationResult" | "coRegYmd"
> & {
coRegYmd: string | null;
};
/** ST00100004 저장 API 요청 */
export interface TreatWaterReuseDesignConstStatusSaveRequest {
@@ -467,8 +474,10 @@ export type TreatWaterReuseBusinessApprStatusApiResponse =
/** ST00100005 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */
export type TreatWaterReuseBusinessApprStatusSaveItem = Omit<
TreatWaterReuseBusinessApprStatusItem,
"sggNm" | "verificationResult"
>;
"sggNm" | "verificationResult" | "coRegYmd"
> & {
coRegYmd: string | null;
};
/** ST00100005 저장 API 요청 */
export interface TreatWaterReuseBusinessApprStatusSaveRequest {
@@ -520,6 +529,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 +604,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,70 @@
import type { ApiResponse } from "@/entities/statistics/model/statistics-interface";
/* ══════════════════════════════════════════════════
16. 통계인구관리 > 내국인 인구 관련 인터페이스
══════════════════════════════════════════════════ */
/** 내국인 인구 임시 적재 행 (좌측 전체 리스트 / 우측 미매핑 리스트 공통) */
export interface DomesticPopulationItem {
statsYr: string; // 통계연도
admdstCd: string; // 행정구역코드
admdstNm: string; // 행정구역명
domplWholPpltnCnt: number; // 내국인 합계 인구수
domplMlPpltnCnt: number; // 내국인 남성 인구수
domplFmlPpltnCnt: number; // 내국인 여성 인구수
/** 미매핑 리스트 전용 - 매핑 대상 행정구역코드 (상위코드) */
mpinCd: string;
}
/** 엑셀 업로드 요청 파라미터 */
export interface DomesticPopulationUploadParams {
file: File;
statsYr: string;
}
/** 임시 적재 리스트 조회 요청 파라미터 */
export interface DomesticPopulationListParams {
statsYr: string;
/** 1: 전체 리스트(좌측), 2: 미매핑 리스트(우측) */
opt1: '1' | '2';
}
/** 수동 매핑 후 최종 반영 요청 파라미터 */
export interface DomesticPopulationFinalParams {
YR: string;
/** 수동 매핑된 행 목록 (admdstCd → subAdmdstCd 매핑 정보) */
regList: DomesticPopulationMappingItem[];
}
/** 수동 매핑 단건 아이템 */
export interface DomesticPopulationMappingItem {
statsYr: string;
admdstCd: string;
admdstNm: string;
subAdmdstCd: string;
}
/** 내국인/외국인 상위 코드 후보 조회 요청 */
export interface UpperCodesByNameParams {
statsYr: string;
admdstNm: string;
}
/** 내국인/외국인 상위 코드 후보 단건 아이템 */
export interface UpperCodeCandidateItem {
admdstCd: string; // 행정동코드
admdstNm: string; // 행정동명
[key: string]: string;
}
/** 행정동 갱신 법정동코드 후보 단건 아이템 */
export interface LawCodeCandidateItem {
lawCd: string; // 법정동코드
lawNm: string; // 법정동명
[key: string]: string;
}
/* ── API 응답 타입 ── */
export type DomesticPopulationListResponse = ApiResponse<DomesticPopulationItem[]>;
export type DomesticPopulationFinalResponse = ApiResponse<string>;
export type UpperCodesByNameResponse = ApiResponse<UpperCodeCandidateItem[]>;
@@ -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 ?? {};
}
@@ -31,7 +31,7 @@ export interface cancelSubmitResponse {
export interface ApiResponse<T> {
status: string;
data: T;
data?: T;
message?: string;
}
@@ -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;
admdstNm: string | null;
admdstCd: string;
displayYear: string;
rowType: 'OLD' | 'CURR';
pumpstNm: string;
impdFcltPstnCn: string;
oprtngStrtYmd: string;
pumpSeNm: string;
swtrmtFcltNm: 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;
// 오수펌프 (DRTW)
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;
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;
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: {
@@ -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(
category
? import(
`@/components/statistics/${category}/${resolvedComponentName}`
),
)
: import(`@/components/statistics/${resolvedComponentName}`),
{ ssr: false, loading: () => <SkeletonPage /> },
);
+1
View File
@@ -71,6 +71,7 @@ export const ENDPOINT = {
INPUT: "/st-input-service/api/v1/input",
REVIEW: "/st-management-service/api/v1/review",
STATS: "/st-management-service/api/stats",
PPLTN: "/st-management-service/api/v1/ppltn",
INPUTCOMMON: "/st-input-service/api/v1/common",
INPUTVERIFY: "/st-input-service/api/v1/verify",
TOTAL: "/st-management-service/api/v1/stats",
@@ -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}`;
}
+140 -1
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,7 +65,48 @@ 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}
@@ -32,6 +120,7 @@ const TabBar = () => {
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)}
>
<span className="flex-1 truncate text-xl leading-none">
{tab.label}
@@ -48,6 +137,56 @@ const TabBar = () => {
))}
</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,