diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx index abeec2245..ba07c98db 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx @@ -14,7 +14,6 @@ import useStore from "@/store/useStore"; import LoadingOverlay from "@/components/ui/LoadingBar"; import { AGED_PIPE_DEFAULT_FILTERS, - AGED_PIPE_LAYER_TYPE, AGED_PIPE_LIST_HEADLIST, AGED_PIPE_PAGE_SIZE, buildAgedPipeCqlFilters, @@ -27,6 +26,7 @@ import { type AgedPipeManagementAgency, } from "./agedPipeUtils"; import { downloadAgedPipeExcel } from "./agedPipeExport"; +import { fetchAgedPipeLayer } from "./agedPipeLayer"; import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable"; import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel"; @@ -100,12 +100,14 @@ export default function AgedPipePopupPage() { const fetchRows = useCallback( async (page: number, nextFilters: AgedPipeFilters, size: number) => { const normalized = normalizeAgedPipeFilters(nextFilters); + // 독립 팝업에서도 현재 DB의 원천 레이어와 필터 템플릿을 사용한다. + const layer = await fetchAgedPipeLayer(); const queryParams = new URLSearchParams({ - lyrTypeNm: AGED_PIPE_LAYER_TYPE, + lyrTypeNm: layer.lyrTyNm, page: String(page - 1), size: String(size), }); - const cqlFilters = buildAgedPipeCqlFilters(normalized); + const cqlFilters = buildAgedPipeCqlFilters(normalized, layer.flterSql); if (cqlFilters.length > 0) { queryParams.append("cqlFiter", cqlFilters.join(" and ")); } @@ -139,13 +141,14 @@ export default function AgedPipePopupPage() { setAppliedFilters(normalizeAgedPipeFilters(nextFilters)); } catch (error) { console.error("노후관로 목록 조회 중 오류:", error); + alert(error instanceof Error ? error.message : "노후관로 목록 조회 중 오류가 발생했습니다."); setRows([]); setTotal(0); } finally { setIsLoading(false); } }, - [fetchRows, filters], + [alert, fetchRows, filters], ); useEffect(() => { diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeLayer.ts b/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeLayer.ts new file mode 100644 index 000000000..04b228ccd --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeLayer.ts @@ -0,0 +1,48 @@ +/** 노후관로의 지도와 목록이 사용하는 DB 레이어 설정 계약이다. */ +import { getRawJson } from "@/components/lib/api"; +import { buildAgedPipeCqlFilters, isAgedPipeLayerNode, type AgedPipeFilters } from "./agedPipeUtils"; + +/** DB 응답을 그대로 보존하며 최초 표출 전에 필터만 해석한다. */ +export const prepareAgedPipeLayerRows = (rows: any[], filters: AgedPipeFilters): any[] => + rows.map((row) => { + const next = { ...row }; + if (isAgedPipeLayerNode(row)) { + next.agedPipeFilterTemplate = row.flterSql; + try { + validateAgedPipeLayer(row); + next.flterSql = buildAgedPipeCqlFilters(filters, row.flterSql).join(" AND "); + } catch (error) { + // 노후관로 DB 미전환이 다른 시설물 레이어까지 중단시키지 않도록 이 행만 차단한다. + next.agedPipeConfigError = error instanceof Error ? error.message : "노후관로 레이어 설정 오류"; + next.actlLyrYn = "N"; + next.flterSql = "EXCLUDE"; + } + } + if (Array.isArray(row.children)) next.children = prepareAgedPipeLayerRows(row.children, filters); + return next; + }); + +/** 환경마다 달라질 수 있는 일련번호 대신 DB 업무명을 통해 설정 행을 찾는다. */ +export const findAgedPipeLayer = (rows: any[]): any | undefined => { + for (const row of rows) { + if (isAgedPipeLayerNode(row)) return row; + const child = Array.isArray(row.children) ? findAgedPipeLayer(row.children) : undefined; + if (child) return child; + } +}; + +/** 예전 제어용 행을 실제 WFS 레이어처럼 요청하지 않도록 전환 상태를 확인한다. */ +export const validateAgedPipeLayer = (row: any) => { + if (row?.agedPipeConfigError) throw new Error(row.agedPipeConfigError); + if (!row || row.actlLyrYn !== "Y" || row.srvcNm !== "wfs" || !row.lyrTyNm || row.lyrTyNm === "tb_old_pipe") { + throw new Error("노후관로 DB 레이어 설정을 실제 WFS 레이어와 sql_filt 템플릿으로 갱신해 주세요."); + } +}; + +/** 독립 목록 팝업도 부모창의 하드코딩 없이 동일 설정 API를 조회한다. */ +export const fetchAgedPipeLayer = async () => { + const rows = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK"); + const row = findAgedPipeLayer(Array.isArray(rows) ? rows : []); + validateAgedPipeLayer(row); + return row; +}; diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts b/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts index 8fb0f963a..f35e868ea 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts @@ -229,13 +229,18 @@ export const getElapsedYearsDisplayText = (value: unknown) => { return elapsedYears == null ? "확인불가" : `${elapsedYears}년`; }; -export const buildAgedPipeCqlFilters = (filters: AgedPipeFilters) => { +/** DB의 SQL 필터 템플릿을 해석한다. 누락/미지원 토큰은 전체 관로 조회로 대체하지 않는다. */ +export const buildAgedPipeCqlFilters = (filters: AgedPipeFilters, filterTemplate: string) => { const normalized = normalizeAgedPipeFilters(filters); - const cqlFilters = [ - "instl_ymd IS NOT NULL", - ...AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES.map((prefix) => `instl_ymd NOT LIKE '${prefix}%'`), - `instl_ymd <= '${getCutoffYmd(normalized.elapsedYears)}'`, - ]; + const template = String(filterTemplate ?? "").trim(); + if (!template.includes("${cutoffYmd}")) { + throw new Error("노후관로 DB sql_filt에 ${cutoffYmd} 조건을 등록해 주세요."); + } + const filter = template.split("${cutoffYmd}").join(getCutoffYmd(normalized.elapsedYears)); + if (/\$\{[^}]*\}/.test(filter)) { + throw new Error("노후관로 DB sql_filt에 지원하지 않는 치환값이 있습니다."); + } + const cqlFilters = [`(${filter})`]; if (normalized.sggCd !== "0") { cqlFilters.push(`pbadms_emd_cd LIKE '${normalized.sggCd}%'`); 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 fcc656ad7..bd67bf2ad 100644 --- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx @@ -397,8 +397,8 @@ export default function PipeAnalysisPopupPage() { })) } > - - {ctpv.map((item) => ( + {/* 시도는 전체 조회 없이 개별 지역만 선택한다. */} + {ctpv.filter((item) => String(item.val).trim() !== "0").map((item) => ( diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipeAnalysisUtils.ts b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipeAnalysisUtils.ts index 6e8327d1e..d26e13ecc 100644 --- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipeAnalysisUtils.ts +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipeAnalysisUtils.ts @@ -144,7 +144,8 @@ export const PIPE_ANALYSIS_TYPE_OPTIONS: Array<{ export const PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS: PipeAnalysisSummaryFilters = { analysisType: "YEAR", - ctpvCd: "0", + // 관로분석 최초 조회·초기화의 기본 시도는 부산광역시다. 계정 지역 제한은 별도 적용한다. + ctpvCd: "26", sggCd: "0", swtrmtZoneNm: "0", scopeRegionCode: "", diff --git a/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx index 126c6140f..f6c0120ae 100644 --- a/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx @@ -150,7 +150,7 @@ const formatAnalysisBaseAt = (value?: string | null) => { return `${date.replaceAll("-", ".")} ${time.slice(0, 5)}`.trim(); }; -/** 개발 DB의 과거 시험자료와 운영 최신자료를 함께 찾도록 최근 1년을 기본 조회기간으로 사용한다. */ +/** 현재 위험 조회와 검색 초기화에 사용하는 최근 1년의 기본 조회조건이다. */ const buildDefaultResultFilters = (): ResultFilters => { const endDate = new Date(); const startDate = new Date(endDate); @@ -191,7 +191,12 @@ const buildResultRowKey = (row: WaterRiskResultRow) => /** 실제 통수위험 API를 조회하고 선택 결과를 부모 지도에 전달하는 팝업이다. */ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) { - const initialFilters = useMemo(() => buildDefaultResultFilters(), []); + // 팝업 최초 진입에만 지정 기간을 사용하고 초기화는 기본 조회조건을 새로 만든다. + const initialFilters = useMemo(() => ({ + ...buildDefaultResultFilters(), + startDate: "2026-08-15", + endDate: "2026-08-17", + }), []); const [resultMode, setResultMode] = useState("current"); const [draftResultFilters, setDraftResultFilters] = useState(initialFilters); @@ -234,9 +239,13 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) { let active = true; setIsLoading(true); setErrorMessage(""); + // 과거 이력의 고정 기본기간이 현재 진행 중인 강우 조회를 제한하지 않게 분리한다. + const dateFilters = resultMode === "current" + ? buildDefaultResultFilters() + : appliedResultFilters; fetchWaterRiskResults({ - startDate: appliedResultFilters.startDate, - endDate: appliedResultFilters.endDate, + startDate: dateFilters.startDate, + endDate: dateFilters.endDate, analysisBaseAt: resultMode === "current" ? currentAnalysisBaseAt : undefined, ctpvCd: @@ -659,8 +668,10 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) { type="button" color="white" onClick={() => { - setDraftResultFilters(initialFilters); - setAppliedResultFilters(initialFilters); + // 초기화 시점의 기본기간·전체 조건으로 첫 페이지를 다시 조회한다. + const defaultFilters = buildDefaultResultFilters(); + setDraftResultFilters(defaultFilters); + setAppliedResultFilters(defaultFilters); setResultCurrentPage(1); setSelectedResultKey(""); }} diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 981120398..2eb372f24 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -110,7 +110,6 @@ import { import { AGED_PIPE_DEFAULT_FILTERS, AGED_PIPE_GUIDE_MIN_ZOOM, - AGED_PIPE_MIN_ZOOM, AGED_PIPE_LAYER_TYPE, AGED_PIPE_PAGE_SIZE, type AgedPipeFilters, @@ -121,6 +120,7 @@ import { toDisplayPipeRow, validateElapsedYears, } from "./components/agedPipe/agedPipeUtils"; +import { findAgedPipeLayer, prepareAgedPipeLayerRows, validateAgedPipeLayer } from "./components/agedPipe/agedPipeLayer"; import { downloadAgedPipeExcel } from "./components/agedPipe/agedPipeExport"; import { useInftwtResultMap } from "./components/inftwtResult/useInftwtResultMap"; import { useRdiiResultMap } from "./components/rdiiResult/useRdiiResultMap"; @@ -194,6 +194,7 @@ import { isSbmrgDngrMapRootNode, } from "./components/sbmrgDngrMap/sbmrgDngrMapTypes"; import { + FLOOD_DNGR_MAP_LAYER_NAME, FLOOD_DNGR_MAP_LAYER_TYPE, FLOOD_DNGR_MAP_POPUP_KEY, isFloodDngrMapRootNode, @@ -217,10 +218,6 @@ const AnalysisPopupMenu = dynamic( { ssr: false }, ); -const AGED_PIPE_CONTROL_NODE_ID = "SN_AGED_PIPE"; -const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT"; -// 노후관로 지도 강조선과 시설물 트리의 선형 범례가 동일하게 사용하는 빨간색이다. -const AGED_PIPE_HIGHLIGHT_COLOR = "rgba(235, 40, 40, 0.82)"; const FLDNG_HSTRY_WFS_LAYER_ID = "SN_FLDNG_HSTRY_WFS"; // 심선은 동일 레이어 유형에 6개 깊이 등급 설정이 있어 prefix 뒤에 lyrSn을 붙여 각각 생성한다. const FLDNG_DEPTH_WFS_LAYER_ID_PREFIX = "SN_FLDNG_DEPTH_WFS"; @@ -978,7 +975,7 @@ const extractCheckedPipeUsageStyles = ( checkedNodesData.forEach((node) => { const data = node?.data ?? node; - if (!data || isFlowDirectionLayerInfo(data)) return; + if (!data || isFlowDirectionLayerInfo(data) || isAgedPipeLayerNode(data)) return; if (getLayerTypeName(data) !== FLOW_DIRECTION_PIPE_LAYER_TYPE) return; if (!isLayerVisibleAtZoom(data, zoom)) return; @@ -999,61 +996,7 @@ const extractCheckedPipeUsageStyles = ( return usageStyleMap; }; -const appendAgedPipeControlNode = (data: any[]) => { - if (!Array.isArray(data)) return data; - const isAgedPipeControlNode = (item: any) => - String(item.lyrSn ?? item.id) === AGED_PIPE_CONTROL_NODE_ID || - item.lyrNm === "노후관로"; - if (data.some(isAgedPipeControlNode)) { - return data.map((item) => - isAgedPipeControlNode(item) - ? { - ...item, - gobjTy: "line", - iconNm: null, - lnColor: AGED_PIPE_HIGHLIGHT_COLOR, - } - : item, - ); - } - - return [ - ...data, - { - lyrSn: AGED_PIPE_CONTROL_NODE_ID, - id: AGED_PIPE_CONTROL_NODE_ID, - lyrNm: "노후관로", - upperLyrSn: null, - upId: null, - sortOrdr: "4", - drawOrdr: null, - flterSql: null, - gobjTy: "line", - lyrTyNm: "tb_old_pipe", - useYn: "Y", - minViewSize: null, - iconNm: null, - iconSize: null, - lblAtrbNm: null, - fontNm: null, - fontIntnColor: null, - fontExtnColor: null, - fontExtnWdth: null, - lnWdth: null, - lnColor: AGED_PIPE_HIGHLIGHT_COLOR, - polgnColor: null, - initEprssYn: "Y", - clickPsblYn: "N", - exprsYn: "Y", - cmncNm: "all", - srvcNm: "wfs", - actlLyrYn: "N", - clckZoom: null, - children: null, - }, - ]; -}; const normalizeTreeRowSpacing = (data: any[]) => { if (!Array.isArray(data)) return data; @@ -1176,11 +1119,24 @@ const Map = () => { const sinkholeScope = useMemo(() => buildSinkholeScope(userInfo), [userInfo]); const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 - // 통수위험 결과 행을 선택할 때 기존 체크를 유지하면서 강우 격자 레이어를 켜기 위한 요청이다. + // 지도 연계 팝업의 행을 선택할 때 기존 체크를 유지하면서 필요한 업무 레이어를 켜기 위한 요청이다. const [checkedLayerRequest, setCheckedLayerRequest] = useState<{ requestKey: number; layerNames: string[]; } | null>(null); + // 침수예측현황에서 선택한 행은 도시침수지도 레이어가 켜진 뒤 지도 이동에 사용한다. + const [pendingFloodPredictionSelection, setPendingFloodPredictionSelection] = + useState<{ + extent: [number, number, number, number]; + sggCd: string; + frequency: string; + analysisDateTime: string; + } | null>(null); + // 팝업 Window 객체는 렌더링 대상이 아니므로 자동 레이어 선택 실패 안내용 ref로 보관한다. + const pendingFloodPredictionPopupRef = useRef<{ + source: Window | null; + origin: string; + } | null>(null); const [layerLoadError, setLayerLoadError] = useState(null); const gisMaps = useRef(null); const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 @@ -1856,6 +1812,74 @@ const Map = () => { [removeFloodPredictionGridLayer], ); + /** 활성화된 도시침수지도에서 선택 행의 시군구·빈도 피처를 선택하고 지도와 오버레이를 이동한다. */ + const applyFloodPredictionSelection = useCallback( + async (selection: { + extent: [number, number, number, number]; + sggCd: string; + frequency: string; + analysisDateTime: string; + }) => { + const map = gisMaps.current; + const select = GisApp.Module.select as any; + if (!map?.getView || !select?.selectFocusedBoundaryFeature) return false; + + const floodLayers = map + .getLayers() + .getArray() + .filter( + (layer: any) => + String(layer.get?.("lyrTyNm") ?? "").trim() === + FLOOD_DNGR_MAP_LAYER_TYPE && layer.getVisible?.() !== false, + ); + if (floodLayers.length === 0) return false; + + await select.resetSelectedFeature?.(); + const selectedFeatureIds = new Set(); + const selectLoadedGroupFeatures = (source: any) => { + const features = source?.getFeatures?.() ?? []; + features.forEach((feature: any) => { + const featureSggCd = String(feature.get?.("sgg_cd") ?? "").trim(); + const featureFrequency = String( + feature.get?.("sbmrg_frq_expln") ?? "", + ).trim(); + if ( + featureSggCd !== selection.sggCd || + featureFrequency !== selection.frequency + ) + return; + const featureId = String( + feature.getId?.() ?? feature.get?.("fid") ?? "", + ); + if (featureId && selectedFeatureIds.has(featureId)) return; + if (featureId) selectedFeatureIds.add(featureId); + select.selectFocusedBoundaryFeature(feature, true); + }); + }; + + floodLayers.forEach((layer: any) => { + const source = layer.getSource?.(); + if (!source?.getFeatures) return; + selectLoadedGroupFeatures(source); + source.once?.("featuresloadend", () => + selectLoadedGroupFeatures(source), + ); + }); + map.getView().fit(selection.extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + }); + void loadFloodPredictionGridLayer( + selection.sggCd, + selection.analysisDateTime, + selection.extent, + ); + return true; + }, + [loadFloodPredictionGridLayer], + ); + useEffect(() => { fetchData(); }, []); @@ -2008,58 +2032,23 @@ const Map = () => { ) { return; } - - await select.resetSelectedFeature?.(); - const selectedFeatureIds = new Set(); - let floodLayerVisible = false; - const selectLoadedGroupFeatures = (source: any) => { - const features = source?.getFeatures?.() ?? []; - features.forEach((feature: any) => { - const featureSggCd = String(feature.get?.("sgg_cd") ?? "").trim(); - const featureFrequency = String( - feature.get?.("sbmrg_frq_expln") ?? "", - ).trim(); - if (featureSggCd !== sggCd || featureFrequency !== frequency) - return; - const featureId = String( - feature.getId?.() ?? feature.get?.("fid") ?? "", - ); - if (featureId && selectedFeatureIds.has(featureId)) return; - if (featureId) selectedFeatureIds.add(featureId); - select.selectFocusedBoundaryFeature(feature, true); - }); + const selection = { + extent: extent as [number, number, number, number], + sggCd, + frequency, + analysisDateTime, }; + if (await applyFloodPredictionSelection(selection)) return; - map - .getLayers() - .getArray() - .forEach((layer: any) => { - if ( - String(layer.get?.("lyrTyNm") ?? "").trim() !== - FLOOD_DNGR_MAP_LAYER_TYPE - ) - return; - if (layer.getVisible?.() !== false) floodLayerVisible = true; - const source = layer.getSource?.(); - if (!source?.getFeatures) return; - selectLoadedGroupFeatures(source); - source.once?.("featuresloadend", () => - selectLoadedGroupFeatures(source), - ); - }); - if (!floodLayerVisible) { - (event.source as Window | null)?.postMessage?.( - { type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_FLOOD_LAYER_REQUIRED }, - event.origin, - ); - return; - } - map.getView().fit(extent, { - duration: 300, - padding: [50, 50, 50, 50], - maxZoom: 16, - }); - void loadFloodPredictionGridLayer(sggCd, analysisDateTime, extent); + pendingFloodPredictionPopupRef.current = { + source: event.source as Window | null, + origin: event.origin, + }; + setPendingFloodPredictionSelection(selection); + setCheckedLayerRequest((current) => ({ + requestKey: (current?.requestKey ?? 0) + 1, + layerNames: [FLOOD_DNGR_MAP_LAYER_NAME], + })); return; } @@ -2098,7 +2087,50 @@ const Map = () => { window.clearInterval(timer); window.removeEventListener("message", handlePopupMessage); }; - }, [loadFloodPredictionGridLayer]); + }, [applyFloodPredictionSelection]); + + useEffect(() => { + if (!pendingFloodPredictionSelection) return; + + let retryTimer: number | null = null; + let retryCount = 0; + const maxRetryCount = 20; + + /** 자동 체크 요청으로 도시침수지도 레이어가 생성될 때까지 선택 행 적용을 제한적으로 재시도한다. */ + const applyPendingSelection = async () => { + if ( + await applyFloodPredictionSelection(pendingFloodPredictionSelection) + ) { + setPendingFloodPredictionSelection((current) => + current === pendingFloodPredictionSelection ? null : current, + ); + pendingFloodPredictionPopupRef.current = null; + return; + } + + retryCount += 1; + if (retryCount >= maxRetryCount) { + const popup = pendingFloodPredictionPopupRef.current; + popup?.source?.postMessage( + { type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_FLOOD_LAYER_REQUIRED }, + popup.origin, + ); + pendingFloodPredictionPopupRef.current = null; + setPendingFloodPredictionSelection((current) => + current === pendingFloodPredictionSelection ? null : current, + ); + return; + } + retryTimer = window.setTimeout(() => { + void applyPendingSelection(); + }, 200); + }; + + void applyPendingSelection(); + return () => { + if (retryTimer !== null) window.clearTimeout(retryTimer); + }; + }, [applyFloodPredictionSelection, pendingFloodPredictionSelection]); useEffect(() => { if (shouldShowGisEditSession) return; @@ -2220,14 +2252,16 @@ const Map = () => { const data = await getRawJson( "/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", ); + // DB 필터 치환 실패는 노후관로에만 한정하고 다른 시설물 레이어는 유지한다. + const preparedData = prepareAgedPipeLayerRows(data, appliedAgedPipeFilters); + const agedPipeConfigError = findAgedPipeLayer(preparedData)?.agedPipeConfigError; + if (agedPipeConfigError) alert(agedPipeConfigError); setTreeData( appendExmnResultControlNodes( - appendAgedPipeControlNode( - appendSinkholeControlNode( - normalizeTreeRowSpacing( - normalizeRainObservationLayerInteraction( - normalizeFlowDirectionLayerInfos(data), - ), + appendSinkholeControlNode( + normalizeTreeRowSpacing( + normalizeRainObservationLayerInteraction( + normalizeFlowDirectionLayerInfos(preparedData), ), ), ), @@ -3114,14 +3148,16 @@ const Map = () => { const fetchAgedPipeRows = useCallback( async (page: number, filters: AgedPipeFilters, size: number) => { const normalized = normalizeAgedPipeFilters(filters); + const layer = findAgedPipeLayer(treeData); + validateAgedPipeLayer(layer); const queryParams = new URLSearchParams({ - lyrTypeNm: AGED_PIPE_LAYER_TYPE, + lyrTypeNm: layer.lyrTyNm, page: String(page - 1), size: String(size), sort: "string", }); - const cqlFilters = buildAgedPipeCqlFilters(normalized); + const cqlFilters = buildAgedPipeCqlFilters(normalized, layer.agedPipeFilterTemplate ?? layer.flterSql); if (cqlFilters.length > 0) { queryParams.append("cqlFiter", cqlFilters.join(" and ")); } @@ -3136,7 +3172,7 @@ const Map = () => { total: Number(response?.count ?? 0), }; }, - [ctpv, sgg], + [ctpv, sgg, treeData], ); const loadAgedPipeList = useCallback( @@ -5086,54 +5122,39 @@ const Map = () => { restoreActiveLayerSettingScroll, ]); + // DB 레이어 ID를 유지하고 조건 변경 시 해당 레이어만 갱신한다. useEffect(() => { if (!mapReady || !gisMaps.current) return; - + const row = findAgedPipeLayer(treeData); + if (!row) return; const core = GisApp.Module.core as any; - core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID); - - if (!isAgedPipeChecked || !isAgedPipeSettingEnabled) return; - - const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find( - (item: any) => - item?.lyrTyNm === AGED_PIPE_LAYER_TYPE && item?.actlLyrYn === "Y", - ) as any; - - if (!baseLayerCode) return; - - const layerCode = { - ...baseLayerCode, - id: AGED_PIPE_HIGHLIGHT_LAYER_ID, - lyrSn: AGED_PIPE_HIGHLIGHT_LAYER_ID, - lyrNm: "노후관로", - lyrTyNm: AGED_PIPE_LAYER_TYPE, - gobjTy: "line", - actlLyrYn: "Y", - exprsYn: "Y", - clickPsblYn: "Y", - cmncNm: "bbox", - srvcNm: "wfs", - flterSql: buildAgedPipeCqlFilters(appliedAgedPipeFilters).join(" and "), - lnWdth: "4", - lnColor: AGED_PIPE_HIGHLIGHT_COLOR, - drawOrdr: "9999", - minViewSize: String(AGED_PIPE_MIN_ZOOM), - }; - - const layer = core?.addWfsLayer?.(layerCode); - layer?.set?.("id", AGED_PIPE_HIGHLIGHT_LAYER_ID); - layer?.set?.("lyrNm", "노후관로"); - layer?.set?.("lyrTyNm", AGED_PIPE_LAYER_TYPE); - - return () => { - core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID); - }; - }, [ - appliedAgedPipeFilters, - isAgedPipeChecked, - isAgedPipeSettingEnabled, - mapReady, - ]); + const id = String(row.lyrSn); + const layerCode = (GisApp.layerCode as any)?.[id]; + if (!layerCode) return; + try { + validateAgedPipeLayer(row); + const filter = buildAgedPipeCqlFilters( + appliedAgedPipeFilters, row.agedPipeFilterTemplate ?? row.flterSql, + ).join(" AND "); + layerCode.flterSql = filter; + const visible = isAgedPipeChecked && isAgedPipeSettingEnabled; + let layer = core?.getLayerById?.(id); + if (!visible) { + layer?.setVisible?.(false); + return; + } + // 숨긴 동안 바뀐 조건도 다시 켤 때 반영하도록 실제 레이어의 생성 당시 필터와 비교한다. + if (!layer || layer.get?.("flterSql") !== filter) { + layer = core?.addWfsLayer?.(layerCode); + core?.setSelectInteraction?.(); + } + layer?.setVisible?.(true); + } catch (error) { + core?.getLayerById?.(id)?.setVisible?.(false); + console.error("노후관로 DB 레이어 설정 오류:", error); + } + }, [appliedAgedPipeFilters, isAgedPipeChecked, isAgedPipeSettingEnabled, + mapReady, treeData, checkedLayerLegendVersion]); useEffect(() => { if (!mapReady || !gisMaps.current) return; diff --git a/apps/sn/docs/작업검증/SFR-011-002-관로분석/001-qa-SFR-011-002-관로분석-통합검증결과.md b/apps/sn/docs/작업검증/SFR-011-002-관로분석/001-qa-SFR-011-002-관로분석-통합검증결과.md index d70d513a3..11575355d 100644 --- a/apps/sn/docs/작업검증/SFR-011-002-관로분석/001-qa-SFR-011-002-관로분석-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-011-002-관로분석/001-qa-SFR-011-002-관로분석-통합검증결과.md @@ -83,3 +83,13 @@ - SN 전체 TypeScript는 기존 `map/page.tsx:3659`의 `postMessage` 타입 오류 1건만 남았고 이번 공통 보강 코드의 추가 오류는 없다. - ESLint는 설치 상태에서 `eslint-plugin-react-hooks`를 찾지 못해 실행 환경 오류로 완료하지 못했다. - 판정: PASS. + +## 2026-09-30 관로분석 시도 기본값 부산 적용 + +- quick_vibe: 사용자 지시로 시도 전체 옵션을 제거하고 기본 시도를 부산광역시(26)로 설정했다. +- `PipeAnalysisPopupPage.tsx`: 시도 전체 option 및 공통 목록의 값 0을 제외한다. 시군구·처리구역 전체 선택은 유지한다. +- `pipeAnalysisUtils.ts`: 집계 기본 필터 ctpvCd=26. 최초 자동 조회와 초기화에서 같은 기본값을 사용한다. 계정 지역 제한은 기존 applyScopeToSummaryFilters를 우선한다. +- API/백엔드/DB 및 상세 필터 기본값은 변경하지 않는다. +- 원본: 요구사항정의서 기능 추적표 88행 SFR-011-002, 화면설계서 10번 SN_MN010103. 부산 기본값은 이번 사용자 지시를 적용한다. +- 검증: git diff --check 통과. ESLint는 eslint-plugin-react-hooks 설치 누락으로 실행 실패. 실제 브라우저 확인과 전체 빌드는 미실행이며 사용자 개발 서버를 유지했다. +- 롤백: 집계 기본 시도를 0으로 되돌리고 시도 전체 option을 복원한다. diff --git a/apps/sn/docs/작업검증/SFR-011-007-통수위험분석/002-qa-SFR-011-007-통수위험분석-통합검증결과.md b/apps/sn/docs/작업검증/SFR-011-007-통수위험분석/002-qa-SFR-011-007-통수위험분석-통합검증결과.md index 1d99320f7..67fd8bfba 100644 --- a/apps/sn/docs/작업검증/SFR-011-007-통수위험분석/002-qa-SFR-011-007-통수위험분석-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-011-007-통수위험분석/002-qa-SFR-011-007-통수위험분석-통합검증결과.md @@ -221,3 +221,12 @@ - 데이터 조회, 행 선택, 위험 판정, API 요청·응답 코드는 변경하지 않았다. - 대상 `WaterRiskPopupPage.tsx` ESLint와 프론트·공통 문서 저장소의 `git diff --check`가 통과했다. - 실행 중인 `sn` 개발 서버가 없어 로그인 화면의 실제 줄바꿈·배치는 확인하지 않았다. + +## 2026-09-30 과거분석이력 기본 강우발생기간 + +- 사용자 지시로 과거분석이력 최초 진입 기간만 2026-08-15 ~ 2026-08-17로 지정했다. 후속 사용자 지시에 따라 초기화는 실행 시점의 최근 1년·전체 검색조건을 새로 생성해 첫 페이지를 조회한다. +- 수정 파일: `frontend/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx`. +- 현재 탭의 조회기간은 기존 최근 1년 기준으로 별도 생성해 과거 고정기간에 의해 현재 데이터가 제외되지 않도록 했다. +- 과거 날짜 직접 변경·조회와 최신순 정렬을 유지한다. 백엔드·API 형식·DB 변경 없음. +- 검증: git diff --check 통과 및 최초 initialFilters와 초기화 buildDefaultResultFilters 분리 및 초기화 후 재조회 흐름 확인. 브라우저 확인 미실행, ESLint는 앞선 작업에서 확인한 eslint-plugin-react-hooks 누락으로 재실행하지 않았다. 개발 서버와 빌드는 조작하지 않았다. +- 롤백: 이 파일의 기본기간 및 조회기간 분리 변경만 복원한다. diff --git a/apps/sn/docs/작업검증/SFR-011-노후관로관리/001-qa-SFR-011-노후관로관리-통합검증결과.md b/apps/sn/docs/작업검증/SFR-011-노후관로관리/001-qa-SFR-011-노후관로관리-통합검증결과.md index 07a93843e..0e7b939d9 100644 --- a/apps/sn/docs/작업검증/SFR-011-노후관로관리/001-qa-SFR-011-노후관로관리-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-011-노후관로관리/001-qa-SFR-011-노후관로관리-통합검증결과.md @@ -75,3 +75,11 @@ | 서버 노드 호환 | 정적 통과 | 기존 노드와 프론트 가상 노드를 동일 기준으로 정규화 | | 변경 공백 검사 | 통과 | 프론트 저장소 `git diff --check` | | 브라우저 육안 확인 | 대기 | 지도 새로고침 후 체크박스 옆 빨간 선형 범례 확인 필요 | + +## 2026-10-01 DB 레이어 설정 이관 검증 +- TypeScript 검사 통과: node node_modules/typescript/bin/tsc --noEmit --incremental false --pretty false. 의존성 읽기 권한으로 상승 실행했다. +- 메모리 내 자동 검증 9건 통과: DB ID/스타일/원본 템플릿 보존, 가상 행 미생성, 경과연수 변경, 시군구 우선, 시도 조건, 빈/미지원 템플릿 거부, 기존 제어 행 오류 격리, 중첩 행, SQL 템플릿 일치. +- git diff --check 통과. 테스트 파일이나 원시 결과 파일은 생성하지 않았다. +- 실행 중 3002 브라우저에서 DB 미전환 안내와 기존 시설물 트리/노후관로 항목 표시를 확인했다. DB 응답이 실제 WFS 설정 요건을 충족하지 않아 정상 관로 표출·목록·Excel 실데이터 회귀는 미완료이다. +- 사용자 서버 3002/8883은 재시작하거나 종료하지 않았다. 전체 빌드는 실행하지 않았다. +- 후속 검증: 기존 139번에 UPDATE 적용 후 새로고침, 체크/해제, 30년→40년, 설정 숨김 중 변경 후 재표시, 지도·목록·Excel 기준 일치 확인. diff --git a/apps/sn/docs/작업내용/SFR-011-002-관로분석/001-fe-SFR-011-002-관로분석-통합구현기록.md b/apps/sn/docs/작업내용/SFR-011-002-관로분석/001-fe-SFR-011-002-관로분석-통합구현기록.md index 504b89518..229c41871 100644 --- a/apps/sn/docs/작업내용/SFR-011-002-관로분석/001-fe-SFR-011-002-관로분석-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-011-002-관로분석/001-fe-SFR-011-002-관로분석-통합구현기록.md @@ -85,3 +85,13 @@ - 동일 관로의 상세 효과가 개발 모드에서 반복 실행돼도 시설관리번호·관리기관코드별 요청 Promise를 재사용해 중복 API 호출을 막는다. 실패한 요청은 캐시에서 제거해 재시도를 허용한다. - 대상 관로 `2015303665`의 화면 표출값은 `시종점 관저고`이며, 계산코드는 `DEPTH`이다. - 수정 파일은 `app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx`이며, `map/page.tsx`는 관로 선택 행을 그대로 상세 컴포넌트에 전달한다. + +## 2026-09-30 관로분석 시도 기본값 부산 적용 + +- quick_vibe: 사용자 지시로 시도 전체 옵션을 제거하고 기본 시도를 부산광역시(26)로 설정했다. +- `PipeAnalysisPopupPage.tsx`: 시도 전체 option 및 공통 목록의 값 0을 제외한다. 시군구·처리구역 전체 선택은 유지한다. +- `pipeAnalysisUtils.ts`: 집계 기본 필터 ctpvCd=26. 최초 자동 조회와 초기화에서 같은 기본값을 사용한다. 계정 지역 제한은 기존 applyScopeToSummaryFilters를 우선한다. +- API/백엔드/DB 및 상세 필터 기본값은 변경하지 않는다. +- 원본: 요구사항정의서 기능 추적표 88행 SFR-011-002, 화면설계서 10번 SN_MN010103. 부산 기본값은 이번 사용자 지시를 적용한다. +- 검증: git diff --check 통과. ESLint는 eslint-plugin-react-hooks 설치 누락으로 실행 실패. 실제 브라우저 확인과 전체 빌드는 미실행이며 사용자 개발 서버를 유지했다. +- 롤백: 집계 기본 시도를 0으로 되돌리고 시도 전체 option을 복원한다. diff --git a/apps/sn/docs/작업내용/SFR-011-007-통수위험분석/001-fe-SFR-011-007-통수위험분석-통합구현기록.md b/apps/sn/docs/작업내용/SFR-011-007-통수위험분석/001-fe-SFR-011-007-통수위험분석-통합구현기록.md index ce9365f91..839b425e3 100644 --- a/apps/sn/docs/작업내용/SFR-011-007-통수위험분석/001-fe-SFR-011-007-통수위험분석-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-011-007-통수위험분석/001-fe-SFR-011-007-통수위험분석-통합구현기록.md @@ -266,3 +266,12 @@ - 현재·과거분석이력이 공유하는 결과표 우측 안내문 뒤에 `(GIS 매핑 관로 중 Capacity 0.8 이상이 1개라도 있으면 위험, 없으면 정상)`을 추가했다. - 지도 요약카드와 동일하게 `위험`은 빨간색(`#ff1c15`), `정상`은 파란색(`#1677ff`)의 굵은 글씨로 표시한다. - 지도 요약카드에만 있던 위험 판정기준을 결과 목록에서도 확인할 수 있게 했으며, 판정 로직·API·DB 계약은 변경하지 않았다. + +## 2026-09-30 과거분석이력 기본 강우발생기간 + +- 사용자 지시로 과거분석이력 최초 진입 기간만 2026-08-15 ~ 2026-08-17로 지정했다. 후속 사용자 지시에 따라 초기화는 실행 시점의 최근 1년·전체 검색조건을 새로 생성해 첫 페이지를 조회한다. +- 수정 파일: `frontend/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx`. +- 현재 탭의 조회기간은 기존 최근 1년 기준으로 별도 생성해 과거 고정기간에 의해 현재 데이터가 제외되지 않도록 했다. +- 과거 날짜 직접 변경·조회와 최신순 정렬을 유지한다. 백엔드·API 형식·DB 변경 없음. +- 검증: git diff --check 통과 및 최초 initialFilters와 초기화 buildDefaultResultFilters 분리 및 초기화 후 재조회 흐름 확인. 브라우저 확인 미실행, ESLint는 앞선 작업에서 확인한 eslint-plugin-react-hooks 누락으로 재실행하지 않았다. 개발 서버와 빌드는 조작하지 않았다. +- 롤백: 이 파일의 기본기간 및 조회기간 분리 변경만 복원한다. diff --git a/apps/sn/docs/작업내용/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측현황-분석시각초기화-구현기록.md b/apps/sn/docs/작업내용/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측현황-분석시각초기화-구현기록.md new file mode 100644 index 000000000..80e0a5d6b --- /dev/null +++ b/apps/sn/docs/작업내용/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측현황-분석시각초기화-구현기록.md @@ -0,0 +1,39 @@ +# 침수예측현황 분석시각 초기화 구현기록 + +## 작업 목적 + +- 침수예측현황 팝업을 처음 열거나 검색조건을 초기화할 때 분석시각 범위를 자동 입력하지 않는다. +- 분석시각 제한 없이 조회한 결과는 최근 분석 데이터부터 표시한다. + +## 원본 및 변경 근거 + +- 요구사항정의서 `SFR-011-008 위험도 산정` +- 화면설계서 슬라이드 24, `SN_MN010205 침수예측현황` +- 사용자 승인사항: 분석시각 기본 검색조건 제거 및 최근 데이터 우선 표시 + +## 변경 내용 + +- 최초·초기화 검색조건의 `analysisFrom`, `analysisTo`를 빈 값으로 변경했다. +- 목록과 Excel 전체 조회에 `analysisDateTime,desc` 정렬을 명시했다. +- 사용자가 필요할 때 직접 분석시각 범위를 입력할 수 있도록 검색 입력란은 유지했다. + +## 영향 범위 + +- `apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx` +- 백엔드 API와 응답 구조는 변경하지 않는다. +- 시도, 시군구, 강우시각, 초과 빈도 검색조건은 기존 동작을 유지한다. + +## 검증 + +- `pnpm --filter sn run typecheck` 통과 +- `git diff --check` 통과 +- 실행 중인 개발 서버를 유지하기 위해 별도 빌드는 수행하지 않는다. + +## 롤백 + +- `buildInitialFilters`에서 분석시각의 금일 기본 범위를 다시 생성하고 명시적 최신순 정렬 파라미터를 제거한다. + +## 남은 확인 + +- 팝업 최초 진입 시 분석시각 입력란이 비어 있는지 확인한다. +- 초기화 버튼을 누르면 분석시각이 비워지고 첫 페이지에 최신 분석 데이터가 표시되는지 확인한다. diff --git a/apps/sn/docs/작업내용/SFR-011-008-침수예측도시침수지도자동선택/001-fe-SFR-011-008-침수예측도시침수지도자동선택-구현기록.md b/apps/sn/docs/작업내용/SFR-011-008-침수예측도시침수지도자동선택/001-fe-SFR-011-008-침수예측도시침수지도자동선택-구현기록.md new file mode 100644 index 000000000..7c5e1187d --- /dev/null +++ b/apps/sn/docs/작업내용/SFR-011-008-침수예측도시침수지도자동선택/001-fe-SFR-011-008-침수예측도시침수지도자동선택-구현기록.md @@ -0,0 +1,42 @@ +# 침수예측현황 도시침수지도 자동 선택 구현기록 + +## 작업 목적 + +- 침수예측현황 팝업에서 행을 선택하면 사용자가 레이어 트리에서 직접 조작하지 않아도 도시침수지도 레이어를 자동으로 체크한다. +- 레이어 활성화 후 선택한 시군구와 빈도에 맞는 위치 이동, 피처 선택, 침수예측 오버레이 표시를 이어서 실행한다. + +## 원본 근거 + +- 요구사항정의서 `3. (기능)요구사항정의 및 추적표` 94행, `SFR-011-008 위험도 산정` +- 화면설계서 슬라이드 24, `SN_MN010205 침수예측현황` +- 화면설계서 슬라이드 28, `SN_MN010206 침수예측지도` +- 자동 레이어 선택 비교 화면: 슬라이드 47, `SN_MN040106 조사결과 지도보기` + +## 변경 내용 + +- 하수관망도에 이미 연결된 `checkedLayerRequest`를 사용하여 `도시침수지도(정보제공포털)` 레이어를 기존 체크 상태에 추가한다. +- 도시침수지도 레이어가 활성화되기 전에 도착한 행 선택 정보는 임시 상태에 보관한다. +- 레이어가 준비되면 기존 시군구·빈도 피처 선택, 지도 범위 이동, 침수예측 오버레이 표시 로직을 실행한다. +- 제한 시간 안에 레이어가 준비되지 않으면 기존 수동 선택 안내 메시지를 팝업에 전달한다. + +## 영향 범위 + +- `apps/sn/app/(map-layout)/map/page.tsx` +- 백엔드 API와 메시지 payload 계약은 변경하지 않는다. +- 기존에 사용자가 체크한 다른 지도 레이어는 유지한다. +- 도시침수지도 미제공 빈도에 대한 기존 안내는 유지한다. + +## 검증 + +- `pnpm --filter sn exec tsc --noEmit` 통과 +- `git diff --check` 통과 +- 개발 서버가 3002 포트에서 실행 중이므로 별도 빌드는 수행하지 않는다. + +## 롤백 + +- `map/page.tsx`의 도시침수지도 자동 체크 요청, 보류 상태, 재시도 effect를 제거하고 기존 레이어 가시성 검사와 수동 선택 안내 흐름으로 복원한다. + +## 남은 확인 + +- 로그인된 브라우저에서 도시침수지도가 꺼진 상태와 이미 켜진 상태를 각각 확인한다. +- 행을 연속 선택했을 때 마지막 선택 행 기준으로 지도와 오버레이가 갱신되는지 확인한다. diff --git a/apps/sn/docs/작업내용/SFR-011-노후관로관리/001-fe-SFR-011-노후관로관리-통합구현기록.md b/apps/sn/docs/작업내용/SFR-011-노후관로관리/001-fe-SFR-011-노후관로관리-통합구현기록.md index bde98f381..762fb9422 100644 --- a/apps/sn/docs/작업내용/SFR-011-노후관로관리/001-fe-SFR-011-노후관로관리-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-011-노후관로관리/001-fe-SFR-011-노후관로관리-통합구현기록.md @@ -115,3 +115,12 @@ - 실제 로그인 지도에서 노후관로 범례와 지도 강조선의 색상 일치 여부를 육안 확인해야 한다. - 사용자 스크린샷 기준의 정확한 위치 체감은 실제 브라우저 수동 확인이 필요하다. - 최소화 후 다시 열 때 사용하는 메인 화면의 미니 카드 위치는 이번 라운드 범위에 포함하지 않았다. + +## 2026-10-01 DB 레이어 설정 이관 +- 변경 파일: map/page.tsx, agedPipe/agedPipeUtils.ts, agedPipe/AgedPipePopupPage.tsx, 신규 agedPipe/agedPipeLayer.ts. +- appendAgedPipeControlNode, SN_AGED_PIPE 및 SN_AGED_PIPE_HIGHLIGHT 생성, 스타일 상수 덮어쓰기를 제거했다. +- 기존 설정 API 응답의 레이어 ID·원천·스타일을 사용하고 최초 요청 전에 sql_filt의 ${cutoffYmd}를 치환한다. 조건 변경 시 같은 ID의 레이어만 갱신한다. +- 숨긴 상태의 조건 변경도 재표시할 때 적용한다. 노후관로를 유하방향 관종 스타일 수집에서 제외한다. +- DB 미전환/필터 오류는 해당 행만 요청 차단하고 안내하며 다른 시설물 트리는 유지한다. +- 독립 목록 팝업도 레이어 설정 API를 사용한다. Excel API와 위치 이동 동작은 유지한다. 백엔드/공통 GIS 코드는 변경하지 않았다. +- DB작업.sql에 INSERT 및 기존 139번용 UPDATE를 작성했다. 실제 DB 변경은 미실행이다. diff --git a/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizFormPage.tsx b/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizFormPage.tsx index c49c8c673..f0507195b 100644 --- a/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizFormPage.tsx +++ b/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizFormPage.tsx @@ -393,6 +393,8 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) { suggestionOptions.length > 0 ? `${fieldId}-suggestions` : undefined; const span: 1 | 2 | 3 = key === "excptnMttrCn" ? 3 : key === "bizNm" || key === "pstn" ? 2 : 1; + // 수정 화면에서는 중복 판정 업무키인 사업순번과 단계를 변경할 수 없다. + const isBusinessKeyLocked = Boolean(id) && (key === "seqNo" || key === "stp"); return ( setForm((current) => ({ diff --git a/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx b/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx index 9d4107ddd..d86dfbb34 100644 --- a/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx +++ b/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx @@ -164,13 +164,11 @@ const formatLocalDateInputValue = (date: Date) => { return `${year}-${month}-${day}`; }; -/** 지정일 기본 검색기간을 오늘까지의 최근 1년으로 생성한다. */ +/** 지정일 기본 검색기간을 최초 사업자료 기준일인 2013-01-01부터 오늘까지로 생성한다. */ const createDefaultDesignationDateRange = () => { const to = new Date(); - const from = new Date(to); - from.setFullYear(from.getFullYear() - 1); return { - dsgnYmdFrom: formatLocalDateInputValue(from), + dsgnYmdFrom: "2013-01-01", dsgnYmdTo: formatLocalDateInputValue(to), }; }; diff --git a/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx b/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx index 67249e17f..91e94b3a7 100644 --- a/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx +++ b/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx @@ -17,7 +17,6 @@ import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable"; import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel"; import { useAlertStore } from "@/shared/lib"; import useStore from "@/store/useStore"; -import { createPredictionAnalysisTimeRange } from "../model/analysisTimeRange"; import { fetchAllPages } from "@/shared/lib/fetch-all-pages"; import { downloadFloodPredictionStatusExcel } from "../lib/fldng-predc-prst-excel"; @@ -111,21 +110,16 @@ const HEADS: UiHeadProps[] = [ { id: "floodRiskGrade", title: "침수위험도", widthPercent: 10 }, ]; -/** 최초·초기화 조회조건은 금일 분석결과를 기준으로 생성한다. */ -const buildInitialFilters = (): Filters => { - const range = createPredictionAnalysisTimeRange(); - const toDateTimeLocal = (value: string) => - `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}T${value.slice(8, 10)}:${value.slice(10, 12)}`; - return { - ctpvCd: "0", - sggCd: "0", - rainBgngFrom: "", - rainBgngTo: "", - analysisFrom: toDateTimeLocal(range.analysisFrom), - analysisTo: toDateTimeLocal(range.analysisTo), - frequencyYear: "0", - }; -}; +/** 최초·초기화 시 시각 범위를 제한하지 않고 최신 침수예측 데이터부터 조회한다. */ +const buildInitialFilters = (): Filters => ({ + ctpvCd: "0", + sggCd: "0", + rainBgngFrom: "", + rainBgngTo: "", + analysisFrom: "", + analysisTo: "", + frequencyYear: "0", +}); const apiDate = (value: string) => value.replace(/[^0-9]/g, "").slice(0, 12); @@ -151,6 +145,7 @@ const buildQuery = (filters: Filters, page?: number, size = PAGE_SIZE) => { if (page !== undefined) { params.set("page", String(page - 1)); params.set("size", String(size)); + params.set("sort", "analysisDateTime,desc"); } appendApiSearchParams(params, { ctpvCd: filters.ctpvCd === "0" ? "0" : filters.ctpvCd.slice(0, 2),