/** * 화면명: 침수발생이력 목록 * 침수발생이력을 검색하고 엑셀 다운로드 및 지도 위치 확인 기능을 제공한다. */ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Button, Input } from "@common/components"; import { StandardPopupLayout } from "@common/business_components"; import { getRawJson } from "@/shared/api/sewerage-api"; import { requestGridExcel } from "@/shared/api/grid-export"; import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types"; import { useAlertStore } from "@/shared/lib"; import useStore from "@/entities/reference-data/model/reference-data-store"; import LoadingOverlay from "@/components/ui/loading-bar"; import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table"; import PopupFooterNote from "@/features/window-popup/ui/popup-footer-note"; import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel"; import { FLDNG_HSTRY_LAYER_TYPE, FLDNG_HSTRY_DEFAULT_FILTERS, FLDNG_HSTRY_LIST_HEADLIST, FLDNG_HSTRY_PAGE_SIZE, FLDNG_HSTRY_ROW_SELECT_MESSAGE, type FldngHstryApiItem, type FldngHstryFilters, type FldngHstryTableRow } from "@/entities/flood-history/model/flood-history-types"; import { attachFldngHstryGeometry, buildFldngHstryListQueryString, buildFldngHstryYearOptions, normalizeFldngHstryFilters, toFldngHstryTableRows } from "@/entities/flood-history/lib/flood-history-utils"; const unwrapPayload = (response: any): T => (response?.data ?? response) as T; const resolveOptionText = ( code: string, options: Array<{ text: string; val: string | number }>, prefixLength?: number, ) => { const normalized = String(code ?? "").trim(); if (!normalized || normalized === "0") return ""; const lookup = prefixLength ? normalized.slice(0, prefixLength) : normalized; return options.find((item) => String(item.val).trim() === lookup)?.text ?? ""; }; const resolveFldngHstryMapFeatureFid = (row: FldngHstryTableRow) => { const fid = String(row.fid ?? "").trim(); if (fid.startsWith(`${FLDNG_HSTRY_LAYER_TYPE}.`)) return fid; if (fid) return `${FLDNG_HSTRY_LAYER_TYPE}.${fid}`; return ""; }; const resolveRegionNames = ( row: FldngHstryTableRow, ctpvOptions: Array<{ text: string; val: string | number }>, sggOptions: Array<{ text: string; val: string | number }>, ): FldngHstryTableRow => ({ ...row, ctpvNm: resolveOptionText(row.ctpvCd ?? "", ctpvOptions, 2) || row.ctpvNm || String((row as any).ctpv_nm ?? row.ctpvCd ?? "").trim(), sggNm: resolveOptionText(row.sggCd ?? "", sggOptions, 5) || row.sggNm || String((row as any).sgg_nm ?? row.sggCd ?? "").trim(), }); export default function FldngHstryPopupPage() { const alert = useAlertStore((state) => state.setMessage); const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); const [filters, setFilters] = useState( FLDNG_HSTRY_DEFAULT_FILTERS, ); const [appliedFilters, setAppliedFilters] = useState( FLDNG_HSTRY_DEFAULT_FILTERS, ); const [rows, setRows] = useState([]); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (ctpv.length === 0) void fetchCtpv(); if (sgg.length === 0) void fetchSgg(); }, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]); useEffect(() => { if (rows.length === 0) return; if (ctpv.length === 0 && sgg.length === 0) return; setRows((current) => current.map((row) => resolveRegionNames(row, ctpv as any, sgg as any)), ); }, [ctpv, rows.length, sgg]); const fldngYearOptions = useMemo(() => buildFldngHstryYearOptions(), []); const sggOptions = useMemo( () => filters.ctpvCd === "0" ? [] : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)), ), [filters.ctpvCd, sgg], ); const fetchRows = useCallback( async (page: number, nextFilters: FldngHstryFilters, size: number) => { const normalized = normalizeFldngHstryFilters(nextFilters); const queryString = buildFldngHstryListQueryString( normalized, page, size, ); const response = await getRawJson( `/sewerage-service/api/v1/fldng/hstry?${queryString}`, ); const payload = unwrapPayload<{ items?: FldngHstryApiItem[]; total?: number; }>(response); const items = Array.isArray(payload?.items) ? payload.items : []; const displayItems = items.map((item) => { const hydrated = attachFldngHstryGeometry(item); return { ...hydrated, ctpvNm: resolveOptionText(hydrated.ctpvCd ?? "", ctpv as any, 2) || String( (hydrated as any).ctpvNm ?? (hydrated as any).ctpv_nm ?? "", ).trim() || String(hydrated.ctpvCd ?? "").trim(), sggNm: resolveOptionText(hydrated.sggCd ?? "", sgg as any, 5) || String( (hydrated as any).sggNm ?? (hydrated as any).sgg_nm ?? "", ).trim() || String(hydrated.sggCd ?? "").trim(), }; }); return { rows: toFldngHstryTableRows(displayItems, page, size), total: Number(payload?.total ?? 0), }; }, [ctpv, sgg], ); const loadRows = useCallback( async (page: number, nextFilters: FldngHstryFilters) => { setIsLoading(true); try { const normalized = normalizeFldngHstryFilters(nextFilters); const response = await fetchRows( page, normalized, FLDNG_HSTRY_PAGE_SIZE, ); setRows(response.rows); setTotal(response.total); setCurrentPage(page); setAppliedFilters(normalized); } catch (error) { console.error("침수발생이력 목록 조회 중 오류:", error); setRows([]); setTotal(0); alert( error instanceof Error ? error.message : "침수발생이력 목록 조회 중 오류가 발생했습니다.", ); } finally { setIsLoading(false); } }, [alert, fetchRows], ); useEffect(() => { void loadRows(1, FLDNG_HSTRY_DEFAULT_FILTERS); // 최초 로딩은 한 번만 수행한다. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleSearch = async (page = 1) => { const normalized = normalizeFldngHstryFilters(filters); if (normalized.sggCd !== "0" && normalized.ctpvCd === "0") { alert("시도를 먼저 선택하세요."); return; } setFilters(normalized); await loadRows(page, normalized); }; const handlePageChange = async (page: number) => { await loadRows(page, appliedFilters); }; const handleRowClick = (row: FldngHstryTableRow) => { const mapFeatureFid = resolveFldngHstryMapFeatureFid(row); if (mapFeatureFid) { window.opener?.postMessage( { type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE, payload: { layerNm: FLDNG_HSTRY_LAYER_TYPE, fid: mapFeatureFid, }, }, window.location.origin, ); return; } window.opener?.postMessage( { type: FLDNG_HSTRY_ROW_SELECT_MESSAGE, row, openDetail: false, }, window.location.origin, ); }; const handleDownload = async () => { if (total === 0) { alert("다운로드할 침수발생이력 데이터가 없습니다."); return; } setIsLoading(true); try { 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( error instanceof Error ? error.message : "침수발생이력 다운로드 중 오류가 발생했습니다.", ); } finally { setIsLoading(false); } }; return (
>} headlist={FLDNG_HSTRY_LIST_HEADLIST} title="침수발생이력 목록" total={total} page={currentPage} pageSize={FLDNG_HSTRY_PAGE_SIZE} onPageChange={(page) => void handlePageChange(page)} onRowClick={handleRowClick} rowId={(row, index) => String(row.fid ?? index)} scrollMode="horizontal" minTableWidth="140rem" actions={ } /> {/* 기존 행 클릭 안내에 인수테스트 요구(자료 원천 표시) 출처 문구를 글머리표로 더한다. */}
); }