From 6c84ba6aeb73f7650f0852dfb620125a3d634118 Mon Sep 17 00:00:00 2001 From: kiminchan Date: Thu, 9 Jul 2026 10:45:53 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EC=B9=A8=EC=9E=85=EC=88=98=C2=B7RDII=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=ED=99=94=EB=A9=B4=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EB=B0=8F=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=EA=B2=B0=EA=B3=BC=20popup=20=EC=97=B0=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../analysisMenu/AnalysisPopupMenu.tsx | 9 +++++- .../inftwtResult/InftwtResultPopupPage.tsx | 15 +++++++++ .../inftwtResult/inftwtResultTypes.ts | 1 + .../inftwtResult/useInftwtResultMap.ts | 19 +++++++++++- .../rdiiResult/RdiiResultPopupPage.tsx | 15 +++++++++ .../components/rdiiResult/rdiiResultTypes.ts | 1 + .../components/rdiiResult/useRdiiResultMap.ts | 19 +++++++++++- apps/sn/app/(map-layout)/map/page.tsx | 12 +++++-- .../app/(with-base-layout)/ii/BaseLayout.tsx | 13 ++++++++ .../ii/analysisResultView.ts | 31 +++++++++++++++++++ .../(with-base-layout)/ii/irsetting/page.tsx | 3 ++ .../app/(with-base-layout)/ii/rdii/page.tsx | 3 ++ .../001-fe-SN-침입수분석결과-지도연계유지보수-작업카드.md | 8 ++++- .../001-fe-SN-RDII분석결과-지도연계유지보수-통합구현기록.md | 14 ++++++++- .../001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md | 14 ++++++++- 15 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 apps/sn/app/(with-base-layout)/ii/analysisResultView.ts diff --git a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx index 37061b293..0de095238 100644 --- a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx +++ b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx @@ -20,6 +20,7 @@ type Props = { onToggleGisEdit?: () => void; isGisEditActive?: boolean; isInfoSearchActive?: boolean; + initialActiveKey?: WindowPopupKey | null; }; type MenuIconProps = { @@ -73,9 +74,10 @@ export default function AnalysisPopupMenu({ onToggleGisEdit, isGisEditActive = false, isInfoSearchActive = false, + initialActiveKey = null, }: Props) { const [isOpen, setIsOpen] = useState(true); - const [activeKey, setActiveKey] = useState(null); + const [activeKey, setActiveKey] = useState(initialActiveKey); useEffect(() => { if (!isGisEditActive && activeKey === "gisEditPopup") { @@ -89,6 +91,11 @@ export default function AnalysisPopupMenu({ } }, [activeKey, isInfoSearchActive]); + useEffect(() => { + if (!initialActiveKey) return; + setActiveKey((currentKey) => currentKey ?? initialActiveKey); + }, [initialActiveKey]); + return (
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 9de542912..11e241455 100644 --- a/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx @@ -12,6 +12,7 @@ import useStore from "@/store/useStore"; import { INFTWT_RESULT_ROW_SELECT_MESSAGE, + INFTWT_RESULT_ROW_SELECT_STORAGE_KEY, type InftwtResultRow, } from "./inftwtResultTypes"; @@ -56,6 +57,20 @@ const DEFAULT_FILTERS: InftwtResultFilters = { }; const postRowSelectMessage = (row: InftwtResultRow) => { + if (typeof window !== "undefined") { + try { + window.localStorage.setItem( + INFTWT_RESULT_ROW_SELECT_STORAGE_KEY, + JSON.stringify({ + row, + timestamp: Date.now(), + }), + ); + } catch (error) { + console.warn("침입수 분석결과 선택 동기화 저장에 실패했습니다.", error); + } + } + if (typeof window === "undefined" || !window.opener) return; window.opener.postMessage( { diff --git a/apps/sn/app/(map-layout)/map/components/inftwtResult/inftwtResultTypes.ts b/apps/sn/app/(map-layout)/map/components/inftwtResult/inftwtResultTypes.ts index 10917e591..5112afe49 100644 --- a/apps/sn/app/(map-layout)/map/components/inftwtResult/inftwtResultTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/inftwtResult/inftwtResultTypes.ts @@ -2,6 +2,7 @@ export const INFTWT_RESULT_ROW_SELECT_MESSAGE = "SN_INFTWT_RESULT_ROW_SELECT"; export const INFTWT_RESULT_OVERLAY_TYPE = "INFTWT_RESULT"; +export const INFTWT_RESULT_ROW_SELECT_STORAGE_KEY = "SN_INFTWT_RESULT_ROW_SELECT_STORAGE"; export type InftwtResultRow = { rowNumber: number; diff --git a/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts b/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts index fa3aba0c1..a7f5088d6 100644 --- a/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts +++ b/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts @@ -18,6 +18,7 @@ import { getSigunguCenter, moveMapToCenter, sidoCenter } from "@common/business_ import { INFTWT_RESULT_OVERLAY_TYPE, INFTWT_RESULT_ROW_SELECT_MESSAGE, + INFTWT_RESULT_ROW_SELECT_STORAGE_KEY, type InftwtResultMapViewResponse, type InftwtResultRow, } from "./inftwtResultTypes"; @@ -410,8 +411,24 @@ export const useInftwtResultMap = ({ void handleRowSelect(event.data.row as InftwtResultRow); }; + const handleStorageEvent = (event: StorageEvent) => { + if (event.key !== INFTWT_RESULT_ROW_SELECT_STORAGE_KEY || !event.newValue) return; + + try { + const payload = JSON.parse(event.newValue) as { row?: InftwtResultRow }; + if (!payload?.row) return; + void handleRowSelect(payload.row); + } catch (error) { + console.warn("침입수 분석결과 storage 동기화 값을 해석하지 못했습니다.", error); + } + }; + window.addEventListener("message", handlePopupMessage); - return () => window.removeEventListener("message", handlePopupMessage); + window.addEventListener("storage", handleStorageEvent); + return () => { + window.removeEventListener("message", handlePopupMessage); + window.removeEventListener("storage", handleStorageEvent); + }; }, [handleRowSelect]); useEffect(() => { 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 bcda216de..e124f6812 100644 --- a/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx @@ -12,6 +12,7 @@ import useStore from "@/store/useStore"; import { RDII_RESULT_ROW_SELECT_MESSAGE, + RDII_RESULT_ROW_SELECT_STORAGE_KEY, type RdiiResultRow, } from "./rdiiResultTypes"; @@ -58,6 +59,20 @@ const DEFAULT_FILTERS: RdiiResultFilters = { }; const postRowSelectMessage = (row: RdiiResultRow) => { + if (typeof window !== "undefined") { + try { + window.localStorage.setItem( + RDII_RESULT_ROW_SELECT_STORAGE_KEY, + JSON.stringify({ + row, + timestamp: Date.now(), + }), + ); + } catch (error) { + console.warn("RDII 분석결과 선택 동기화 저장에 실패했습니다.", error); + } + } + if (typeof window === "undefined" || !window.opener) return; window.opener.postMessage( { diff --git a/apps/sn/app/(map-layout)/map/components/rdiiResult/rdiiResultTypes.ts b/apps/sn/app/(map-layout)/map/components/rdiiResult/rdiiResultTypes.ts index 30604e653..7dc76db34 100644 --- a/apps/sn/app/(map-layout)/map/components/rdiiResult/rdiiResultTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/rdiiResult/rdiiResultTypes.ts @@ -2,6 +2,7 @@ export const RDII_RESULT_ROW_SELECT_MESSAGE = "SN_RDII_RESULT_ROW_SELECT"; export const RDII_RESULT_OVERLAY_TYPE = "RDII_RESULT"; +export const RDII_RESULT_ROW_SELECT_STORAGE_KEY = "SN_RDII_RESULT_ROW_SELECT_STORAGE"; export type RdiiResultRow = { rowNumber: number; diff --git a/apps/sn/app/(map-layout)/map/components/rdiiResult/useRdiiResultMap.ts b/apps/sn/app/(map-layout)/map/components/rdiiResult/useRdiiResultMap.ts index bca19cc0b..91b7efcdc 100644 --- a/apps/sn/app/(map-layout)/map/components/rdiiResult/useRdiiResultMap.ts +++ b/apps/sn/app/(map-layout)/map/components/rdiiResult/useRdiiResultMap.ts @@ -16,6 +16,7 @@ import { getSigunguCenter, moveMapToCenter, sidoCenter } from "@common/business_ import { RDII_RESULT_OVERLAY_TYPE, RDII_RESULT_ROW_SELECT_MESSAGE, + RDII_RESULT_ROW_SELECT_STORAGE_KEY, type RdiiResultMapViewResponse, type RdiiResultRow, } from "./rdiiResultTypes"; @@ -303,8 +304,24 @@ export const useRdiiResultMap = ({ void handleRowSelect(event.data.row as RdiiResultRow); }; + const handleStorageEvent = (event: StorageEvent) => { + if (event.key !== RDII_RESULT_ROW_SELECT_STORAGE_KEY || !event.newValue) return; + + try { + const payload = JSON.parse(event.newValue) as { row?: RdiiResultRow }; + if (!payload?.row) return; + void handleRowSelect(payload.row); + } catch (error) { + console.warn("RDII 분석결과 storage 동기화 값을 해석하지 못했습니다.", error); + } + }; + window.addEventListener("message", handlePopupMessage); - return () => window.removeEventListener("message", handlePopupMessage); + window.addEventListener("storage", handleStorageEvent); + return () => { + window.removeEventListener("message", handlePopupMessage); + window.removeEventListener("storage", handleStorageEvent); + }; }, [handleRowSelect]); useEffect(() => { diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 082460f20..780f69028 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -19,7 +19,7 @@ import dynamic from "next/dynamic"; import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; -import { usePathname } from "next/navigation"; +import { usePathname, useSearchParams } from "next/navigation"; import Collection from "ol/Collection.js"; import Feature from "ol/Feature.js"; @@ -41,7 +41,7 @@ import Style from "ol/style/Style.js"; import Text from "ol/style/Text.js"; import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup"; -import type { WindowPopupKey } from "@/features/windowPopup/model/popupDefinitions"; +import { isWindowPopupKey, type WindowPopupKey } from "@/features/windowPopup/model/popupDefinitions"; import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types"; import Gis from "@common/business_components/ui/map/Gis"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; @@ -2401,7 +2401,14 @@ const appendSinkholeControlNode = (data: any[]) => { }; const Map = () => { + const searchParams = useSearchParams(); const pathname = usePathname(); + const initialAnalysisPopupKey = useMemo(() => { + const popupKey = searchParams.get("analysisPopupKey"); + if (!popupKey || !isWindowPopupKey(popupKey)) return null; + if (popupKey !== "inftwtResultPopup" && popupKey !== "rdiiResultPopup") return null; + return popupKey; + }, [searchParams]); const flowDirectionMode = useMemo(() => getFlowDirectionModeByPathname(pathname), [pathname]); const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore(); const userInfo = useUserStore((state) => state.userInfo); @@ -7080,6 +7087,7 @@ const Map = () => { /> ) : null} setIsSearchModalOpen(true)} onToggleGisEdit={handleToggleGisEdit} diff --git a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx index 7109141b7..4634eec80 100644 --- a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx +++ b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx @@ -49,6 +49,7 @@ type BaseLayout = { onSubmitEvent: Function graphBtn?: boolean analysisBtn?: string + resultViewBtn?: string firstDivBtn?: boolean selectBox?: SelectBox selectionComponent?: ReactNode @@ -71,6 +72,7 @@ type BaseLayout = { onExcelDownload?: () => void; onDeleteFile?: () => void; onAnalysis?: () => void; + onResultView?: () => void; selectedRowData?: any; isIr?: boolean; isRdii?: boolean; @@ -83,6 +85,7 @@ const BaseLayout = ({ onSubmitEvent, graphBtn = false, analysisBtn, + resultViewBtn, selectBox, selectionComponent, showDatePicker = false, @@ -104,6 +107,7 @@ const BaseLayout = ({ onExcelDownload, onDeleteFile, onAnalysis, + onResultView, isIr = false, isRdii = false, }: BaseLayout) => { @@ -251,6 +255,15 @@ const BaseLayout = ({ {analysisBtn && ( )} + {resultViewBtn && ( + + )}
diff --git a/apps/sn/app/(with-base-layout)/ii/analysisResultView.ts b/apps/sn/app/(with-base-layout)/ii/analysisResultView.ts new file mode 100644 index 000000000..35c2bec26 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/analysisResultView.ts @@ -0,0 +1,31 @@ +"use client"; + +import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup"; + +type AnalysisResultPopupKey = "inftwtResultPopup" | "rdiiResultPopup"; + +const ANALYSIS_RESULT_MAP_WINDOW_NAME = "sn-analysis-result-map"; + +export const openAnalysisResultMapView = (popupKey: AnalysisResultPopupKey) => { + if (typeof window === "undefined") return; + + const mapUrl = `/map?analysisPopupKey=${encodeURIComponent(popupKey)}`; + const mapWindow = window.open(mapUrl, ANALYSIS_RESULT_MAP_WINDOW_NAME); + + if (!mapWindow) { + alert("하수관망도 창이 차단되었습니다. 브라우저 팝업 허용 후 다시 시도해주세요."); + return; + } + + // 분석 화면에서 직접 결과 popup을 열면 opener는 map이 아니라 현재 화면이 된다. + // map 창은 별도 탭으로 띄우고, 결과 popup의 row 선택은 storage 이벤트로 다시 map에 전달한다. + const popupWindow = openWindowPopup(popupKey); + mapWindow.focus(); + + if (!popupWindow) { + alert("분석결과 popup이 차단되었습니다. 브라우저 팝업 허용 후 다시 시도해주세요."); + return; + } + + popupWindow.focus(); +}; diff --git a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx index 7cbbff083..8b4276ca9 100644 --- a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx @@ -28,6 +28,7 @@ import { useForm } from 'react-hook-form' import { z } from 'zod' import { excelToFlowRateData, excelToMatchingResultData, excelToTimePatternData, excelToRcptrInfoData } from '@/components/lib/utils' import BaseLayout from '../BaseLayout' +import { openAnalysisResultMapView } from '../analysisResultView' import { validateMultipleFiles, FILE_EXTENSIONS, type FileValidationConfig } from '../../fileValidation'; import { validateBasicSelectionsWithAlert } from '../../selectionValidation'; import { tokenManager } from '@/entities/auth/lib/token-manager'; @@ -377,6 +378,7 @@ const IrSetting = () => { form={form} onSubmitEvent={() => { }} graphBtn={false} + resultViewBtn={'결과보기'} analysisBtn={'침입수분석 실행'} selectBox={{ title: '분석 대상지역 선택' }} ctpvCd={ctpvCd} @@ -389,6 +391,7 @@ const IrSetting = () => { setSggName={setSggName} setSwtrmtFcltNm={setSwtrmtFcltNm} setIsLoading={setIsLoadingResult} + onResultView={() => openAnalysisResultMapView('inftwtResultPopup')} onAnalysis={handleAnalysisClick} isIr={true} > diff --git a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx index 4fd6d947a..30da5f7cb 100644 --- a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx @@ -22,6 +22,7 @@ import { postMutiPart } from '@/components/lib/api' import FileUploadForm from "@/components/ui/fileUploadForm" import FormSection from "@/components/ui/formSection" import BaseLayout from "../BaseLayout" +import { openAnalysisResultMapView } from "../analysisResultView" import { DateMonthPicker, filePicked } from "@common/business_components/ui" import { Input } from "@common/components/ui" import { zodResolver } from "@hookform/resolvers/zod" @@ -413,6 +414,7 @@ export default function RTKForm() { form={form} // react-hook-form 인스턴스 전달 onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit graphBtn={false} + resultViewBtn={'결과보기'} analysisBtn={'RDII분석 실행'} ctpvCd={ctpvCd} sggCd={sggCd} @@ -424,6 +426,7 @@ export default function RTKForm() { setSggName={setSggName} setSwtrmtFcltNm={setSwtrmtFcltNm} setIsLoading={setIsLoadingResult} + onResultView={() => openAnalysisResultMapView('rdiiResultPopup')} onAnalysis={handleAnalysisClick} isRdii={true} > diff --git a/apps/sn/docs/작업계획/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-작업카드.md b/apps/sn/docs/작업계획/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-작업카드.md index 68a99e880..2b1352fba 100644 --- a/apps/sn/docs/작업계획/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-작업카드.md +++ b/apps/sn/docs/작업계획/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-작업카드.md @@ -135,4 +135,10 @@ - `ii/irsetting`만 기존 시설 우선 selector를 그대로 쓰지 않고, 커스텀 `selectionComponent`로 처리구역 우선 selector를 주입한다. - 새 selector는 시도/시군구 선택 뒤 `GET /api/v1/swagePrcsZones/selectionOptions`를 호출해 `하수처리구역명` 목록을 받고, 선택 시 `하수처리시설명`을 읽기 전용으로 자동 표시한다. - `ii/rdii`, `ii/result`, 지도 검색 모달 등 기존 `CtpvSggPrcsFcltSelectBox` 사용처는 이번 범위에서 변경하지 않는다. -- 저장 payload는 기존과 동일하게 `swtrmtFcltNm`만 유지해 회귀 범위를 최소화한다. \ No newline at end of file +- 저장 payload는 기존과 동일하게 `swtrmtFcltNm`만 유지해 회귀 범위를 최소화한다. +## 14. 2026-07-09 분석 실행 화면 결과보기 유지보수 + +- `ii/irsetting` 우측 액션 영역에 `결과보기` 버튼을 추가한다. +- 버튼 클릭 시 분석 화면을 닫지 않고, 별도 `/map` 창과 `침입수 분석결과` popup을 함께 열어 기존 지도형 결과 흐름으로 바로 진입시킨다. +- popup row select는 기존 `window.opener.postMessage`를 유지하되, opener가 분석 화면일 수 있으므로 storage 이벤트 기반 보조 동기화를 함께 둔다. +- `map` 화면은 `analysisPopupKey` query를 받아 `분석도구` 활성 메뉴 상태를 `침입수 분석결과` 기준으로 맞춘다. diff --git a/apps/sn/docs/작업내용/SN-RDII분석결과-지도연계유지보수/001-fe-SN-RDII분석결과-지도연계유지보수-통합구현기록.md b/apps/sn/docs/작업내용/SN-RDII분석결과-지도연계유지보수/001-fe-SN-RDII분석결과-지도연계유지보수-통합구현기록.md index 6d7b8c591..498bd7407 100644 --- a/apps/sn/docs/작업내용/SN-RDII분석결과-지도연계유지보수/001-fe-SN-RDII분석결과-지도연계유지보수-통합구현기록.md +++ b/apps/sn/docs/작업내용/SN-RDII분석결과-지도연계유지보수/001-fe-SN-RDII분석결과-지도연계유지보수-통합구현기록.md @@ -43,4 +43,16 @@ - `node node_modules\typescript\bin\tsc --noEmit --ignoreDeprecations 5.0 -p tsconfig.json` 재실행 결과, 이번 변경과 무관한 기존 선행 오류만 남고 RDII 변경 파일 기준 신규 오류는 확인되지 않았다. ## 8. 최종 확인 메모 - 사용자 실동작 기준으로 `분석도구 > RDII 분석` popup, row click 지도 이동, 관로 highlight, resultOnly popup 흐름이 정상 동작하는 것으로 마감한다. -- FE 전체 타입 오류는 이번 RDII 변경과 무관한 기존 선행 이슈만 남아 별도 정리 대상으로 유지한다. \ No newline at end of file +- FE 전체 타입 오류는 이번 RDII 변경과 무관한 기존 선행 이슈만 남아 별도 정리 대상으로 유지한다. +## 9. 2026-07-09 분석 화면 결과보기 버튼 연계 + +- `ii/BaseLayout.tsx` 공통 액션 영역에 `결과보기` 버튼 슬롯을 추가하고, `ii/rdii/page.tsx`에서 `rdiiResultPopup` 전용 진입 함수를 연결했다. +- `ii/analysisResultView.ts`는 `/map?analysisPopupKey=rdiiResultPopup` 창과 RDII 결과 popup을 같은 사용자 클릭에서 함께 연다. +- `RdiiResultPopupPage` row click은 storage 이벤트용 payload를 함께 남기고, `useRdiiResultMap`은 storage listener를 추가해 opener가 map이 아닌 경우에도 highlight 흐름을 이어가게 했다. +- `map/page.tsx`와 `AnalysisPopupMenu.tsx`는 query 기준으로 `RDII 분석결과` 메뉴 활성 상태를 맞춘다. +- 같은 TypeScript 검증에서 이번 RDII 결과보기 버튼 연계 파일 기준 신규 오류는 확인되지 않았다. + +## 10. 2026-07-09 버튼 순서 미세조정 + +- 사용자 요청에 따라 RDII 분석 화면 액션 버튼 순서를 `양식 받기 -> RDII분석 실행 -> 결과보기`로 조정했다. +- 변경은 공통 `ii/BaseLayout.tsx` 렌더 순서만 수정해 침입수 분석 화면과 동일한 배치 규칙을 사용하도록 맞췄다. diff --git a/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md b/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md index c1deaceba..e9916a137 100644 --- a/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md +++ b/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md @@ -99,4 +99,16 @@ - 사용자 요청 기준으로 좌측 `I/I 분석` 메뉴의 `침입수 분석결과` 항목은 현재 화면에서 숨겼다. - 숨김 방식은 공통 LNB 데이터 구조를 다시 건드리지 않고, `FE_APP_ROOT/src/app/styles/components.css`에서 해당 서브메뉴 항목만 표시하지 않도록 제한했다. - 이 보정은 메뉴 노출만 감추는 수준이며, 기존 `ii/result` route와 화면 코드는 유지한다. -- 따라서 추후 필요 시 스타일 규칙만 제거하면 기존 업무형 결과 화면 진입선을 다시 살릴 수 있다. \ No newline at end of file +- 따라서 추후 필요 시 스타일 규칙만 제거하면 기존 업무형 결과 화면 진입선을 다시 살릴 수 있다. +## 15. 2026-07-09 분석 화면 결과보기 버튼 연계 + +- `ii/BaseLayout.tsx`에 `결과보기` 버튼 슬롯을 추가하고, `ii/irsetting/page.tsx`에서 `inftwtResultPopup` 전용 진입 함수를 연결했다. +- 새 helper `ii/analysisResultView.ts`는 사용자 클릭 1회에서 `/map?analysisPopupKey=inftwtResultPopup` 창과 결과 popup을 함께 열어 popup 차단 가능성을 줄인다. +- `InftwtResultPopupPage` row click은 기존 opener postMessage 외에 `localStorage`에도 선택 row를 기록하고, `useInftwtResultMap`은 storage 이벤트를 함께 구독해 map 창이 opener가 아니어도 지도 연계를 이어가게 했다. +- `map/page.tsx`와 `AnalysisPopupMenu.tsx`는 `analysisPopupKey`가 있을 때 `침입수 분석결과` 메뉴를 활성 상태로 보여 주도록 보강했다. +- `node node_modules\typescript\bin\tsc --noEmit --ignoreDeprecations 5.0 -p tsconfig.json` 재실행 결과, 이번 변경 파일 기준 신규 오류는 없고 기존 선행 오류만 남았다. + +## 16. 2026-07-09 버튼 순서 미세조정 + +- 사용자 요청에 따라 침입수 분석 화면 액션 버튼 순서를 `양식 받기 -> 침입수분석 실행 -> 결과보기`로 조정했다. +- 변경은 공통 `ii/BaseLayout.tsx` 렌더 순서만 수정해 RDII 분석 화면에도 같은 순서가 적용되도록 맞췄다. From ca7ac992e29e8a4e433beccb313e8ef52403bf18 Mon Sep 17 00:00:00 2001 From: kiminchan Date: Thu, 9 Jul 2026 11:37:26 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EC=B9=A8=EC=9E=85=EC=88=98=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=EA=B2=B0=EA=B3=BC=20=EC=8B=A4=EC=B8=A1=ED=95=98?= =?UTF-8?q?=EC=88=98=EB=9F=89=20=EA=B4=80=EB=A1=9C=20=EC=9A=B0=EC=84=A0=20?= =?UTF-8?q?=EC=A4=8C=EC=9D=B8=20=EB=B0=8F=20=EC=A7=80=EC=97=AD=20=EC=84=A0?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inftwtResult/useInftwtResultMap.ts | 87 +++++++++++++------ .../001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md | 12 +++ .../src/ui/map/searchBar/procSearchBar.tsx | 36 ++++---- 3 files changed, 94 insertions(+), 41 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts b/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts index a7f5088d6..11984c8e1 100644 --- a/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts +++ b/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts @@ -1,4 +1,4 @@ -"use client"; +"use client"; import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react"; import GeoJSON from "ol/format/GeoJSON.js"; @@ -161,6 +161,33 @@ const getFirstFeatureId = (payload: any) => { return String(firstFeature?.id ?? "").trim(); }; +const getOilqPipeExtent = (features: any[]) => { + const oilqFeatures = features.filter((feature) => Boolean(feature?.get?.("hasOilq"))); + if (oilqFeatures.length === 0) return null; + + let nextExtent: [number, number, number, number] | null = null; + oilqFeatures.forEach((feature) => { + const geometryExtent = feature?.getGeometry?.()?.getExtent?.(); + if (!Array.isArray(geometryExtent) || geometryExtent.some((value: number) => !Number.isFinite(value))) { + return; + } + + if (!nextExtent) { + nextExtent = [...geometryExtent] as [number, number, number, number]; + return; + } + + nextExtent = [ + Math.min(nextExtent[0], geometryExtent[0]), + Math.min(nextExtent[1], geometryExtent[1]), + Math.max(nextExtent[2], geometryExtent[2]), + Math.max(nextExtent[3], geometryExtent[3]), + ]; + }); + + return nextExtent; +}; + export const useInftwtResultMap = ({ gisMaps, setFeatureData, @@ -198,7 +225,7 @@ export const useInftwtResultMap = ({ }); layer.set("id", INFTWT_ZONE_LAYER_ID); - layer.set("lyrNm", "침입수분석-처리구역"); + layer.set("lyrNm", "移⑥엯?섎텇??泥섎━援ъ뿭"); layer.set("lyrTyNm", "tb_ms_swage_prcs_zone"); layer.set("clickPsblYn", "N"); layer.setVisible?.(true); @@ -224,7 +251,7 @@ export const useInftwtResultMap = ({ }); layer.set("id", INFTWT_PIPE_LAYER_ID); - layer.set("lyrNm", "침입수분석-결과관로"); + layer.set("lyrNm", "침입수분석 결과관로"); layer.set("lyrTyNm", "tb_ms_swage_ppl"); layer.set("clickPsblYn", "Y"); layer.setVisible?.(true); @@ -241,6 +268,7 @@ export const useInftwtResultMap = ({ const zoneExtent = zoneSourceRef.current?.getExtent?.(); const pipeExtent = pipeSourceRef.current?.getExtent?.(); const pipeFeatures = pipeSourceRef.current?.getFeatures?.() ?? []; + const oilqPipeExtent = getOilqPipeExtent(pipeFeatures); const hasPipeFeatures = pipeFeatures.length > 0; const hasZoneFeatures = Boolean(zoneSourceRef.current?.getFeatures?.().length); const preferZone = mapView?.focusTargetType === "ZONE" && hasZoneFeatures; @@ -257,13 +285,15 @@ export const useInftwtResultMap = ({ : false; const shouldUseZoneExtent = preferZone && (hasPipeInsideZone || !hasPipeFeatures); - const extent = shouldUseZoneExtent - ? zoneExtent - : hasPipeFeatures - ? pipeExtent - : hasZoneFeatures - ? zoneExtent - : null; + const extent = oilqPipeExtent + ? oilqPipeExtent + : shouldUseZoneExtent + ? zoneExtent + : hasPipeFeatures + ? pipeExtent + : hasZoneFeatures + ? zoneExtent + : null; if (!map || !extent || extent.some((value: number) => !Number.isFinite(value))) { return; @@ -272,7 +302,7 @@ export const useInftwtResultMap = ({ map.getView?.().fit?.(extent, { duration: 350, padding: DEFAULT_RESULT_FIT_PADDING, - maxZoom: shouldUseZoneExtent ? 15 : 16, + maxZoom: oilqPipeExtent ? 17 : shouldUseZoneExtent ? 15 : 16, constrainResolution: true, nearest: true, }); @@ -307,6 +337,11 @@ export const useInftwtResultMap = ({ const moveToStableFocus = useCallback( async (mapView: InftwtResultMapViewResponse) => { + if ((mapView.oilqPipeCount ?? 0) > 0) { + fitToRenderedFeatures(mapView); + return; + } + const activeRow = activeResultRowRef.current; const movedByRegion = moveByRegionCodes( activeRow?.ctpvCd ?? mapView.analysis?.ctpvCd, @@ -353,7 +388,7 @@ export const useInftwtResultMap = ({ pipeLayerRef.current?.changed?.(); gisMaps.current?.renderSync?.(); - console.info("침입수 FE overlay 렌더 결과", { + console.info("移⑥엯??FE overlay ?뚮뜑 寃곌낵", { focusTargetType: mapView.focusTargetType, zoneFeatureCount: zoneFeatures.length, pipeFeatureCount: pipeFeatures.length, @@ -372,6 +407,15 @@ export const useInftwtResultMap = ({ setIsOpen(false); setIsSearchModalOpen(false); + const cachedMapView = mapViewCacheRef.current.get(row.inftwtAnlsSn); + let mapView = cachedMapView; + + if (!mapView) { + const payload = await getRawJson(`/sewerage-service/api/v1/inftwtAnlsRslt/mapView/${row.inftwtAnlsSn}`); + mapView = (payload?.data ?? payload) as InftwtResultMapViewResponse; + mapViewCacheRef.current.set(row.inftwtAnlsSn, mapView); + } + if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent(INFTWT_PROC_SEARCH_EVENT, { @@ -379,26 +423,17 @@ export const useInftwtResultMap = ({ ctpvCd: row.ctpvCd ?? "0", sggCd: row.sggCd ?? "0", swtrmtFcltNm: row.swtrmtFcltNm ?? "", + suppressMapMove: (mapView.oilqPipeCount ?? 0) > 0, }, }), ); } - const cachedMapView = mapViewCacheRef.current.get(row.inftwtAnlsSn); - if (cachedMapView) { - setActiveMapView(cachedMapView); - void renderMapView(cachedMapView); - return; - } - - const payload = await getRawJson(`/sewerage-service/api/v1/inftwtAnlsRslt/mapView/${row.inftwtAnlsSn}`); - const mapView = (payload?.data ?? payload) as InftwtResultMapViewResponse; - mapViewCacheRef.current.set(row.inftwtAnlsSn, mapView); setActiveMapView(mapView); void renderMapView(mapView); } catch (error) { - console.error("침입수 분석결과 지도 연계 중 오류:", error); - alert("침입수 분석결과를 지도에 연계하지 못했습니다."); + console.error("移⑥엯??遺꾩꽍寃곌낵 吏€???곌퀎 以??ㅻ쪟:", error); + alert("移⑥엯??遺꾩꽍寃곌낵瑜?吏€?꾩뿉 ?곌퀎?섏? 紐삵뻽?듬땲??"); } }, [renderMapView, setFeatureData, setIsOpen, setIsSearchModalOpen], @@ -419,7 +454,7 @@ export const useInftwtResultMap = ({ if (!payload?.row) return; void handleRowSelect(payload.row); } catch (error) { - console.warn("침입수 분석결과 storage 동기화 값을 해석하지 못했습니다.", error); + console.warn("移⑥엯??遺꾩꽍寃곌낵 storage ?숆린??媛믪쓣 ?댁꽍?섏? 紐삵뻽?듬땲??", error); } }; @@ -459,7 +494,7 @@ export const useInftwtResultMap = ({ }); if (!popupWindow) { - alert("팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요."); + alert("?앹뾽??李⑤떒?섏뿀?듬땲?? 釉뚮씪?곗? ?앹뾽 ?덉슜 ?ㅼ젙???뺤씤?댁<?몄슂."); } return true; diff --git a/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md b/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md index e9916a137..2f18e01e1 100644 --- a/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md +++ b/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md @@ -112,3 +112,15 @@ - 사용자 요청에 따라 침입수 분석 화면 액션 버튼 순서를 `양식 받기 -> 침입수분석 실행 -> 결과보기`로 조정했다. - 변경은 공통 `ii/BaseLayout.tsx` 렌더 순서만 수정해 RDII 분석 화면에도 같은 순서가 적용되도록 맞췄다. + +## 17. 2026-07-09 실측하수량 관로 우선 줌인 보정 + +- 침입수 분석결과 popup에서 row click 시 `oilqPipeCount > 0`이면 시도/시군구 중심 이동보다 `hasOilq` 관로 extent fit을 우선 적용하도록 `useInftwtResultMap`을 보정했다. +- 실측하수량 관로가 없는 row만 기존 지역 중심 이동과 일반 결과 관로 fallback을 유지한다. +- 이 보정으로 주황색으로 표시되는 실측하수량 관로가 있는 케이스는 지도 진입 직후 해당 관로 주변으로 바로 확대된다. + +## 18. 2026-07-09 지역 선이동 제거 보정 + +- `useInftwtResultMap`은 row click 직후 mapView를 먼저 확보한 뒤, `oilqPipeCount > 0`이면 검색바 동기화 이벤트에 `suppressMapMove`를 내려 지역 중심 이동을 건너뛰도록 보정했다. +- `procSearchBar`는 `suppressMapMove`가 내려오면 시도/시군구/시설 상태만 갱신하고 `moveMapToCenter`는 호출하지 않는다. +- 이 보정으로 실측하수량 관로가 있는 케이스는 처음부터 해당 관로 extent로 바로 확대된다. diff --git a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx index 84016e5a8..b3f715086 100644 --- a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx @@ -1,4 +1,4 @@ -"use client"; +"use client"; import { SelectBox, type SelectDataType } from "../../../ui"; import { Button } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; @@ -29,7 +29,7 @@ interface SearchBarProps { } interface CenterSelectDataType extends SelectDataType { - center?: [number, number]; // 좌표 중심 위치 + center?: [number, number]; // 醫뚰몴 以묒떖 ?꾩튂 } export const ProcSearchBar = ({ @@ -74,7 +74,7 @@ export const ProcSearchBar = ({ center: sidoCenter[String(f.properties.mctpv_cd ?? "").trim()] //getGeometryCenter(f.geometry) ?? undefined, })) .sort((a: any, b: any) => { - // 커스텀 순서 정의 + // 而ㅼ뒪?€ ?쒖꽌 ?뺤쓽 const customOrder = [ "서울특별시", "부산광역시", @@ -98,19 +98,19 @@ export const ProcSearchBar = ({ const indexA = customOrder.indexOf(a.text); const indexB = customOrder.indexOf(b.text); - // 둘 다 커스텀 순서에 있으면 해당 순서로 + // ????而ㅼ뒪?€ ?쒖꽌???덉쑝硫??대떦 ?쒖꽌濡? if (indexA !== -1 && indexB !== -1) { return indexA - indexB; } - // 하나만 커스텀 순서에 있으면 그것을 앞으로 + // ?섎굹留?而ㅼ뒪?€ ?쒖꽌???덉쑝硫?洹멸쾬???욎쑝濡? if (indexA !== -1) return -1; if (indexB !== -1) return 1; - // 둘 다 커스텀 순서에 없으면 기본 알파벳 순서 + // ????而ㅼ뒪?€ ?쒖꽌???놁쑝硫?湲곕낯 ?뚰뙆踰??쒖꽌 return a.text.localeCompare(b.text, 'ko'); }); - setSidoData([{ text: "시도", val: "0" }, ...list]); + setSidoData([{ text: "?쒕룄", val: "0" }, ...list]); }; const fetchSigungu = async () => { @@ -174,7 +174,7 @@ export const ProcSearchBar = ({ const json = await res.json(); list = (json.features || []) .map((f: any) => ({ - text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", + text: f.properties.swtrmt_fclt_nm ?? "(?대쫫?놁쓬)", val: f.id, })) .sort((a: any, b: any) => a.text.localeCompare(b.text, "ko")); @@ -193,28 +193,28 @@ export const ProcSearchBar = ({ }); const json = await res.json(); list = (json.data || []).map((item: any) => ({ - text: item.ruseUtztnFcltNm ?? "(이름없음)", + text: item.ruseUtztnFcltNm ?? "(?대쫫?놁쓬)", val: item.pbnstRuseFcltId, })); } else if (type === "tb_wr_ruse_fclt_dmd_co") { const res = await fetch(url); const json = await res.json(); list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", + text: item.dmdCoNm ?? "(?대쫫?놁쓬)", val: item.dmdInfoId, })); } else if (type === "tb_wr_tmpr_dmd_co") { const res = await fetch(url); const json = await res.json(); list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", + text: item.dmdCoNm ?? "(?대쫫?놁쓬)", val: item.fctryMngSn, })); } else { const res = await fetch(url); const json = await res.json(); list = (json.data || []).map((item: any) => ({ - text: item.bdstNm ?? "(이름없음)", + text: item.bdstNm ?? "(?대쫫?놁쓬)", val: item.bdstInfoSn, })); } @@ -264,7 +264,7 @@ export const ProcSearchBar = ({ const prefix = nextSidoCd.substring(0, 2); const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); - setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]); + setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]); setSigunValue("0"); }, [sigunData], @@ -315,7 +315,7 @@ export const ProcSearchBar = ({ const handleSearch = useCallback(() => { if (sidoValue === "0") { - alert("시도를 선택해 주세요"); + alert("시도를 선택해 주세요."); return; } @@ -344,10 +344,12 @@ export const ProcSearchBar = ({ ctpvCd?: string; sggCd?: string; swtrmtFcltNm?: string; + suppressMapMove?: boolean; }>; const nextCtpvCd = String(event.detail?.ctpvCd ?? "0").trim() || "0"; const nextSggCd = String(event.detail?.sggCd ?? "0").trim() || "0"; const nextSwtrmtFcltNm = String(event.detail?.swtrmtFcltNm ?? "").trim(); + const suppressMapMove = Boolean(event.detail?.suppressMapMove); const nextSidoCenter = sidoCenter[nextCtpvCd.slice(0, 2)]; const nextSigunguCenter = getSigunguCenter(nextSggCd); @@ -364,6 +366,10 @@ export const ProcSearchBar = ({ setFacilityValue("0"); setPendingExternalFacilityName(nextSwtrmtFcltNm); + if (suppressMapMove) { + return; + } + if (nextSggCd !== "0" && nextSigunguCenter) { moveMapToCenter(gisMaps, nextSigunguCenter, 12); return; @@ -430,7 +436,7 @@ export const ProcSearchBar = ({ />
From 3db8babee7c764b84e18b52c6c76074d21c0928e Mon Sep 17 00:00:00 2001 From: kiminchan Date: Thu, 9 Jul 2026 13:03:21 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EC=B9=A8=EC=9E=85=EC=88=98=C2=B7RDII=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EB=8C=80=EC=83=81=EC=A7=80=EC=97=AD=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B0=92=EC=9D=84=20=EC=96=91=ED=8F=89?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(with-base-layout)/ii/BaseLayout.tsx | 3 ++ .../(with-base-layout)/ii/irsetting/page.tsx | 18 +++++--- .../app/(with-base-layout)/ii/rdii/page.tsx | 23 ++++++---- .../ui/ctpvSggPrcsFcltSelectBox.tsx | 42 ++++++++++++------- .../001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md | 6 +++ 5 files changed, 66 insertions(+), 26 deletions(-) diff --git a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx index 4634eec80..702c5beb2 100644 --- a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx +++ b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx @@ -68,6 +68,7 @@ type BaseLayout = { setSggCd: (sgg: string) => void; setSggName?: (sggName: string) => void; setSwtrmtFcltNm?: (swtrmtFcltNm: string) => void; + defaultSwtrmtFcltNm?: string; setIsLoading?: (isLoading: boolean) => void; onExcelDownload?: () => void; onDeleteFile?: () => void; @@ -103,6 +104,7 @@ const BaseLayout = ({ setSggCd, setSggName, setSwtrmtFcltNm, + defaultSwtrmtFcltNm, setIsLoading, onExcelDownload, onDeleteFile, @@ -212,6 +214,7 @@ const BaseLayout = ({ selectedSwagePrcsFclt={selectedSwagePrcsFclt} onSwagePrcsFcltChange={onSwagePrcsFcltChange} setSwtrmtFcltNm={setSwtrmtFcltNm} + defaultSwtrmtFcltNm={defaultSwtrmtFcltNm} setIsLoading={setIsLoading} /> )} diff --git a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx index 8b4276ca9..80b4b58cd 100644 --- a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx @@ -59,6 +59,10 @@ interface UploadedFileInfo { } // 초기 파일 정보 상태 const initialFileInfo: UploadedFileInfo = { file: null, atflCd: null, atflId: null }; +const DEFAULT_CTPV_CD = '41'; +const DEFAULT_SGG_CD = '41830'; +const DEFAULT_SGG_NAME = '\uC591\uD3C9\uAD70'; +const DEFAULT_SWTRMT_FCLT_NM = '\uC591\uD3C9'; const IrSetting = () => { const [input1, setInput1] = useState('0.8'); // 오수전환율 초기값 @@ -66,10 +70,10 @@ const IrSetting = () => { const [selectedSwagePrcsFclt, setSelectedSwagePrcsFclt] = useState('0'); // ?????? ?? ?? const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 const [isLoadingResult, setIsLoadingResult] = useState(false); // 로딩 상태 - const [ctpvCd, setCtpvCd] = useState('0'); // 시도 코드 상태 - const [sggCd, setSggCd] = useState('0'); // 시군구 코드 상태 - const [sggName, setSggName] = useState(''); // 시군구 이름 상태 - const [swtrmtFcltNm, setSwtrmtFcltNm] = useState(''); // 하수처리시설명 상태 + const [ctpvCd, setCtpvCd] = useState(DEFAULT_CTPV_CD); // 시도 코드 상태 + const [sggCd, setSggCd] = useState(DEFAULT_SGG_CD); // 시군구 코드 상태 + const [sggName, setSggName] = useState(DEFAULT_SGG_NAME); // 시군구 이름 상태 + const [swtrmtFcltNm, setSwtrmtFcltNm] = useState(DEFAULT_SWTRMT_FCLT_NM); // 하수처리시설명 상태 const [uploadedFilesData, setUploadedFilesData] = useState([ initialFileInfo, // 0: 오수관망도 initialFileInfo, // 1: 수용가-맨홀 매칭결과 @@ -128,7 +132,10 @@ const IrSetting = () => { setInput1(''); // 입력 필드 초기화 setInput2(''); // 입력 필드 초기화 setSelectedSwagePrcsFclt('0'); - setSwtrmtFcltNm(''); + setCtpvCd(DEFAULT_CTPV_CD); + setSggCd(DEFAULT_SGG_CD); + setSggName(DEFAULT_SGG_NAME); + setSwtrmtFcltNm(DEFAULT_SWTRMT_FCLT_NM); setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경 }; @@ -390,6 +397,7 @@ const IrSetting = () => { setSggCd={setSggCd} setSggName={setSggName} setSwtrmtFcltNm={setSwtrmtFcltNm} + defaultSwtrmtFcltNm={swtrmtFcltNm} setIsLoading={setIsLoadingResult} onResultView={() => openAnalysisResultMapView('inftwtResultPopup')} onAnalysis={handleAnalysisClick} diff --git a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx index 30da5f7cb..b1c273be3 100644 --- a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx @@ -75,9 +75,9 @@ const formSchema = z.object({ type FormSchemaType = z.infer; // 기본 폼 값 정의 (테스트 편의를 위해 초기값 설정, 추후 삭제 필요) const defaultFormValues: FormSchemaType = { - ctpvCd: "", - sggCd: "", - swtrmtFcltNm: "", + ctpvCd: "41", + sggCd: "41830", + swtrmtFcltNm: "\uC591\uD3C9", rtkSource: "", rdiiArea: "", // 문자열로 기본값 설정 unitFactors: [ @@ -113,6 +113,10 @@ interface UploadedFileInfo { // 초기 파일 정보 상태 const initialFileInfo: UploadedFileInfo = { file: null, atflCd: null, atflId: null }; +const DEFAULT_CTPV_CD = '41'; +const DEFAULT_SGG_CD = '41830'; +const DEFAULT_SGG_NAME = '\uC591\uD3C9\uAD70'; +const DEFAULT_SWTRMT_FCLT_NM = '\uC591\uD3C9'; export default function RTKForm() { const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 @@ -127,10 +131,10 @@ export default function RTKForm() { ]); const [select1, setSelect1] = useState('0'); // 하수처리장 (swtrmtFcltCd) - const [ctpvCd, setCtpvCd] = useState('0'); // 시도 코드 상태 - const [sggCd, setSggCd] = useState('0'); // 시군구 코드 상태 - const [sggName, setSggName] = useState(''); // 시군구 이름 상태 - const [swtrmtFcltNm, setSwtrmtFcltNm] = useState(''); // 하수처리시설명 상태 + const [ctpvCd, setCtpvCd] = useState(DEFAULT_CTPV_CD); // 시도 코드 상태 + const [sggCd, setSggCd] = useState(DEFAULT_SGG_CD); // 시군구 코드 상태 + const [sggName, setSggName] = useState(DEFAULT_SGG_NAME); // 시군구 이름 상태 + const [swtrmtFcltNm, setSwtrmtFcltNm] = useState(DEFAULT_SWTRMT_FCLT_NM); // 하수처리시설명 상태 const { data: userInfo } = useQuery({ queryKey: [ENDPOINT.USER_SERVICE.USERS], @@ -154,6 +158,10 @@ export default function RTKForm() { ]); setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경 setSelect1('0'); + setCtpvCd(DEFAULT_CTPV_CD); + setSggCd(DEFAULT_SGG_CD); + setSggName(DEFAULT_SGG_NAME); + setSwtrmtFcltNm(DEFAULT_SWTRMT_FCLT_NM); }; // FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수 @@ -425,6 +433,7 @@ export default function RTKForm() { setSggCd={setSggCd} setSggName={setSggName} setSwtrmtFcltNm={setSwtrmtFcltNm} + defaultSwtrmtFcltNm={swtrmtFcltNm} setIsLoading={setIsLoadingResult} onResultView={() => openAnalysisResultMapView('rdiiResultPopup')} onAnalysis={handleAnalysisClick} diff --git a/apps/sn/app/components/ui/ctpvSggPrcsFcltSelectBox.tsx b/apps/sn/app/components/ui/ctpvSggPrcsFcltSelectBox.tsx index 25eb0a9d1..51eb5a166 100644 --- a/apps/sn/app/components/ui/ctpvSggPrcsFcltSelectBox.tsx +++ b/apps/sn/app/components/ui/ctpvSggPrcsFcltSelectBox.tsx @@ -35,10 +35,15 @@ interface CtpvSggPrcsFcltSelectBoxProps { selectedSwagePrcsFclt?: string; onSwagePrcsFcltChange?: (value: string) => void; setSwtrmtFcltNm?: (name: string) => void; + defaultSwtrmtFcltNm?: string; setIsLoading?: (isLoading: boolean) => void; } const defaultOption: ApiSelectOption = { val: '0', text: '전체' }; +const buildFacilityDefaultOption = (facilityName: string): ApiSelectOption => ({ + val: '0', + text: facilityName || defaultOption.text, +}); export function CtpvSggPrcsFcltSelectBox({ sido, @@ -52,6 +57,7 @@ export function CtpvSggPrcsFcltSelectBox({ selectedSwagePrcsFclt, onSwagePrcsFcltChange, setSwtrmtFcltNm, + defaultSwtrmtFcltNm, setIsLoading, }: CtpvSggPrcsFcltSelectBoxProps) { @@ -59,18 +65,7 @@ export function CtpvSggPrcsFcltSelectBox({ const { ctpv, sgg } = useStore(); const sidoStr = String(sido ?? ''); const sidoPrefix = sidoStr.slice(0, 2); - - useEffect(() => { - (async () => { - onSidoChange("0"); - onSigunguChange("0"); - if (setCtpvName && setSggName) { - setCtpvName(""); - setSggName(""); - } - if (setSwtrmtFcltNm) setSwtrmtFcltNm(""); - })(); - }, []); + const normalizedDefaultFacilityName = String(defaultSwtrmtFcltNm ?? '').trim(); useEffect(() => { onSwagePrcsFcltChange?.('0'); @@ -115,6 +110,25 @@ export function CtpvSggPrcsFcltSelectBox({ fetchData(); }, [sggName]); + useEffect(() => { + if (!normalizedDefaultFacilityName) return; + if (!selectedSwagePrcsFclt || selectedSwagePrcsFclt !== "0") return; + if (prcsFcltOptions.length === 0) return; + + const matchedDefaultFacility = prcsFcltOptions.find((option) => + String(option.text ?? "").trim() === normalizedDefaultFacilityName, + ); + if (!matchedDefaultFacility) return; + + onSwagePrcsFcltChange?.(String(matchedDefaultFacility.val)); + setSwtrmtFcltNm?.(matchedDefaultFacility.text); + }, [ + normalizedDefaultFacilityName, + onSwagePrcsFcltChange, + prcsFcltOptions, + selectedSwagePrcsFclt, + setSwtrmtFcltNm, + ]); return (
@@ -153,7 +167,7 @@ export function CtpvSggPrcsFcltSelectBox({ className="w-[160px]" selectClass="rounded-lg bg-white truncate" label="" - selectArray={[defaultOption, ...prcsFcltOptions]} + selectArray={[buildFacilityDefaultOption(normalizedDefaultFacilityName), ...prcsFcltOptions]} value={selectedSwagePrcsFclt} onSelectValue={(val) => { onSwagePrcsFcltChange?.(String(val)) @@ -165,4 +179,4 @@ export function CtpvSggPrcsFcltSelectBox({ )}
); -} \ No newline at end of file +} diff --git a/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md b/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md index 2f18e01e1..5067d4fcc 100644 --- a/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md +++ b/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md @@ -124,3 +124,9 @@ - `useInftwtResultMap`은 row click 직후 mapView를 먼저 확보한 뒤, `oilqPipeCount > 0`이면 검색바 동기화 이벤트에 `suppressMapMove`를 내려 지역 중심 이동을 건너뛰도록 보정했다. - `procSearchBar`는 `suppressMapMove`가 내려오면 시도/시군구/시설 상태만 갱신하고 `moveMapToCenter`는 호출하지 않는다. - 이 보정으로 실측하수량 관로가 있는 케이스는 처음부터 해당 관로 extent로 바로 확대된다. + +## 19. 2026-07-09 분석 대상지역 기본값 보정 + +- `ii/irsetting`, `ii/rdii`의 기본 선택값을 `경기도 / 양평군 / 양평`으로 맞췄다. +- 공통 `CtpvSggPrcsFcltSelectBox`는 mount 시 부모 값을 강제로 `0`으로 되돌리던 초기화 흐름을 제거하고, 시설 목록 로딩 후 `defaultSwtrmtFcltNm` 이름 기준으로 기본 시설을 자동 선택하게 보강했다. +- 이 보정으로 두 분석 화면 진입 직후 `하수처리시설명` select도 `양평` 기준으로 바로 보이도록 정리했다.