diff --git a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx index 19657d6f0..a8db43e10 100644 --- a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx @@ -14,8 +14,10 @@ import { formatDateInputToYmd, formatYmdToDateInput, normalizeSinkholeFilters, - SINKHOLE_EXCEL_HEADERS, + SINKHOLE_COORD_STATUS_OPTIONS, + SINKHOLE_DOWNLOAD_EXCEL_HEADERS, SINKHOLE_FILTER_SYNC_MESSAGE, + SINKHOLE_MIN_SEARCH_YMD, SINKHOLE_RESTORE_STATUS_OPTIONS, SINKHOLE_ROW_SELECT_MESSAGE, type SinkholeFilters, @@ -23,6 +25,7 @@ import { } from "./sinkholeTypes"; const PAGE_SIZE = 20; +const DOWNLOAD_BATCH_SIZE = 2000; type SinkholeTableRow = SinkholeListItem & { rowNo: number; @@ -60,10 +63,10 @@ const toCoordStatusDisplay = (value: unknown) => { return normalized || "-"; }; -const toTableRows = (items: SinkholeListItem[], page: number) => +const toTableRows = (items: SinkholeListItem[], page: number, pageSize = PAGE_SIZE) => items.map((item, index) => ({ ...item, - rowNo: page * PAGE_SIZE + index + 1, + rowNo: page * pageSize + index + 1, sizeWidthDisplay: toDisplay(item.sizeWidth), sizeLengthDisplay: toDisplay(item.sizeLength), sizeDepthDisplay: toDisplay(item.sizeDepth), @@ -82,7 +85,6 @@ const buildDownloadRows = (rows: SinkholeTableRow[]) => sizeDepth: row.sizeDepthDisplay, detailCause: row.detailCauseDisplay, restoreStatus: row.restoreStatus || "-", - coordStatus: row.coordStatusDisplay, restoreCompletedYmd: row.restoreCompletedYmdDisplay, })); @@ -100,6 +102,25 @@ export default function SinkholePopupPage() { ctpvCd: searchParams.get("ctpvCd") ?? "0", sggCd: searchParams.get("sggCd") ?? "0", restoreStatus: searchParams.get("restoreStatus") ?? "0", + coordStatus: searchParams.get("coordStatus") ?? "0", + }, + undefined, + ); + + if (!seeded.startYmd || !seeded.endYmd) { + return normalizeSinkholeFilters(buildSinkholeDefaultFilters(), undefined); + } + return seeded; + }); + const [appliedFilters, setAppliedFilters] = useState(() => { + const seeded = normalizeSinkholeFilters( + { + startYmd: searchParams.get("startYmd") ?? "", + endYmd: searchParams.get("endYmd") ?? "", + ctpvCd: searchParams.get("ctpvCd") ?? "0", + sggCd: searchParams.get("sggCd") ?? "0", + restoreStatus: searchParams.get("restoreStatus") ?? "0", + coordStatus: searchParams.get("coordStatus") ?? "0", }, undefined, ); @@ -128,13 +149,28 @@ export default function SinkholePopupPage() { }, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]); const fetchRows = useCallback( - async (page: number, nextFilters: SinkholeFilters) => { - const queryString = buildSinkholeListQueryString(nextFilters, ctpv as SelectDataType[], sgg as SelectDataType[], page - 1, PAGE_SIZE); + async ( + page: number, + nextFilters: SinkholeFilters, + pageSize = PAGE_SIZE, + options?: { + includeCoordStatus?: boolean; + }, + ) => { + const queryString = buildSinkholeListQueryString( + nextFilters, + ctpv as SelectDataType[], + sgg as SelectDataType[], + page - 1, + pageSize, + options, + ); const response = await getRawJson(`/sewerage-service/api/v1/sinkholes?${queryString}`); const payload = unwrapPayload<{ items?: SinkholeListItem[]; total?: number }>(response); const items = Array.isArray(payload?.items) ? payload.items : []; return { - rows: toTableRows(items, page - 1), + rows: toTableRows(items, page - 1, pageSize), + items, total: Number(payload?.total ?? 0), }; }, @@ -150,6 +186,7 @@ export default function SinkholePopupPage() { setRows(response.rows); setTotal(response.total); setCurrentPage(page); + setAppliedFilters(normalized); window.opener?.postMessage( { type: SINKHOLE_FILTER_SYNC_MESSAGE, @@ -197,18 +234,43 @@ export default function SinkholePopupPage() { }; const handleDownload = async () => { - if (rows.length === 0) { + if (total === 0) { alert("다운로드할 데이터가 없습니다."); return; } - await downloadExcelWithKoreanHeaders({ - data: buildDownloadRows(rows) as unknown as JSON[], - fileName: `지반침하정보_${new Date().toISOString().slice(0, 10)}.xlsx`, - sheetNameArray: ["지반침하정보"], - headerRange: "A1:J2", - headerMapping: SINKHOLE_EXCEL_HEADERS, - }); + setIsLoading(true); + try { + const normalized = normalizeSinkholeFilters(appliedFilters); + const downloadItems: SinkholeListItem[] = []; + const totalPages = Math.max(1, Math.ceil(total / DOWNLOAD_BATCH_SIZE)); + + for (let page = 1; page <= totalPages; page += 1) { + const response = await fetchRows(page, normalized, DOWNLOAD_BATCH_SIZE, { + includeCoordStatus: false, + }); + downloadItems.push(...response.items); + } + + if (downloadItems.length === 0) { + alert("다운로드할 데이터가 없습니다."); + return; + } + + const downloadRows = toTableRows(downloadItems, 0, 1); + await downloadExcelWithKoreanHeaders({ + data: buildDownloadRows(downloadRows) as unknown as JSON[], + fileName: `지반침하정보_${new Date().toISOString().slice(0, 10)}.xlsx`, + sheetNameArray: ["지반침하정보"], + headerRange: "A1:I1", + headerMapping: SINKHOLE_DOWNLOAD_EXCEL_HEADERS, + }); + } catch (error) { + console.error("지반침하 다운로드 중 오류:", error); + alert(error instanceof Error ? error.message : "지반침하 다운로드 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; return ( @@ -220,12 +282,13 @@ export default function SinkholePopupPage() {
-
+
+ +