@@ -468,9 +466,7 @@ function AttachmentsFormSection({
fileName={item.file?.name ?? item.fileName}
isUploading={isUploading[item.fileKey] ?? false}
onRemove={() => void removeEtcFile(idx)}
- onDownload={
- item.atflId ? () => downloadByAtflId(item.atflId!) : undefined
- }
+ onDownload={item.atflId ? () => downloadByAtflId(item.atflId!) : undefined}
/>
))}
diff --git a/apps/sn/AGENTS.md b/apps/sn/AGENTS.md
index a4923b8c4..f8b84ffa9 100644
--- a/apps/sn/AGENTS.md
+++ b/apps/sn/AGENTS.md
@@ -134,6 +134,8 @@ Codex 작업 루트는 가능하면 `../../../../../`로 잡고, 공통 시작
## 작업 원칙
+- 일반 목록·모든 팝업은 공통 AGENTS.md의 서버 페이징·지도·Excel 계약을 따른다. 지도 전체 표출에 현재 그리드 페이지 배열을 재사용하지 않는다. Excel은 적용조건 전체를 사용하고 대량 원시 자료는 서버 생성으로 분리한다. 화면 페이징/페이징 없음 예외는 사유와 검증 범위를 메뉴 > 화면명별로 기록한다.
+
1. 모든 변경은 원본 요구사항정의서와 화면설계서, 2차 요구사항, 기능 명세 또는 승인된 작업 문서를 근거로 한다.
2. `apps/sn`과 `packages/common/business_components` 범위를 넘는 수정은 하지 않는다.
3. 공통 패키지 수정은 `sn` 요구사항을 충족하기 위한 최소 범위로 제한한다.
diff --git a/apps/sn/app/(map-layout)/map/components/common/table.ts b/apps/sn/app/(map-layout)/map/components/common/table.ts
index 8a13eb830..a12e8c8e1 100644
--- a/apps/sn/app/(map-layout)/map/components/common/table.ts
+++ b/apps/sn/app/(map-layout)/map/components/common/table.ts
@@ -529,6 +529,15 @@ export const FldngDpthMapCol = [
// { id: 'shape_area', title: '형상면적(㎡)' },
// { id: 'shape_len', title: '형상길이' },
];
+/** 기본정보 목록 API의 camelCase 속성을 침수흔적심선 그리드에 연결한다. */
+export const FldngDpthMapCol2 = [
+ { id: 'rowNumber', title: '순번' },
+ { id: 'fldngYr', title: '침수년도' },
+ { id: 'ctpvNm', title: '시도명' },
+ { id: 'sggNm', title: '시군구명' },
+ { id: 'fldngDpth', title: '침수깊이(m)' },
+ { id: 'fldngArea', title: '침수면적(㎡)' },
+];
export const FldngWtlvMapExcelCol = {
'fldngYr': '침수년도',
'fldngDstrNm': '침수재해명',
diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx
index b289831a3..79a1265a6 100644
--- a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx
+++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx
@@ -8,10 +8,9 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, Input } from "@common/components";
import { StandardPopupLayout } from "@common/business_components";
import { getRawJson } from "@/components/lib/api";
-import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
+import { requestGridExcel } from "@/shared/api/grid-export";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
import { useAlertStore } from "@/shared/lib";
-import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
import useStore from "@/store/useStore";
import LoadingOverlay from "@/components/ui/LoadingBar";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
@@ -19,8 +18,6 @@ import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import {
FLDNG_HSTRY_LAYER_TYPE,
FLDNG_HSTRY_DEFAULT_FILTERS,
- FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE,
- FLDNG_HSTRY_DOWNLOAD_HEADERS,
FLDNG_HSTRY_LIST_HEADLIST,
FLDNG_HSTRY_PAGE_SIZE,
FLDNG_HSTRY_ROW_SELECT_MESSAGE,
@@ -58,26 +55,6 @@ const resolveFldngHstryMapFeatureFid = (row: FldngHstryTableRow) => {
return "";
};
-const buildDownloadRows = (rows: FldngHstryTableRow[]) =>
- rows.map((row) => ({
- rowNo: row.rowNo,
- floodYearDisplay: row.floodYearDisplay,
- ctpvNm: row.ctpvNm,
- sggNm: row.sggNm,
- disasterName: row.disasterName,
- rainBgngTmDisplay: row.rainBgngTmDisplay,
- rainEndTmDisplay: row.rainEndTmDisplay,
- rainTotDisplay: row.rainTotDisplay,
- rainStrDisplay: row.rainStrDisplay,
- floodHourDisplay: row.floodHourDisplay,
- districtName: row.districtName,
- causeDetailCode: row.causeDetailCode,
- causeDetailName: row.causeDetailName,
- avgFloodDepthDisplay: row.avgFloodDepthDisplay,
- floodAreaDisplay: row.floodAreaDisplay,
- exmnYearDisplay: row.exmnYearDisplay,
- damageAmountDisplay: row.damageAmountDisplay,
- }));
const resolveRegionNames = (
row: FldngHstryTableRow,
@@ -265,19 +242,13 @@ export default function FldngHstryPopupPage() {
setIsLoading(true);
try {
- const response = await fetchAllPages({
- batchSize: FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE,
- fetchPage: (page, size) => fetchRows(page + 1, appliedFilters, size),
- getItems: (page) => page.rows,
- getTotal: (page) => page.total,
- });
- await downloadExcelWithKoreanHeaders({
- data: buildDownloadRows(response.items) as unknown as JSON[],
- fileName: `침수발생이력_${new Date().toISOString().slice(0, 10)}.xlsx`,
- sheetNameArray: ["침수발생이력"],
- headerRange: "A1:Q1",
- headerMapping: FLDNG_HSTRY_DOWNLOAD_HEADERS,
- });
+ const params = new URLSearchParams(buildFldngHstryListQueryString(appliedFilters, 1, FLDNG_HSTRY_PAGE_SIZE));
+ params.delete("page"); params.delete("size");
+ await requestGridExcel("v1/spatialGridExports/floodHistories", {
+ history: Object.fromEntries(params),
+ ctpvNames: Object.fromEntries(ctpv.map((item) => [String(item.val).trim(), item.text])),
+ sggNames: Object.fromEntries(sgg.map((item) => [String(item.val).trim(), item.text])),
+ }, `침수발생이력_${new Date().toISOString().slice(0, 10)}.xlsx`);
} catch (error) {
console.error("침수발생이력 다운로드 중 오류:", error);
alert(
diff --git a/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx
index 25c2b5977..32d8d1004 100644
--- a/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx
+++ b/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx
@@ -11,8 +11,7 @@ import {
type UiHeadProps,
type UiTableSort,
} from "@common/business_components";
-import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
-import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
+import { downloadAnalysisExcel } from "@/shared/lib/download-analysis-excel";
import { useAlertStore } from "@/shared/lib";
import { Button } from "@common/components";
@@ -241,29 +240,7 @@ export default function InftwtResultPopupPage() {
try {
setIsLoading(true);
- const response = await fetchAllPages
>({
- fetchPage: (page, size) =>
- getData(
- `v1/inftwtAnlsRslt/ppl/${selectedRow.inftwtAnlsSn}?page=${page}&size=${size}`,
- ),
- getItems: (page) =>
- Array.isArray(page?.content) ? page.content : [],
- getTotal: (page) => Number(page?.totalElements ?? 0),
- });
- const excelRows = response.items;
-
- await downloadExcelWithKoreanHeaders({
- data: excelRows as unknown as JSON[],
- fileName: `침입수분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.inftwtAnlsSn}`,
- sheetNameArray: ["침입수 분석결과"],
- headerRange: "A1:D1",
- headerMapping: {
- inftwtAnlsSn: "침입수분석\n분석번호",
- pplMngNo: "관로\n관리번호",
- anlsTm: "분석시각",
- predcOilq: "예측유량\n(m³/h)",
- },
- });
+ await downloadAnalysisExcel("inftwt", selectedRow.inftwtAnlsSn, `침입수분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.inftwtAnlsSn}`);
} catch (error) {
console.error("침입수 분석결과 다운로드 중 오류:", error);
alert("침입수 분석결과 다운로드 중 오류가 발생했습니다.");
diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx
index e04012733..06c82e009 100644
--- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx
+++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx
@@ -8,20 +8,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { Button, Input } from "@common/components";
import { StandardPopupLayout } from "@common/business_components";
-import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
+import { requestGridExcel } from "@/shared/api/grid-export";
import { getData } from "@/components/lib/api";
import { appendApiSearchParams } from "@/shared/util/util";
import LoadingOverlay from "@/components/ui/LoadingBar";
import useStore from "@/store/useStore";
import { useAlertStore } from "@/shared/lib";
-import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
import { useUserStore } from "@/shared/lib/use-user-store";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from "../common/table";
import {
PIPE_ANALYSIS_DETAIL_DEFAULT_FILTERS,
- PIPE_ANALYSIS_DETAIL_EXCEL_HEADERS,
PIPE_ANALYSIS_DETAIL_HEADLIST,
PIPE_ANALYSIS_DETAIL_ROW_SELECT_MESSAGE,
applyScopeToDetailFilters,
@@ -41,19 +39,6 @@ import {
type DetailViewRow = ReturnType[number];
const PIPE_ANALYSIS_DETAIL_PAGE_SIZE = 10;
const PIPE_ANALYSIS_MAP_WINDOW_NAME = "sn-sewerage-map-root";
-const PIPE_ANALYSIS_DETAIL_EXPORT_KEYS = [
- "rowNo",
- "ctpvNm",
- "sggNm",
- "fcltMngNo",
- "instMngNo",
- "swtrmtZoneNm",
- "instlYmd",
- "useNm",
- "pdmtVl",
- "prlgVl",
- "materialName",
-] as const;
const PIPE_ANALYSIS_DETAIL_STANDARD_HEADLIST = PIPE_ANALYSIS_DETAIL_HEADLIST;
const resolveErrorMessage = (error: unknown, fallbackMessage: string) =>
@@ -240,33 +225,19 @@ export default function PipeAnalysisDetailPopupPage() {
try {
setIsLoading(true);
- const response = await fetchAllPages({
- fetchPage: (page, size) => fetchRows(page + 1, appliedFilters, size),
- getItems: (page) => page.rows,
- getTotal: (page) => page.total,
- });
- const exportRows = response.items.map((row) => ({
- rowNo: row.rowNo,
- ctpvNm: row.ctpvNm,
- sggNm: row.sggNm,
- fcltMngNo: row.fcltMngNo,
- instMngNo: row.instMngNo,
- swtrmtZoneNm: row.swtrmtZoneNm,
- instlYmd: row.instlYmd,
- useNm: row.useNm,
- pdmtVl: row.pdmtVl,
- prlgVl: row.prlgVl,
- materialName:
- resolveMaterialKoreanName(row.materialCode) || row.materialName,
- }));
- const detailHeaderRange = `A1:${String.fromCharCode(64 + PIPE_ANALYSIS_DETAIL_EXPORT_KEYS.length)}1`;
- await downloadExcelWithKoreanHeaders({
- data: exportRows as unknown as JSON[],
- fileName: `관로분석_관로현황_${buildPipeAnalysisFileDate()}`,
- sheetNameArray: ["관로현황"],
- headerRange: detailHeaderRange,
- headerMapping: PIPE_ANALYSIS_DETAIL_EXCEL_HEADERS,
+ const params = new URLSearchParams({ analysisType: baseAnalysisType });
+ appendApiSearchParams(params, {
+ ctpvCd: appliedFilters.ctpvCd, sggCd: appliedFilters.sggCd,
+ swtrmtZoneNm: appliedFilters.swtrmtZoneNm, elapsedYearBucket: baseElapsedYearBucket,
+ materialCode: appliedFilters.materialCode, useCd: appliedFilters.useCd,
+ diameterFrom: appliedFilters.diameterFrom, diameterTo: appliedFilters.diameterTo,
});
+ if (scope.regionCode) params.set("scopeRegionCode", scope.regionCode);
+ await requestGridExcel("v1/spatialGridExports/pipes", {
+ pipe: Object.fromEntries(params),
+ ctpvNames: Object.fromEntries(ctpv.map((item) => [String(item.val).trim(), item.text])),
+ sggNames: Object.fromEntries(sgg.map((item) => [String(item.val).trim(), item.text])),
+ }, `관로분석_관로현황_${buildPipeAnalysisFileDate()}.xlsx`);
} catch (error) {
console.error("관로현황 다운로드 중 오류:", error);
alert(
diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx
index ad07fc886..fcc656ad7 100644
--- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx
+++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx
@@ -83,8 +83,12 @@ export default function PipeAnalysisPopupPage() {
Array<{ value: string; label: string }>
>([]);
const [summaryCurrentPage, setSummaryCurrentPage] = useState(1);
+ // 복합표는 물리 행 수가 아니라 서버가 반환하는 처리구역 그룹 수를 사용한다.
+ const [summaryTotal, setSummaryTotal] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const initializedRef = useRef(false);
+ // 늦게 완료된 이전 페이지 응답이 최신 조회조건의 표를 덮어쓰지 않게 한다.
+ const summaryRequestId = useRef(0);
useEffect(() => {
if (ctpv.length === 0) fetchCtpv();
@@ -124,9 +128,13 @@ export default function PipeAnalysisPopupPage() {
}, [draftFilters.ctpvCd, draftFilters.sggCd, scope.regionCode, sgg]);
const fetchSummary = useCallback(
- async (nextFilters: PipeAnalysisSummaryFilters) => {
+ async (nextFilters: PipeAnalysisSummaryFilters, page = 1) => {
+ const requestId = ++summaryRequestId.current;
+ setIsLoading(true);
const params = new URLSearchParams({
analysisType: nextFilters.analysisType,
+ page: String(page - 1),
+ size: String(PIPE_ANALYSIS_SUMMARY_PAGE_SIZE),
});
appendApiSearchParams(params, {
@@ -137,22 +145,27 @@ export default function PipeAnalysisPopupPage() {
if (nextFilters.scopeRegionCode)
params.set("scopeRegionCode", nextFilters.scopeRegionCode);
- const response = (await getData(
- `v1/fclts/pplAnalysis/summaries?${params.toString()}`,
- )) as PipeAnalysisSummaryResponse;
-
- const nextMaterialColumns = Array.isArray(response?.materialColumns)
- ? response.materialColumns
- : [];
- setMaterialColumns(nextMaterialColumns);
- setRows(buildPipeAnalysisSummaryViewRows(response, ctpv, sgg));
+ try {
+ const response = (await getData(
+ `v1/fclts/pplAnalysis/summaryPage?${params.toString()}`,
+ )) as PipeAnalysisSummaryResponse & { count: number };
+ if (requestId !== summaryRequestId.current) return;
+ const nextMaterialColumns = Array.isArray(response?.materialColumns) ? response.materialColumns : [];
+ setMaterialColumns(nextMaterialColumns);
+ setRows(buildPipeAnalysisSummaryViewRows(response, ctpv, sgg));
+ setSummaryTotal(response.count);
+ setSummaryCurrentPage(page);
+ } catch (error) {
+ if (requestId === summaryRequestId.current) throw error;
+ } finally {
+ if (requestId === summaryRequestId.current) setIsLoading(false);
+ }
},
[ctpv, sgg],
);
const handleSearch = useCallback(
async (nextFilters = draftFilters) => {
- setIsLoading(true);
try {
const scopedFilters = applyScopeToSummaryFilters(nextFilters, scope);
setDraftFilters(scopedFilters);
@@ -168,9 +181,8 @@ export default function PipeAnalysisPopupPage() {
),
);
setRows([]);
+ setSummaryTotal(0);
setMaterialColumns([]);
- } finally {
- setIsLoading(false);
}
},
[alert, draftFilters, fetchSummary, scope],
@@ -191,6 +203,18 @@ export default function PipeAnalysisPopupPage() {
}
try {
+ // 화면은 한 페이지뿐이므로 Excel만 별도 전체 집계 계약으로 조회한다.
+ const params = new URLSearchParams({ analysisType: appliedFilters.analysisType });
+ appendApiSearchParams(params, {
+ ctpvCd: appliedFilters.ctpvCd, sggCd: appliedFilters.sggCd,
+ swtrmtZoneNm: appliedFilters.swtrmtZoneNm,
+ });
+ if (appliedFilters.scopeRegionCode) params.set("scopeRegionCode", appliedFilters.scopeRegionCode);
+ const response = await getData(`v1/fclts/pplAnalysis/summaries?${params}`) as PipeAnalysisSummaryResponse;
+ const materialColumns = response.materialColumns ?? [];
+ const rows = buildPipeAnalysisSummaryViewRows(response, ctpv, sgg);
+ const orderedYearMaterialGroups = buildPipeAnalysisYearMaterialGroups(rows, materialColumns)
+ .map((group, index) => ({ ...group, rowNo: index + 1 }));
const exportHeadList = buildPipeAnalysisSummaryHeadList(
appliedFilters.analysisType,
materialColumns,
@@ -334,31 +358,11 @@ export default function PipeAnalysisPopupPage() {
() =>
yearMaterialGroups.map((group, index) => ({
...group,
- rowNo: index + 1,
+ rowNo: (summaryCurrentPage - 1) * PIPE_ANALYSIS_SUMMARY_PAGE_SIZE + index + 1,
})),
- [yearMaterialGroups],
+ [yearMaterialGroups, summaryCurrentPage],
);
- const summaryPageOffset =
- (summaryCurrentPage - 1) * PIPE_ANALYSIS_SUMMARY_PAGE_SIZE;
- const pagedRows = useMemo(
- () =>
- appliedFilters.analysisType === "YEAR_MATERIAL"
- ? rows
- : rows.slice(
- summaryPageOffset,
- summaryPageOffset + PIPE_ANALYSIS_SUMMARY_PAGE_SIZE,
- ),
- [appliedFilters.analysisType, rows, summaryPageOffset],
- );
- const visibleYearMaterialGroups = useMemo(
- () =>
- orderedYearMaterialGroups.slice(
- summaryPageOffset,
- summaryPageOffset + PIPE_ANALYSIS_SUMMARY_PAGE_SIZE,
- ),
- [orderedYearMaterialGroups, summaryPageOffset],
- );
const sggOptions = useMemo(
() =>
draftFilters.ctpvCd === "0"
@@ -498,19 +502,18 @@ export default function PipeAnalysisPopupPage() {
{
+ void fetchSummary(appliedFilters, page)
+ .catch(() => alert("관로분석 페이지를 불러오지 못했습니다."));
+ }}
onCellClick={handleCellClick}
onGroupedCellClick={handleYearMaterialCellClick}
actions={
diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtPrcsZoneAnalysis.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtPrcsZoneAnalysis.tsx
index a15e37b55..5887d73c5 100644
--- a/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtPrcsZoneAnalysis.tsx
+++ b/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtPrcsZoneAnalysis.tsx
@@ -34,7 +34,7 @@ import {
getSwtrmtPrcsZoneId,
} from "./swtrmtPrcsZoneAnalysisApi";
import type {
- RepairHistoryRow,
+ RepairHistoryMapFeature,
SwtrmtPrcsZoneAnalysisMetric,
SwtrmtPrcsZoneAnalysisResponse,
} from "./swtrmtPrcsZoneAnalysisApi";
@@ -140,6 +140,8 @@ const SwtrmtPrcsZoneAnalysis = ({
// 보수이력 지도 레이어는 React 렌더링 대상이 아니므로 ref로 보관해 팝업 종료·구역 전환 때 제거한다.
const repairLayerRef = useRef | null>(null);
const repairPopupRef = useRef(null);
+ // 목록이 먼저 표시되어도 지도 도형 준비 후 마지막으로 클릭한 관로로 이동한다.
+ const pendingRepairFidRef = useRef("");
const requestKey = useMemo(() => {
const rawValues = rawFeatureData?.values_ ?? rawFeatureData ?? {};
@@ -173,7 +175,7 @@ const SwtrmtPrcsZoneAnalysis = ({
/** 윈도우 팝업 조회 결과 전체를 청록색 관로 레이어로 교체한다. */
const replaceRepairLayer = useCallback(
- (rows: RepairHistoryRow[]) => {
+ (rows: RepairHistoryMapFeature[]) => {
if (!gisMap) return;
clearRepairLayer();
@@ -285,16 +287,19 @@ const SwtrmtPrcsZoneAnalysis = ({
if (
data?.type === WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS
) {
- replaceRepairLayer(
- Array.isArray(data.payload?.rows) ? data.payload.rows : [],
- );
+ const features = Array.isArray(data.payload?.rows) ? data.payload.rows : [];
+ if (features.length === 0) pendingRepairFidRef.current = "";
+ replaceRepairLayer(features);
+ if (pendingRepairFidRef.current) focusRepairFeature(pendingRepairFidRef.current);
return;
}
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SELECT_PIPE) {
- focusRepairFeature(String(data.payload?.fid ?? ""));
+ pendingRepairFidRef.current = String(data.payload?.fid ?? "");
+ focusRepairFeature(pendingRepairFidRef.current);
return;
}
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_CLEAR) {
+ pendingRepairFidRef.current = "";
clearRepairLayer();
}
};
diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/swtrmtPrcsZoneAnalysisApi.ts b/apps/sn/app/(map-layout)/map/components/popupComponent/swtrmtPrcsZoneAnalysisApi.ts
index d0c92d678..0221f1918 100644
--- a/apps/sn/app/(map-layout)/map/components/popupComponent/swtrmtPrcsZoneAnalysisApi.ts
+++ b/apps/sn/app/(map-layout)/map/components/popupComponent/swtrmtPrcsZoneAnalysisApi.ts
@@ -69,7 +69,8 @@ export type RepairHistoryResponse = {
};
const ANALYSIS_ENDPOINT_PREFIX = "v1/swagePrcsZones";
-const REPAIR_HISTORY_PAGE_SIZE = 200;
+/** 지도 전체 강조에 필요한 필드만 제공하며 표의 행 수와 독립적이다. */
+export type RepairHistoryMapFeature = Pick;
const firstValue = (...values: unknown[]) => {
const value = values.find((item) => {
@@ -158,41 +159,26 @@ const getRepairHistoryPath = (
return `${ANALYSIS_ENDPOINT_PREFIX}/${encodeURIComponent(zoneId)}/repairHistories${suffix}${query ? `?${query}` : ""}`;
};
-/** 윈도우 팝업과 부모 지도가 같은 전체 결과를 사용하도록 서버 페이지를 끝까지 합친다. */
-export const fetchAllRepairHistoriesByZoneId = async (
+/** 목록은 현재 페이지 하나만 조회한다. 전체 지도/Excel과 호출 계약을 분리한다. */
+export const fetchRepairHistoryPage = async (
zoneId: string,
filters: RepairHistoryFilters,
+ page: number,
+ size: number,
): Promise => {
if (!zoneId.trim()) throw new Error("처리구역 식별값이 없습니다.");
-
- const firstParams = new URLSearchParams();
- appendRepairHistoryFilters(firstParams, filters);
- firstParams.set("page", "0");
- firstParams.set("size", String(REPAIR_HISTORY_PAGE_SIZE));
- const first = (await getData(
- `${ANALYSIS_ENDPOINT_PREFIX}/${encodeURIComponent(zoneId)}/repairHistories?${firstParams.toString()}`,
- )) as RepairHistoryResponse;
-
- const pages: RepairHistoryResponse[] = [first];
- for (let page = 1; page < (first.totalPages ?? 0); page += 1) {
- const params = new URLSearchParams(firstParams);
- params.set("page", String(page));
- pages.push((await getData(
- `${ANALYSIS_ENDPOINT_PREFIX}/${encodeURIComponent(zoneId)}/repairHistories?${params.toString()}`,
- )) as RepairHistoryResponse);
- }
-
- const data = pages.flatMap((page) => page?.data ?? []);
- return {
- ...first,
- totalElements: data.length,
- totalPages: 1,
- page: 0,
- size: data.length,
- data,
- };
+ return getData(getRepairHistoryPath(zoneId, filters, "", {
+ page: String(page), size: String(size),
+ })) as Promise;
};
+/** 검색조건에 맞는 전체 도형을 한 번 조회하며 목록 페이지 이동 시에는 호출하지 않는다. */
+export const fetchRepairHistoryMapFeatures = (
+ zoneId: string,
+ filters: RepairHistoryFilters,
+): Promise =>
+ getData(getRepairHistoryPath(zoneId, filters, "/mapFeatures"));
+
/** 현재 조회 조건의 화면 목록 항목 파일을 내려받는다. */
export const downloadRepairHistoriesByZoneId = async (
zoneId: string,
diff --git a/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx
index 1b1427d1e..4bb4f1553 100644
--- a/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx
+++ b/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx
@@ -15,8 +15,7 @@ import { Button } from "@common/components";
import { getData } from "@/components/lib/api";
import { appendApiSearchParams } from "@/shared/util/util";
-import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
-import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
+import { downloadAnalysisExcel } from "@/shared/lib/download-analysis-excel";
import { useAlertStore } from "@/shared/lib";
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
@@ -243,29 +242,7 @@ export default function RdiiResultPopupPage() {
try {
setIsLoading(true);
- const response = await fetchAllPages>({
- fetchPage: (page, size) =>
- getData(
- `v1/rdiiAnlsRslt/ppl/${selectedRow.rainInfwtAnlsSn}?page=${page}&size=${size}`,
- ),
- getItems: (page) =>
- Array.isArray(page?.content) ? page.content : [],
- getTotal: (page) => Number(page?.totalElements ?? 0),
- });
- const excelRows = response.items;
-
- await downloadExcelWithKoreanHeaders({
- data: excelRows as unknown as JSON[],
- fileName: `RDII분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.rainInfwtAnlsSn}`,
- sheetNameArray: ["RDII 분석결과"],
- headerRange: "A1:D1",
- headerMapping: {
- rainInfwtAnlsSn: "RDII분석\n분석번호",
- pplMngNo: "관로\n관리번호",
- anlsTm: "분석시각",
- predcOilq: "예측유량\n(m³/h)",
- },
- });
+ await downloadAnalysisExcel("rdii", selectedRow.rainInfwtAnlsSn, `RDII분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.rainInfwtAnlsSn}`);
} catch (error) {
console.error("RDII 분석결과 다운로드 중 오류:", error);
alert("RDII 분석결과 다운로드 중 오류가 발생했습니다.");
diff --git a/apps/sn/app/(map-layout)/map/components/repairHistory/RepairHistoryPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/repairHistory/RepairHistoryPopupPage.tsx
index 228b1c1b8..6406a436b 100644
--- a/apps/sn/app/(map-layout)/map/components/repairHistory/RepairHistoryPopupPage.tsx
+++ b/apps/sn/app/(map-layout)/map/components/repairHistory/RepairHistoryPopupPage.tsx
@@ -4,7 +4,7 @@
*/
"use client";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Button, Input } from "@common/components";
import {
StandardPopupLayout,
@@ -23,7 +23,8 @@ import {
import { MS_SWAGE_PPL_CODES } from "../common/table";
import {
downloadRepairHistoriesByZoneId,
- fetchAllRepairHistoriesByZoneId,
+ fetchRepairHistoryPage,
+ fetchRepairHistoryMapFeatures,
type RepairHistoryFilters,
type RepairHistoryRow,
} from "../popupComponent/swtrmtPrcsZoneAnalysisApi";
@@ -175,17 +176,21 @@ export default function RepairHistoryPopupPage({
useState(buildEmptyFilters);
const [appliedFilters, setAppliedFilters] =
useState(buildEmptyFilters);
- // 전체 조회 결과는 부모 지도 강조와 팝업 내 10건 페이지 표가 함께 사용한다.
+ // 목록에는 현재 서버 페이지의 행만 저장하고 지도 전체 도형은 별도로 전달한다.
const [rows, setRows] = useState([]);
+ const [total, setTotal] = useState(0);
+ // 페이지 요청과 지도 요청을 분리해 페이지 이동이 진행 중인 전체 지도 조회를 취소하지 않는다.
+ const pageRequestId = useRef(0);
+ const mapRequestId = useRef(0);
// 용도 선택지는 별도 하드코딩하지 않고 관망도 트리 설정 API의 sortOrdr를 따른다.
const [pipeUseOptions, setPipeUseOptions] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
- /** 조회 결과 전체를 부모 지도 레이어와 팝업 표에 동시에 반영한다. */
+ /** 목록은 한 페이지만 조회하고 검색조건 변경 시에만 지도 전체 도형을 갱신한다. */
const searchRepairHistories = useCallback(
- async (nextFilters: RepairHistoryFilters) => {
+ async (nextFilters: RepairHistoryFilters, page = 1, refreshMap = true) => {
if (!zoneId) {
setErrorMessage("처리구역 식별값이 없습니다.");
return;
@@ -193,38 +198,45 @@ export default function RepairHistoryPopupPage({
setIsLoading(true);
setErrorMessage("");
+ const requestId = ++pageRequestId.current;
+ if (refreshMap) {
+ const mapId = ++mapRequestId.current;
+ postRepairMessage(WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS, { zoneId, rows: [] });
+ void fetchRepairHistoryMapFeatures(zoneId, nextFilters).then((features) => {
+ if (mapId !== mapRequestId.current) return;
+ postRepairMessage(WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS, { zoneId, rows: features });
+ }).catch(() => {
+ if (mapId === mapRequestId.current) setErrorMessage("보수이력 지도 도형을 불러오지 못했습니다. 조회를 다시 실행해주세요.");
+ });
+ }
try {
- const response = await fetchAllRepairHistoriesByZoneId(
+ const response = await fetchRepairHistoryPage(
zoneId,
nextFilters,
+ page - 1,
+ REPAIR_HISTORY_PAGE_SIZE,
);
+ if (requestId !== pageRequestId.current) return;
const nextRows = response.data ?? [];
setRows(nextRows);
+ setTotal(response.totalElements);
setAppliedFilters({ ...nextFilters });
- setCurrentPage(1);
- postRepairMessage(
- WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
- {
- zoneId,
- rows: nextRows,
- },
- );
+ setCurrentPage(page);
} catch (error) {
+ if (requestId !== pageRequestId.current) return;
setRows([]);
- postRepairMessage(
- WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
- {
- zoneId,
- rows: [],
- },
- );
+ setTotal(0);
+ if (refreshMap) {
+ mapRequestId.current += 1;
+ postRepairMessage(WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS, { zoneId, rows: [] });
+ }
setErrorMessage(
error instanceof Error
? error.message
: "보수이력을 불러오지 못했습니다.",
);
} finally {
- setIsLoading(false);
+ if (requestId === pageRequestId.current) setIsLoading(false);
}
},
[zoneId],
@@ -270,6 +282,8 @@ export default function RepairHistoryPopupPage({
};
window.addEventListener("beforeunload", clearParentLayer);
return () => {
+ pageRequestId.current += 1;
+ mapRequestId.current += 1;
window.removeEventListener("beforeunload", clearParentLayer);
clearParentLayer();
};
@@ -277,7 +291,7 @@ export default function RepairHistoryPopupPage({
/** 화면에서 적용한 조건과 동일한 9개 목록 항목의 XLSX를 내려받는다. */
const handleDownload = async () => {
- if (!zoneId || rows.length === 0) return;
+ if (!zoneId || total === 0) return;
setIsLoading(true);
setErrorMessage("");
try {
@@ -306,18 +320,10 @@ export default function RepairHistoryPopupPage({
});
};
- const visibleRows = useMemo(
- () =>
- rows.slice(
- (currentPage - 1) * REPAIR_HISTORY_PAGE_SIZE,
- currentPage * REPAIR_HISTORY_PAGE_SIZE,
- ),
- [currentPage, rows],
- );
// 공통 표에 전달할 표시값은 원본 API 행과 분리해 지도 연계 식별값을 보존한다.
const tableRows = useMemo(
() =>
- visibleRows.map((row) => ({
+ rows.map((row) => ({
fid: row.fid,
fcltMngNo: formatCellValue(row.fcltMngNo),
useName: formatCellValue(row.useNm),
@@ -329,7 +335,7 @@ export default function RepairHistoryPopupPage({
installationYear: formatYear(row.installationYear),
burialDepth: formatCellValue(row.burialDepth),
})),
- [visibleRows],
+ [rows],
);
return (
@@ -459,12 +465,12 @@ export default function RepairHistoryPopupPage({
}
headlist={REPAIR_HISTORY_HEADLIST}
title={`${zoneName || "처리구역"} 보수이력 목록`}
- total={rows.length}
+ total={total}
page={currentPage}
pageSize={REPAIR_HISTORY_PAGE_SIZE}
- onPageChange={setCurrentPage}
+ onPageChange={(page) => void searchRepairHistories(appliedFilters, page, false)}
onRowClick={(_, index) => {
- const row = visibleRows[index];
+ const row = rows[index];
if (row) handleRowClick(row);
}}
rowId={(row, index) => String(row.fid ?? index)}
diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx
index 2ad4328cf..7e063073f 100644
--- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx
+++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx
@@ -23,6 +23,7 @@ import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefini
import {
ExclnSewagePumpCol2,
FldngWtlvMapCol2,
+ FldngDpthMapCol2,
MsSwageMhlCol2,
MsSwagePplCol2,
stofcbrCol2,
@@ -103,6 +104,8 @@ const SearchModal: React.FC = ({
? SwtrmtPrcsZoneCol2
: basicType === "tb_ms_fldng_wtlv_map"
? FldngWtlvMapCol2
+ : basicType === "tb_ms_fldng_dpth_map"
+ ? FldngDpthMapCol2
: basicType === "tb_ms_swage_mhl"
? MsSwageMhlCol2
: basicType === "tb_ms_swage_pmstn"
diff --git a/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts
index 6a35bf112..eb7c07465 100644
--- a/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts
+++ b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts
@@ -12,21 +12,13 @@
* 🔗 사용 API
* - 관망도 시설물 목록 조회: GET /api/v1/fclts (sewerage-service)
*/
-import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
-import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
+import { requestGridExcel } from "@/shared/api/grid-export";
import {
MS_SWAGE_PPL_USE_CODES,
MS_SWAGE_PPL_CODES,
MS_SWAGE_PPL_SHAPE,
MS_SWAGE_MHL_CODES,
MS_SWAGE_MHL_USE_CODES,
- FldngWtlvMapExcelCol,
- MsSwagePplExcelCol,
- SwtrmtFcltExcelCol,
- SwtrmtPrcsZoneExcelCol,
- MsSwageMhlExcelCol,
- ExclnSewagePumpExcelCol,
- stofcbrExcelCol,
} from "../components/common/table";
import { getData } from "@/components/lib/api";
import { BASIC_TYPES } from "./basicTab";
@@ -51,9 +43,6 @@ const isPopupWindow = () =>
!!window.opener &&
window.location.pathname.startsWith("/popup/");
-// Excel 열에 포함되지 않는 화면용 유하방향 보강은 제외하고 서버가 일괄 병합할 수 있는 크기로 순회한다.
-const BASIC_EXCEL_BATCH_SIZE = 2_000;
-
export default function useBasicTab() {
const alert = useAlertStore((state) => state.setMessage);
const { confirm } = useConfirm();
@@ -232,284 +221,17 @@ export default function useBasicTab() {
try {
const appliedQuery = useSearchModalStore.getState().appliedBasicQuery;
- if (!appliedQuery) {
- throw new Error(
- "화면 조회조건을 확인할 수 없습니다. 다시 조회해주세요.",
- );
- }
- const appliedBasicType = appliedQuery.basicType;
- const queryParams = new URLSearchParams(appliedQuery.query);
- if (appliedBasicType === "tb_ms_swage_ppl") {
- queryParams.delete("includeFlowDirectionBasis");
- }
-
- let headerMapping: { [key: string]: string } = {};
- let headerRange: string = "A1:F1";
-
- switch (appliedBasicType) {
- case "tb_ms_swage_ppl": {
- // 하수관로
- headerMapping = MsSwagePplExcelCol;
- headerRange = "A1:Q1";
- break;
- }
- case "tb_il_swage_prcs_fclt": {
- // 하수처리시설
- headerMapping = SwtrmtFcltExcelCol;
- headerRange = "A1:J1";
- break;
- }
- case "tb_ms_swage_prcs_zone": {
- // 하수처리구역
- headerMapping = SwtrmtPrcsZoneExcelCol;
- headerRange = "A1:D1";
- break;
- }
- case "tb_ms_fldng_wtlv_map": {
- // 침수흔적도
- headerMapping = FldngWtlvMapExcelCol;
- headerRange = "A1:O1";
- break;
- }
- case "tb_ms_swage_mhl": {
- // 하수맨홀
- headerMapping = MsSwageMhlExcelCol;
- headerRange = "A1:J1";
- break;
- }
- case "tb_ms_swage_pmstn": {
- // 하수펌프장
- headerMapping = ExclnSewagePumpExcelCol;
- headerRange = "A1:J1";
- break;
- }
- case "tb_ms_stofcbr": {
- // 우수토실
- headerMapping = stofcbrExcelCol;
- headerRange = "A1:H1";
- break;
- }
- }
-
- const res = await fetchAllPages({
- batchSize: BASIC_EXCEL_BATCH_SIZE,
- fetchPage: (page, size) => {
- const pageParams = new URLSearchParams(queryParams);
- pageParams.set("page", String(page));
- pageParams.set("size", String(size));
- return getData(`v1/fclts?${pageParams.toString()}`);
- },
- getItems: (page) => (Array.isArray(page?.data) ? page.data : []),
- getTotal: (page) => Number(page?.count ?? 0),
- });
-
- const mhlUsgCdMap = new Map(
- MS_SWAGE_MHL_USE_CODES.map((code) => [code.val?.trim(), code.text]),
- );
- const pplUsgCdMap = new Map(
- MS_SWAGE_PPL_USE_CODES.map((code) => [code.val?.trim(), code.text]),
- );
- const pplMqltCdMap = new Map(
- MS_SWAGE_PPL_CODES.map((code) => [code.val?.trim(), code.text]),
- );
- const pplShpCdMap = new Map(
- MS_SWAGE_PPL_SHAPE.map((code) => [code.val?.trim(), code.text]),
- ); // 관로 시설물형태코드
- const ctpvMap = new Map(
- ctpv.map((code) => [code.val?.trim(), code.text]),
- );
- const sggMap = new Map(
- sgg.map((code) => [code.val?.trim(), code.text]),
- );
-
- let mappedContent = res.items.map((item: any) => {
- const formattedItem = formatDisplayData(item);
-
- let ctpvCd = item?.ctpvCd ?? formattedItem?.ctpvCd ?? "";
- let sggCd = item?.sggCd ?? formattedItem?.sggCd ?? "";
-
- if (!ctpvCd || !sggCd) {
- const code = String(
- item?.pbadmsEmdCd ??
- formattedItem?.pbadmsEmdCd ??
- item?.stdgCd ??
- formattedItem?.stdgCd ??
- "",
- )?.trim();
- if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
- if (!sggCd && code) sggCd = code.slice(0, 5);
- }
-
- // trim 한 번만 수행
- const trimmedUsgCd = item?.usgCd?.trim();
- // const trimmedMqltCd = item?.mqltCd?.trim();
-
- switch (appliedBasicType) {
- case "tb_ms_swage_ppl": {
- // 하수관로
- return {
- fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? "-",
- instMngNo: formattedItem.instMngNo ?? item.instMngNo ?? "-",
- ctpvNm: ctpvMap.get(ctpvCd) ?? item.ctpvNm ?? "-",
- sggNm: sggMap.get(sggCd) ?? item.sggNm ?? "-",
- swtrmt_zone_nm:
- formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? "-",
- instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? "-",
- usgCd:
- pplUsgCdMap.get(trimmedUsgCd) ||
- item.usgCd ||
- formattedItem.usgCd ||
- "-",
- pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? "-",
- prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? "-",
- tmptDpth: formattedItem.tmptDpth ?? item.tmptDpth ?? "-",
- tmptMhlMngNo:
- formattedItem.tmptMhlMngNo ?? item.tmptMhlMngNo ?? "-",
- scendDpth: formattedItem.scendDpth ?? item.scendDpth ?? "-",
- scendMhlMngNo:
- formattedItem.scendMhlMngNo ?? item.scendMhlMngNo ?? "-",
- vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? "-",
- wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? "-",
- fcltyShpCd:
- pplShpCdMap.get(item?.fcltyShpCd?.trim()) ||
- item.fcltyShpCd ||
- formattedItem.fcltyShpCd ||
- "-",
- mqltCd:
- pplMqltCdMap.get(item?.mqltCd?.trim()) ||
- item.mqltCd ||
- formattedItem.mqltCd ||
- "-",
- };
- }
- case "tb_il_swage_prcs_fclt": {
- // 하수처리시설
- return {
- swtrmtFcltNm:
- formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? "-",
- fcltCpct: formattedItem.fcltCpct ?? item.fcltCpct ?? "-",
- rgnExpln: formattedItem.rgnExpln ?? item.rgnExpln ?? "-",
- oprtngStrtYmd:
- formattedItem.oprtngStrtYmd ?? item.oprtngStrtYmd ?? "-",
- laeNm: formattedItem.laeNm ?? item.laeNm ?? "-",
- sareaNm: formattedItem.sareaNm ?? item.sareaNm ?? "-",
- rvrNm: formattedItem.rvrNm ?? item.rvrNm ?? "-",
- dschgWtstNm:
- formattedItem.dschgWtstNm ?? item.dschgWtstNm ?? "-",
- swtrmtPpltnCnt:
- formattedItem.swtrmtPpltnCnt ?? item.swtrmtPpltnCnt ?? "-",
- rprsMsrcCn: formattedItem.rprsMsrcCn ?? item.rprsMsrcCn ?? "-",
- };
- }
- case "tb_ms_swage_prcs_zone": {
- // 하수처리구역
- return {
- swtrmtZoneNm:
- formattedItem.swtrmtZoneNm ?? item.swtrmtZoneNm ?? "-",
- ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? "-",
- sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? "-",
- swtrmtFcltNm:
- formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? "-",
- };
- }
- case "tb_ms_fldng_wtlv_map": {
- // 침수흔적도
- return {
- fldngYr: formattedItem.fldngYr ?? item.fldngYr ?? "-",
- fldngDstrNm:
- formattedItem.fldngDstrNm ?? item.fldngDstrNm ?? "-",
- ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? "-",
- sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? "-",
- bgngYmd: formattedItem.bgngYmd ?? item.bgngYmd ?? "-",
- fldngBgngHm:
- formattedItem.fldngBgngHm ?? item.fldngBgngHm ?? "-",
- endYmd: formattedItem.endYmd ?? item.endYmd ?? "-",
- fldngEndHm: formattedItem.fldngEndHm ?? item.fldngEndHm ?? "-",
- fldngHr: formattedItem.fldngHr ?? item.fldngHr ?? "-",
- fldngDstrctNm:
- formattedItem.fldngDstrctNm ?? item.fldngDstrctNm ?? "-",
- fldngCsDtlCd:
- formattedItem.fldngCsDtlCd ?? item.fldngCsDtlCd ?? "-",
- avgFldngWtlv:
- formattedItem.avgFldngWtlv ?? item.avgFldngWtlv ?? "-",
- fldngArea: formattedItem.fldngArea ?? item.fldngArea ?? "-",
- exmnYr: formattedItem.exmnYr ?? item.exmnYr ?? "-",
- damAmt: formattedItem.damAmt ?? item.damAmt ?? "-",
- };
- }
- case "tb_ms_swage_mhl": {
- // 하수맨홀
- return {
- fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? "-",
- ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? "-",
- sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? "-",
- instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? "-",
- usgCd:
- mhlUsgCdMap.get(trimmedUsgCd) ||
- item.usgCd ||
- formattedItem.usgCd ||
- "-",
- swageMhlDiamVl:
- formattedItem.swageMhlDiamVl ?? item.swageMhlDiamVl ?? "-",
- wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? "-",
- vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? "-",
- altdVl: formattedItem.altdVl ?? item.altdVl ?? "-",
- dpthlVl: formattedItem.dpthlVl ?? item.dpthlVl ?? "-",
- };
- }
- case "tb_ms_swage_pmstn": {
- // 하수펌프장
- return {
- fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? "-",
- ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? "-",
- sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? "-",
- swagePmstnNm:
- formattedItem.swagePmstnNm ?? item.swagePmstnNm ?? "-",
- instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? "-",
- dailyPrcsCpct:
- formattedItem.dailyPrcsCpct ?? item.dailyPrcsCpct ?? "-",
- maxImpndCpct:
- formattedItem.maxImpndCpct ?? item.maxImpndCpct ?? "-",
- altdVl: formattedItem.altdVl ?? item.altdVl ?? "-",
- wtlvVl: formattedItem.wtlvVl ?? item.wtlvVl ?? "-",
- exclnWpmpAbltVl:
- formattedItem.exclnWpmpAbltVl ?? item.exclnWpmpAbltVl ?? "-",
- };
- }
- case "tb_ms_stofcbr": {
- // 우수토실
- return {
- fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? "-",
- mapshtNo: formattedItem.mapshtNo ?? item.mapshtNo ?? "-",
- ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? "-",
- sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? "-",
- instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? "-",
- worflwWallHgt:
- formattedItem.worflwWallHgt ?? item.worflwWallHgt ?? "-",
- swageMhlMngNo:
- formattedItem.swageMhlMngNo ?? item.swageMhlMngNo ?? "-",
- drtnAngl: formattedItem.drtnAngl ?? item.drtnAngl ?? "-",
- };
- }
- }
- });
-
- // 선택된 행 데이터를 Excel 다운로드 형식으로 변환
- const excelData = mappedContent; // 배열로 감싸서 전달
-
- // 파일명 생성 (분석번호 기반)
- const label = BASIC_TYPES.find(
- (type) => type.val === appliedBasicType,
- )?.text;
- const fileName = `${label} 조회 결과_${todayYear}${todayMonth}${todayDate}`;
-
- await downloadExcelWithKoreanHeaders({
- data: excelData as unknown as JSON[],
- fileName: fileName,
- sheetNameArray: [fileName],
- headerRange: headerRange,
- headerMapping: headerMapping,
- });
+ if (!appliedQuery) throw new Error("화면 조회조건을 확인할 수 없습니다. 다시 조회해주세요.");
+ const params = new URLSearchParams(appliedQuery.query);
+ // 현재 페이지나 목록 배열을 보내지 않고 전체 조건과 표시명만 서버 작업으로 전달한다.
+ const label = BASIC_TYPES.find((type) => type.val === appliedQuery.basicType)?.text ?? "시설";
+ await requestGridExcel("v1/facilityExports", {
+ lyrTypeNm: appliedQuery.basicType,
+ cqlFilter: params.get("cqlFiter") ?? "",
+ sort: params.get("sort"),
+ ctpvNames: Object.fromEntries(ctpv.map((item) => [String(item.val).trim(), item.text])),
+ sggNames: Object.fromEntries(sgg.map((item) => [String(item.val).trim(), item.text])),
+ }, `${label} 조회 결과_${todayYear}${todayMonth}${todayDate}.xlsx`);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
diff --git a/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx
index aefbb5917..c426473b4 100644
--- a/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx
+++ b/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx
@@ -19,8 +19,7 @@
*/
"use client";
import { deleteData, getData } from "@/components/lib/api";
-import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
-import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
+import { downloadAnalysisExcel } from "@/shared/lib/download-analysis-excel";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
@@ -240,37 +239,7 @@ const Result = () => {
}
try {
- const data = await fetchAllPages>({
- fetchPage: (page, size) =>
- getData(
- `v1/rdiiAnlsRslt/ppl/${selectedRowData.rainInfwtAnlsSn}?page=${page}&size=${size}`,
- ),
- getItems: (page) =>
- Array.isArray(page?.content) ? page.content : [],
- getTotal: (page) => Number(page?.totalElements ?? 0),
- });
-
- // 선택된 분석의 전체 관로 결과를 Excel 다운로드 형식으로 변환한다.
- const excelData = data.items;
-
- // 헤더 매핑 정의
- const headerMapping: { [key: string]: string } = {
- rainInfwtAnlsSn: "RDII분석\n분석번호",
- pplMngNo: "관로\n관리번호",
- anlsTm: "분석시각",
- predcOilq: "예측유량\n(m³/h)",
- };
-
- // 파일명 생성 (분석번호 기반)
- const fileName = `RDII분석결과_${selectedRowData.swtrmtFcltNm}_${selectedRowData.aplcnYm}_${selectedRowData.rainInfwtAnlsSn}`;
-
- await downloadExcelWithKoreanHeaders({
- data: excelData as unknown as JSON[],
- fileName: fileName,
- sheetNameArray: ["RDII 분석결과"],
- headerRange: "A1:D1",
- headerMapping: headerMapping,
- });
+ await downloadAnalysisExcel("rdii", selectedRowData.rainInfwtAnlsSn, `RDII분석결과_${selectedRowData.swtrmtFcltNm}_${selectedRowData.aplcnYm}_${selectedRowData.rainInfwtAnlsSn}`);
setIsLoadingResult(false);
} catch (error) {
setIsLoadingResult(false);
diff --git a/apps/sn/app/(with-base-layout)/ii/result/page.tsx b/apps/sn/app/(with-base-layout)/ii/result/page.tsx
index 78f151630..084795639 100644
--- a/apps/sn/app/(with-base-layout)/ii/result/page.tsx
+++ b/apps/sn/app/(with-base-layout)/ii/result/page.tsx
@@ -19,8 +19,7 @@
*/
"use client";
import { deleteData, getData } from "@/components/lib/api";
-import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
-import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
+import { downloadAnalysisExcel } from "@/shared/lib/download-analysis-excel";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
@@ -240,37 +239,7 @@ const Result = () => {
}
try {
- const data = await fetchAllPages>({
- fetchPage: (page, size) =>
- getData(
- `v1/inftwtAnlsRslt/ppl/${selectedRowData.inftwtAnlsSn}?page=${page}&size=${size}`,
- ),
- getItems: (page) =>
- Array.isArray(page?.content) ? page.content : [],
- getTotal: (page) => Number(page?.totalElements ?? 0),
- });
-
- // 선택된 분석의 전체 관로 결과를 Excel 다운로드 형식으로 변환한다.
- const excelData = data.items;
-
- // 헤더 매핑 정의
- const headerMapping: { [key: string]: string } = {
- inftwtAnlsSn: "침입수분석\n분석번호",
- pplMngNo: "관로\n관리번호",
- anlsTm: "분석시각",
- predcOilq: "예측유량\n(m³/h)",
- };
-
- // 파일명 생성 (분석번호 기반)
- const fileName = `침입수분석결과_${selectedRowData.swtrmtFcltNm}_${selectedRowData.aplcnYm}_${selectedRowData.inftwtAnlsSn}`;
-
- await downloadExcelWithKoreanHeaders({
- data: excelData as unknown as JSON[],
- fileName: fileName,
- sheetNameArray: ["침입수 분석결과"],
- headerRange: "A1:D1",
- headerMapping: headerMapping,
- });
+ await downloadAnalysisExcel("inftwt", selectedRowData.inftwtAnlsSn, `침입수분석결과_${selectedRowData.swtrmtFcltNm}_${selectedRowData.aplcnYm}_${selectedRowData.inftwtAnlsSn}`);
setIsLoadingResult(false);
} catch (error) {
setIsLoadingResult(false);
diff --git a/apps/sn/app/(with-base-layout)/oper/dredging/page.tsx b/apps/sn/app/(with-base-layout)/oper/dredging/page.tsx
index 3565bacbf..d16914bb4 100644
--- a/apps/sn/app/(with-base-layout)/oper/dredging/page.tsx
+++ b/apps/sn/app/(with-base-layout)/oper/dredging/page.tsx
@@ -40,7 +40,8 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
deleteDredgingRecord,
- fetchAllDredgingRecords,
+ fetchDredgingGridPage,
+ downloadDredgingGridExcel,
isDredgingBackendEnabled,
} from "./lib/client";
import { createDredgingClientUuid } from "./lib/uuid";
@@ -343,6 +344,9 @@ const Page = () => {
const [records, setRecords] = useState(
isDredgingBackendEnabled ? [] : initialRecords,
);
+ // 서버 페이지와 전체 건수는 별도 보관해 목록 10건이 전체 결과로 오인되지 않게 한다.
+ const [serverRows, setServerRows] = useState([]);
+ const [serverTotal, setServerTotal] = useState(0);
const [isLoading, setIsLoading] = useState(isDredgingBackendEnabled);
const [loadError, setLoadError] = useState(null);
const [selectedRecordId, setSelectedRecordId] = useState(null);
@@ -376,16 +380,26 @@ const Page = () => {
setIsLoading(true);
setLoadError(null);
try {
- const response = await fetchAllDredgingRecords({
+ const response = await fetchDredgingGridPage({
+ viewType,
fromDate: appliedFromDate,
toDate: appliedToDate,
ctpv: appliedCtpvName,
sgg: appliedSggName,
+ page: currentPage - 1,
+ size: PAGE_SIZE,
+ sort: sort ? `${sort.key},${sort.direction}` : undefined,
});
- if (!cancelled) setRecords(response.items ?? []);
+ if (!cancelled) {
+ setServerRows(response.content);
+ setServerTotal(response.totalElements);
+ setRecords(viewType === "MUNICIPALITY" ? response.content as unknown as DredgingRecord[] : []);
+ }
} catch {
if (!cancelled) {
setRecords([]);
+ setServerRows([]);
+ setServerTotal(0);
setLoadError(DREDGING_LOAD_ERROR_MESSAGE);
}
} finally {
@@ -398,7 +412,7 @@ const Page = () => {
};
// 조회 버튼과 등록 완료 이벤트에서 searchVersion을 증가시켜 명시적으로 재조회한다.
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [searchVersion]);
+ }, [searchVersion, currentPage, viewType, sort, appliedFromDate, appliedToDate, appliedCtpvName, appliedSggName]);
const filteredRecords = useMemo(
() =>
@@ -420,6 +434,14 @@ const Page = () => {
);
const tableRows = useMemo(() => {
+ if (isDredgingBackendEnabled) {
+ return serverRows.map((row) => viewType === "MUNICIPALITY" ? {
+ ...row,
+ pipeMaterial: resolvePipeMaterialName(String(row.pipeMaterial ?? "")),
+ installationYear: row.installationYear ?? "확인불가",
+ elapsedYears: row.elapsedYears ?? "확인불가",
+ } : row);
+ }
if (viewType === "DIAMETER") return buildDiameterRows(filteredRecords);
if (viewType === "ELAPSED_YEAR")
return buildElapsedYearRows(filteredRecords);
@@ -436,17 +458,17 @@ const Page = () => {
: currentYear - record.installationYear,
recordIds: [record.id],
}));
- }, [filteredRecords, viewType]);
+ }, [filteredRecords, viewType, serverRows]);
- // 준설 API의 모든 페이지를 수집한 뒤 표시명·집계값 기준으로 정렬한다.
+ // 운영 모드는 서버가 정렬·페이징하며 로컬 시연 모드만 메모리 처리를 유지한다.
const sortedTableRows = useMemo(
- () => sortStandardTableRows(tableRows, sort),
+ () => isDredgingBackendEnabled ? tableRows : sortStandardTableRows(tableRows, sort),
[sort, tableRows],
);
const pageRows = useMemo(
() =>
- sortedTableRows.slice(
+ isDredgingBackendEnabled ? sortedTableRows : sortedTableRows.slice(
(currentPage - 1) * PAGE_SIZE,
currentPage * PAGE_SIZE,
),
@@ -455,11 +477,6 @@ const Page = () => {
const selectedRecord =
records.find((record) => record.id === selectedRecordId) ?? null;
- const selectedJobRecords = selectedRecord
- ? records.filter(
- (record) => record.dredgingJobSn === selectedRecord.dredgingJobSn,
- )
- : [];
const canManageSelectedRecord =
viewType === "MUNICIPALITY" && selectedRecord !== null && !isMutating;
@@ -477,6 +494,7 @@ const Page = () => {
headlist.map((head) => ({
id: head.id,
accessorFn: (row) => row[head.id],
+ // 표시명 컬럼까지 서버 전체 정렬을 사용한다.
header: () => (
{
align: head.align ?? "center",
},
})),
- [headlist, sort],
+ [headlist, sort, viewType],
);
const pagination: PaginationState = {
pageIndex: currentPage - 1,
@@ -513,7 +531,7 @@ const Page = () => {
pagination,
rowSelection: selectedTableRowId ? { [selectedTableRowId]: true } : {},
},
- rowCount: sortedTableRows.length,
+ rowCount: isDredgingBackendEnabled ? serverTotal : sortedTableRows.length,
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
getRowId: (row) => row.recordIds.join("|"),
@@ -559,6 +577,18 @@ const Page = () => {
const handleDownload = async () => {
try {
+ if (isDredgingBackendEnabled) {
+ setIsLoading(true);
+ await downloadDredgingGridExcel({
+ viewType,
+ fromDate: appliedFromDate,
+ toDate: appliedToDate,
+ ctpv: appliedCtpvName,
+ sgg: appliedSggName,
+ sort: sort ? `${sort.key},${sort.direction}` : undefined,
+ });
+ return;
+ }
const visibleHeads = headlist.filter((head) => head.id !== "recordIds");
const excelRows = [
visibleHeads.map((head) => head.title),
@@ -581,6 +611,8 @@ const Page = () => {
} catch (error) {
console.error("준설현황 엑셀 다운로드 실패", error);
alert("엑셀 파일을 생성하지 못했습니다.");
+ } finally {
+ setIsLoading(false);
}
};
@@ -645,7 +677,7 @@ const Page = () => {
const handleDeleteRecord = async () => {
if (!canManageSelectedRecord || !selectedRecord) return;
const confirmed = await confirm(
- `선택한 준설작업 전체를 삭제하시겠습니까?\n준설일자: ${selectedRecord.dredgingDate}\n연결 관로: ${selectedJobRecords.length}건 · 등록사진: ${selectedRecord.photoCount}건`,
+ `선택한 준설작업 전체를 삭제하시겠습니까?\n준설일자: ${selectedRecord.dredgingDate}\n현재 페이지에 보이지 않는 연결 관로와 사진도 모두 삭제됩니다.`,
);
if (!confirmed) return;
@@ -877,7 +909,7 @@ const Page = () => {
}}
actions={
<>
-