/** * 화면명: 침수위험지도 목록 * 침수위험지도를 검색하고 목록·엑셀·지도 보기 기능을 제공한다. */ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@common/components"; import { StandardPopupLayout } from "@common/business_components"; import { getRawJson } from "@/shared/api/sewerage-api"; 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 { 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 { SBMRG_DNGR_MAP_DEFAULT_FILTERS, SBMRG_DNGR_MAP_FREQUENCY_OPTIONS, SBMRG_DNGR_MAP_HEADLIST, SBMRG_DNGR_MAP_LAYER_TYPE, SBMRG_DNGR_MAP_PAGE_SIZE, SBMRG_DNGR_MAP_SCOPE_SIZE, buildSbmrgDngrMapListQueryString, extractAdministrativeCodes, toSbmrgDngrMapTableRows, type SbmrgDngrMapApiItem, type SbmrgDngrMapFilters, type SbmrgDngrMapTableRow, } from "./sbmrg-dngr-map-types"; const unwrapPayload = (response: any): T => (response?.data ?? response) as T; /** 시설물 레이어의 중점관리지역 침수위험지도 피처를 조건별 목록으로 제공한다. */ export default function SbmrgDngrMapPopupPage() { const alert = useAlertStore((state) => state.setMessage); const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); // 입력 중인 조건과 실제 조회에 적용된 조건을 분리해 페이지 이동 시 조건이 흔들리지 않게 한다. const [filters, setFilters] = useState( SBMRG_DNGR_MAP_DEFAULT_FILTERS, ); const [appliedFilters, setAppliedFilters] = useState( SBMRG_DNGR_MAP_DEFAULT_FILTERS, ); const [items, setItems] = useState([]); const [administrativeScopeItems, setAdministrativeScopeItems] = useState< SbmrgDngrMapApiItem[] | null >(null); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); const availableCtpvCodes = useMemo( () => new Set( (administrativeScopeItems ?? []).flatMap((item) => extractAdministrativeCodes(item.ctpvCd, 2), ), ), [administrativeScopeItems], ); const availableSggCodes = useMemo( () => new Set( (administrativeScopeItems ?? []).flatMap((item) => extractAdministrativeCodes(item.sggCd, 5), ), ), [administrativeScopeItems], ); const ctpvOptions = useMemo( () => administrativeScopeItems === null ? ctpv : ctpv.filter((item) => availableCtpvCodes.has(String(item.val).trim().slice(0, 2)), ), [administrativeScopeItems, availableCtpvCodes, ctpv], ); const sggOptions = useMemo(() => { const scopedOptions = administrativeScopeItems === null ? sgg : sgg.filter((item) => availableSggCodes.has(String(item.val).trim().slice(0, 5)), ); return filters.ctpvCd === "0" ? [] : scopedOptions.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)), ); }, [administrativeScopeItems, availableSggCodes, filters.ctpvCd, sgg]); const rows = useMemo( () => toSbmrgDngrMapTableRows( items, currentPage - 1, SBMRG_DNGR_MAP_PAGE_SIZE, ctpv, sgg, total, ), [ctpv, currentPage, items, sgg, total], ); useEffect(() => { if (ctpv.length === 0) void fetchCtpv(); if (sgg.length === 0) void fetchSgg(); }, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]); /** 마지막 적용 조건으로 서버 페이지를 조회한다. */ const loadRows = useCallback( async (page: number, nextFilters: SbmrgDngrMapFilters) => { setIsLoading(true); try { const queryString = buildSbmrgDngrMapListQueryString( nextFilters, page - 1, SBMRG_DNGR_MAP_PAGE_SIZE, ); const response = await getRawJson( `/sewerage-service/api/v1/fldng/sbmrgDngrMap?${queryString}`, ); const payload = unwrapPayload<{ items?: SbmrgDngrMapApiItem[]; total?: number; }>(response); const items = Array.isArray(payload?.items) ? payload.items : []; setItems(items); setTotal(Number(payload?.total ?? 0)); setCurrentPage(page); setAppliedFilters(nextFilters); } catch (error) { console.error("침수위험지도 목록 조회 중 오류:", error); setItems([]); setTotal(0); alert( error instanceof Error ? error.message : "침수위험지도 목록 조회 중 오류가 발생했습니다.", ); } finally { setIsLoading(false); } }, [alert], ); /** 전체 50개 그룹에서 실제 존재하는 행정구역 코드만 검색 선택지로 제한한다. */ const loadAdministrativeScope = useCallback(async () => { try { const queryString = buildSbmrgDngrMapListQueryString( SBMRG_DNGR_MAP_DEFAULT_FILTERS, 0, SBMRG_DNGR_MAP_SCOPE_SIZE, ); const response = await getRawJson( `/sewerage-service/api/v1/fldng/sbmrgDngrMap?${queryString}`, ); const payload = unwrapPayload<{ items?: SbmrgDngrMapApiItem[] }>( response, ); setAdministrativeScopeItems( Array.isArray(payload?.items) ? payload.items : [], ); } catch (error) { console.error("침수위험지도 행정구역 범위 조회 중 오류:", error); setAdministrativeScopeItems(null); } }, []); useEffect(() => { void loadRows(1, SBMRG_DNGR_MAP_DEFAULT_FILTERS); void loadAdministrativeScope(); }, [loadAdministrativeScope, loadRows]); /** 현재 입력 조건을 적용하고 첫 페이지부터 다시 조회한다. */ const handleSearch = () => { void loadRows(1, filters); }; /** 선택한 지역·빈도 그룹의 전체 fid를 부모 지도에 전달해 전체 범위 이동·강조를 실행한다. */ const handleRowClick = (row: SbmrgDngrMapTableRow) => { if (!Array.isArray(row.fids) || row.fids.length === 0) return; const featureIds = row.fids.map((fid) => String(fid).startsWith(`${SBMRG_DNGR_MAP_LAYER_TYPE}.`) ? String(fid) : `${SBMRG_DNGR_MAP_LAYER_TYPE}.${fid}`, ); window.opener?.postMessage( { type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE, payload: { layerNm: SBMRG_DNGR_MAP_LAYER_TYPE, fids: JSON.stringify(featureIds), }, }, window.location.origin, ); }; return (
>} headlist={SBMRG_DNGR_MAP_HEADLIST} title="침수위험지도 목록" total={total} page={currentPage} pageSize={SBMRG_DNGR_MAP_PAGE_SIZE} onPageChange={(page) => void loadRows(page, appliedFilters)} onRowClick={handleRowClick} rowId={(row, index) => [ row.emphsMngRgnMngNo ?? "unknown", row.floodWtlvFrqVl ?? "unknown", row.ctpvCd ?? "unknown", row.sggCd ?? "unknown", index, ].join(":") } /> {/* 인수테스트 요구: 팝업 자료의 원천을 목록 아래에 표시한다. */}
); }