From 7f10e20a413534178feb1f264841cb55c1b22471 Mon Sep 17 00:00:00 2001 From: mind-one001 Date: Wed, 7 Oct 2026 15:03:03 +0900 Subject: [PATCH 01/15] =?UTF-8?q?=EC=A0=84=EA=B5=AD=20=EC=A7=80=EB=8F=84?= =?UTF-8?q?=20=EB=B3=B4=EC=9D=B4=EA=B2=8C=20=EC=A4=8C=20=EB=A0=88=EB=B2=A8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(map-layout)/map/model/map-constants.ts | 2 ++ .../(map-layout)/map/ui/map-page-client.tsx | 1 + .../001-fe-SN-남한전체축소-수정결과.md | 31 +++++++++++++++++++ .../business_components/src/ui/map/Gis.js | 3 ++ 4 files changed, 37 insertions(+) create mode 100644 apps/sn/docs/작업계획/SN-관망도전체범위/001-fe-SN-남한전체축소-수정결과.md diff --git a/apps/sn/app/(map-layout)/map/model/map-constants.ts b/apps/sn/app/(map-layout)/map/model/map-constants.ts index c93fd11c5..5e8ff7a81 100644 --- a/apps/sn/app/(map-layout)/map/model/map-constants.ts +++ b/apps/sn/app/(map-layout)/map/model/map-constants.ts @@ -25,6 +25,8 @@ export const EXMN_PROC_SEARCH_EVENT = "SN_INFTWT_PROC_SEARCH"; // 지반침하 지도 포인트와 같은 파란 계열의 기존 레이어 트리 심볼 자산을 재사용한다. export const SINKHOLE_TREE_ICON_NAME = "SN_SINKHOLE_SPOT_BLUE.png"; +// EPSG:5179의 남한·도서 주변 여유 범위 [minX, minY, maxX, maxY]. +// 화면 비율 때문에 전체 범위가 잘리지 않도록 Gis의 viewShowFullExtent와 함께 사용한다. export const SOUTH_KOREA_VIEW_EXTENT: [number, number, number, number] = [ 600000, 1300000, 1500000, 2500000, ]; diff --git a/apps/sn/app/(map-layout)/map/ui/map-page-client.tsx b/apps/sn/app/(map-layout)/map/ui/map-page-client.tsx index fd2f012ef..a5943dede 100644 --- a/apps/sn/app/(map-layout)/map/ui/map-page-client.tsx +++ b/apps/sn/app/(map-layout)/map/ui/map-page-client.tsx @@ -3335,6 +3335,7 @@ const MapPageClient = () => { defaultZoom={7.5} defaultLonLat={[129, 36.3]} viewExtent={SOUTH_KOREA_VIEW_EXTENT} + viewShowFullExtent={true} /> {isAgedPipeListMinimized && (
diff --git a/apps/sn/docs/작업계획/SN-관망도전체범위/001-fe-SN-남한전체축소-수정결과.md b/apps/sn/docs/작업계획/SN-관망도전체범위/001-fe-SN-남한전체축소-수정결과.md new file mode 100644 index 000000000..ae3a6b5e7 --- /dev/null +++ b/apps/sn/docs/작업계획/SN-관망도전체범위/001-fe-SN-남한전체축소-수정결과.md @@ -0,0 +1,31 @@ +# 관망도 남한 전체 축소 표시 + +- 작업일: 2026-10-07 +- 목적: 최소 줌을 낮춰도 지정 영역 제한 때문에 남한 전체까지 축소되지 않는 현상 보완. +- 근거: 사용자 요청. 화면설계서 7번 SN_MN010101의 하수관망도, 요구사항정의서 기능 추적표 97~100행(PR-SN-030)을 확인했다. 해당 원본에는 정확한 줌/좌표 제한값이 없으므로 이번 값은 원본 요구사항으로 주장하지 않는다. + +## 수정 + +| 파일 | 내용 | +| --- | --- | +| app/(map-layout)/map/model/map-constants.ts | SOUTH_KOREA_VIEW_EXTENT의 좌표계·용도 주석 추가. 기존 좌표 유지 | +| app/(map-layout)/map/ui/map-page-client.tsx | Gis에 viewShowFullExtent=true 전달 | +| packages/common/business_components/src/ui/map/Gis.js | 선택 속성을 View.showFullExtent에 연결. 기본 false로 다른 앱의 기존 동작 유지 | + +범위는 EPSG:5179 `[600000, 1300000, 1500000, 2500000]`이다. 이 범위에는 남한이 포함되어 있으나, OpenLayers의 기본 extent 제약은 화면 가로·세로 중 어느 방향도 범위 밖으로 나가지 못하도록 축소를 제한한다. showFullExtent는 지정 영역 전체가 보이도록 한 축의 화면이 범위를 넘는 축소를 허용한다. 영역을 무제한으로 확장하거나 minZoom을 무시하는 설정이 아니다. + +## 검증 + +- 사용자 SN 개발서버 3002(PID 69796)를 유지하고 HMR로 검증했다. 서버 재시작과 Next build는 수행하지 않았다. +- 기존 Chrome 탭 연결은 시간 초과하여 별도 인앱 브라우저 검증 탭을 사용했다. +- localhost:3002에서 승인된 테스트 계정으로 로그인 후 관망도 진입. +- 1280×720 브라우저에서 축소 버튼으로 줌 7.5 → 6.5 → 6.0 이동 확인. +- 줌 6.0에서 남한 본토, 제주도와 울릉도·독도가 함께 보이는 화면을 확인했다. 레이어 패널과 분석도구 패널을 유지한 상태다. +- git diff --check 통과. 기존 배경지도 타일의 모든 외부 네트워크 요청, 전체 업무 API 및 다른 화면 회귀까지 검증한 것은 아니다. + +## 영향 및 롤백 + +- API/DB/레이어 검색·조회조건 변경 없음. 초기 줌 7.5와 중심좌표는 유지한다. +- 다른 앱은 선택 속성을 전달하지 않으므로 기존 기본값 false를 유지한다. +- 롤백은 이번 선택 속성 전달과 View 연결, 설명 주석만 제거한다. 사용자 변경이나 공통 minZoom은 되돌리지 않는다. +- 별도 테스트 파일·캡처 파일을 생성하지 않았다. 최종 문서만 보존하며 검증용 브라우저 탭은 종료한다. diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 823a9b7ca..f7bd71ec6 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -84,6 +84,8 @@ function Gis({ defaultLonLat = [127.4875, 37.4916], viewExtent, viewMinZoom, + // 지정 범위 전체까지 축소를 허용한다. 기본값은 기존 타 앱의 범위 제한을 유지한다. + viewShowFullExtent = false, showZoomLevel = false, toolBarIconSize, zoomGuide, @@ -176,6 +178,7 @@ function Gis({ minZoom: viewMinZoom ?? gisCore.defaults.minZoom, maxZoom: gisCore.defaults.maxZoom, extent: viewExtent, + showFullExtent: viewShowFullExtent, }), }); } else { From fed1383ced5c5793ac2565723f82f3e3ecaab22d Mon Sep 17 00:00:00 2001 From: mind-one001 Date: Thu, 8 Oct 2026 10:30:51 +0900 Subject: [PATCH 02/15] =?UTF-8?q?=EC=8B=9C=EB=8F=84=EB=B3=84=20=EB=85=B8?= =?UTF-8?q?=ED=9B=84=EA=B4=80=EB=A1=9C=ED=98=84=ED=99=A9=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=A1=B0=ED=9A=8C=EA=B8=B0=EA=B0=84=EC=9D=84=20?= =?UTF-8?q?=EB=8B=B9=ED=95=B4=EC=97=B0=EB=8F=84=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aged-pipe-status-by-ctpv-utils.ts | 5 +++-- .../001-qa-SFR-011-001-시도별 노후관로현황-검증기록.md | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/aged-pipe-status-by-ctpv/aged-pipe-status-by-ctpv-utils.ts b/apps/sn/app/(map-layout)/map/components/aged-pipe-status-by-ctpv/aged-pipe-status-by-ctpv-utils.ts index 430e81a73..96f8d3fed 100644 --- a/apps/sn/app/(map-layout)/map/components/aged-pipe-status-by-ctpv/aged-pipe-status-by-ctpv-utils.ts +++ b/apps/sn/app/(map-layout)/map/components/aged-pipe-status-by-ctpv/aged-pipe-status-by-ctpv-utils.ts @@ -66,7 +66,8 @@ export const normalizeAgedPipeStatusByCtpvFilters = ( filters: Partial, ): AgedPipeStatusByCtpvFilters => { const currentYear = new Date().getFullYear(); - let startYear = Math.trunc(Number(filters.startYear ?? currentYear - 2)); + // 최초 진입·초기화는 당해연도 1년 조회로 시작하며 직접 선택한 범위는 유지한다. + let startYear = Math.trunc(Number(filters.startYear ?? currentYear)); let endYear = Math.trunc(Number(filters.endYear ?? currentYear)); let elapsedYears = Math.trunc( Number( @@ -74,7 +75,7 @@ export const normalizeAgedPipeStatusByCtpvFilters = ( ), ); - if (!Number.isFinite(startYear)) startYear = currentYear - 2; + if (!Number.isFinite(startYear)) startYear = currentYear; if (!Number.isFinite(endYear)) endYear = currentYear; if ( !Number.isFinite(elapsedYears) || diff --git a/apps/sn/docs/작업검증/SFR-011-001-시도별 노후관로현황/001-qa-SFR-011-001-시도별 노후관로현황-검증기록.md b/apps/sn/docs/작업검증/SFR-011-001-시도별 노후관로현황/001-qa-SFR-011-001-시도별 노후관로현황-검증기록.md index b11584943..1d7f76e93 100644 --- a/apps/sn/docs/작업검증/SFR-011-001-시도별 노후관로현황/001-qa-SFR-011-001-시도별 노후관로현황-검증기록.md +++ b/apps/sn/docs/작업검증/SFR-011-001-시도별 노후관로현황/001-qa-SFR-011-001-시도별 노후관로현황-검증기록.md @@ -15,3 +15,14 @@ - 순번은 전체 건수 기준 역순으로 계산해 로컬 3002에서 1페이지 18~9, 2페이지 8~1 표출을 확인했다. - 원본 근거는 요구사항정의서 `SFR-011-001`, 화면설계서 8~9번 `SN_MN010102`이며 순번·제목 건수는 원본 필수항목이 아닌 공통 그리드 표시 규칙이다. - API, 조회조건, 연도 그룹 헤더와 Excel 다운로드 형식은 변경하지 않았다. + +## 4. 2026-10-08 기본 검색기간 단년 변경 + +- 사용자 요청으로 디지털하수관로의 시도별 노후관로현황 기본 검색기간을 최근 3년에서 당해연도 1년으로 변경했다. 최초 진입·초기화·잘못된 연도값의 기본 처리가 동일한 정규화 함수를 사용한다. +- 원본 근거: 요구사항정의서 `3. (기능)요구사항정의 및 추적표` 87행 `SFR-011-001`, 화면설계서 슬라이드 9 `SN_MN010102`. 화면설계서의 시작·종료 연도 예시는 모두 2026년이다. +- 수정: `aged-pipe-status-by-ctpv-utils.ts`의 기본 시작연도와 비정상 시작연도 fallback만 당해연도로 변경. 사용자가 선택한 최대 3년 범위, 경과년도 기본 30년, API 계약·DB·지도 기능·Excel 적용조건 처리는 유지한다. +- 로컬 3002 실제 브라우저: 최초 진입 `2026~2026` → 2026년 단일 그룹/10행, 직접 `2024~2026` 선택 후 조회 → 3개 연도 그룹, 초기화 → `2026~2026` 복귀 확인. 해당 API 요청 모두 HTTP 200. +- 기본값·초기화 입력·비정상 연도·명시적 3년 유지·4년 제한 등 6개 확인과 `git diff --check` 통과. 사용자 개발 서버가 실행 중이므로 빌드/서버 재시작은 하지 않았다. 일회성 테스트 파일은 생성하지 않았다. +- 시도 집계 목록의 기존 클라이언트 페이징 예외(18개 고정 집계)는 변경하지 않았다. 지도 호출 영향 없음. Excel은 동일한 마지막 적용조건을 사용하는 기존 코드 유지, 실제 다운로드 재검증은 이번 범위에서 수행하지 않았다. +- 배포는 사용자가 수행한다. 원복은 기본 시작연도 및 fallback을 `currentYear - 2`로 되돌린다. 단년 조건의 성능 통과 여부는 배포 후 재시험 전까지 미확정이다. +- 후속 결과서: 시스템명은 `디지털하수관로`, 표시 메뉴명에는 `SN01` 등 시험 ID를 붙이지 않는다. 사용자가 수정한 HWP를 기준으로 추가 편집하고, 실제 캡처는 마우스 커서가 보이지 않게 준비한다. 기존 측정 원본은 변경하지 않는다. From 791bc20e8e24034f8849ccbc9e6bbc01da576e53 Mon Sep 17 00:00:00 2001 From: ljpark Date: Tue, 13 Oct 2026 15:22:16 +0900 Subject: [PATCH 03/15] =?UTF-8?q?sn:=20=EC=A7=80=EB=8F=84=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=A0=95=EB=B3=B4=EA=B0=80=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20404>>=EC=9C=84=EC=B9=98=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EA=B0=80=20=EC=97=86=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flood-dngr-map-popup-page.tsx | 17 ++++++----- .../001-fe-SN-도시침수지도-빈목록응답-작업카드.md | 28 +++++++++++++++++++ .../lib/use-flood-prediction-map.ts | 6 ++-- .../ui/fldng-predc-prst-popup-content.tsx | 7 ++++- 4 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 apps/sn/docs/작업계획/SN-도시침수지도-정보제공포털-목록보기/001-fe-SN-도시침수지도-빈목록응답-작업카드.md diff --git a/apps/sn/app/(map-layout)/map/components/flood-dngr-map/flood-dngr-map-popup-page.tsx b/apps/sn/app/(map-layout)/map/components/flood-dngr-map/flood-dngr-map-popup-page.tsx index 5474e8fad..52220d34d 100644 --- a/apps/sn/app/(map-layout)/map/components/flood-dngr-map/flood-dngr-map-popup-page.tsx +++ b/apps/sn/app/(map-layout)/map/components/flood-dngr-map/flood-dngr-map-popup-page.tsx @@ -226,7 +226,12 @@ export default function FloodDngrMapPopupPage() { const response = await getFloodDngrMapJson( `/sewerage-service/api/v1/fldng/floodDngrMap/group/extent?${params.toString()}`, ); - const payload = response as FloodDngrMapExtent; + const payload = response as FloodDngrMapExtent | null | undefined; + // 성공 응답의 null·생략된 data는 위치 부재이다. 좌표 접근과 지도 메시지를 중단한다. + if (payload == null) { + setErrorMessage("위치 정보가 없습니다"); + return; + } const extent = [ payload?.minX, payload?.minY, @@ -237,7 +242,7 @@ export default function FloodDngrMapPopupPage() { extent.length !== 4 || extent.some((value) => !Number.isFinite(value)) ) { - throw new Error("해당 도시침수지도 피처의 위치 정보가 없습니다."); + throw new Error("도시침수지도 위치 응답의 좌표가 올바르지 않습니다."); } window.opener?.postMessage( { @@ -253,11 +258,9 @@ export default function FloodDngrMapPopupPage() { } catch (error) { console.error("도시침수지도 위치 이동 중 오류:", error); const message = - error instanceof Error && error.message.includes("404") - ? "해당 도시침수지도 데이터에 위치 정보가 없어 이동할 수 없습니다." - : error instanceof Error - ? error.message - : "도시침수지도 위치 이동 중 오류가 발생했습니다."; + error instanceof Error + ? error.message + : "도시침수지도 위치 이동 중 오류가 발생했습니다."; setErrorMessage(message); } finally { setIsLoading(false); diff --git a/apps/sn/docs/작업계획/SN-도시침수지도-정보제공포털-목록보기/001-fe-SN-도시침수지도-빈목록응답-작업카드.md b/apps/sn/docs/작업계획/SN-도시침수지도-정보제공포털-목록보기/001-fe-SN-도시침수지도-빈목록응답-작업카드.md new file mode 100644 index 000000000..2fdf7daf4 --- /dev/null +++ b/apps/sn/docs/작업계획/SN-도시침수지도-정보제공포털-목록보기/001-fe-SN-도시침수지도-빈목록응답-작업카드.md @@ -0,0 +1,28 @@ +# FE 도시침수지도 빈 결과 안내 작업카드 + +- 작성일: 2026-10-13 / 상태: 구현 완료 / 실서버·브라우저 검증 대기 +- 근거: 사용자 요청. extent 위치 정보가 없으면 `위치 정보가 없습니다`로 안내한다. + +## 변경 계획 + +- 목록 200·items=[]·total=0: 총 0건과 빈 표를 표시하고 이전 행·오류·로딩 상태를 정리한다. +- 단건·그룹 extent 200 응답에 위치 데이터가 없으면 `위치 정보가 없습니다`를 표시하고 지도 이동·선택 강조를 수행하지 않는다. +- 공통 응답에서 data가 null이거나 생략된 경우를 좌표 접근 전에 처리한다. 404 문자열로 위치 부재를 판단하는 기존 분기는 제거한다. +- 위치 데이터가 있으면 기존 지도 이동을 유지한다. 실제 HTTP 오류·잘못된 좌표는 기존 오류 처리로 구분한다. + +## 수정 대상 + +- `app/(map-layout)/map/components/flood-dngr-map/flood-dngr-map-popup-page.tsx`: 목록·그룹 extent·안내 처리. +- sn 내 단건·그룹 extent 호출부(침수예측 현황·지도 포함): 검색으로 사용처를 확정하고 동일한 빈 결과 처리 적용. +- `src/shared/api/sewerage-api.ts`, `http-transport.ts`: null·data 생략 응답의 전달 방식 확인. 전역 오류 치환은 하지 않는다. + +## 검증·영향 + +- 두 extent의 위치 없음: 200, 지정 안내 문구, 지도 이동 없음, null 접근 오류 없음. +- 위치 있음: 기존 이동 정상. 실제 404·401·403·5xx·timeout: 오류 안내 유지. +- 목록은 데이터 있음 → 0건 → 데이터 있음 재검색 시 행·건수·로딩이 일치한다. +- BE 성공 JSON의 data 생략·null 처리를 연동 확인한다. 빈 본문·비정상 응답을 임의로 위치 없음으로 처리하지 않는다. +- 영향은 sn의 extent 사용 화면. UI·DB 변경 없음. 롤백은 실제 수정한 기능 파일 복원. +- 남은 확인: 전체 extent 호출부와 실제 응답 형태·브라우저 동작. + +FE 구현 및 타입 검사·클릭 처리 모의 검증 완료. 로그인 브라우저 검증은 미수행이다. diff --git a/apps/sn/src/features/fldng-predc-prst/lib/use-flood-prediction-map.ts b/apps/sn/src/features/fldng-predc-prst/lib/use-flood-prediction-map.ts index e8f0c6596..9a649e309 100644 --- a/apps/sn/src/features/fldng-predc-prst/lib/use-flood-prediction-map.ts +++ b/apps/sn/src/features/fldng-predc-prst/lib/use-flood-prediction-map.ts @@ -7,7 +7,7 @@ import Overlay from "ol/Overlay.js"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; -import { getRawJson } from "@/shared/api/sewerage-api"; +import { getData } from "@/shared/api/sewerage-api"; import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types"; import { getFloodRiskGradeClassName } from "./flood-risk-grade"; @@ -251,11 +251,11 @@ export const useFloodPredictionMap = ({ removeFloodPredictionOverlay(); try { - const response = await getRawJson( + // 공통 응답의 data만 읽어 null·생략된 값을 빈 오버레이 결과로 처리한다. + const payload = await getData( `/sewerage-service/api/v1/fldng/predc/prst/gridMap?sggCd=${encodeURIComponent(sggCd)}&analysisDateTime=${encodeURIComponent(analysisDateTime)}`, ); if (!navigation?.isCurrent()) return; - const payload = response?.data ?? response; const row = payload && typeof payload === "object" && !Array.isArray(payload) ? (payload as FloodPredictionOverlayRow) diff --git a/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx b/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx index e5d6b98db..9f437b19e 100644 --- a/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx +++ b/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx @@ -367,6 +367,11 @@ export default function FldngPredcPrstPopupContent() { const extentData = await getData( `/sewerage-service/api/v1/fldng/floodDngrMap/group/extent?${params.toString()}`, ); + // null·생략된 data는 정상적인 위치 부재이며 부모 지도 이동·강조를 요청하지 않는다. + if (extentData == null) { + alert("위치 정보가 없습니다"); + return; + } const extent = [ extentData?.minX, extentData?.minY, @@ -374,7 +379,7 @@ export default function FldngPredcPrstPopupContent() { extentData?.maxY, ].map(Number); if (extent.some((value) => !Number.isFinite(value))) { - throw new Error("도시침수지도 위치 정보가 없습니다."); + throw new Error("도시침수지도 위치 응답의 좌표가 올바르지 않습니다."); } window.opener.postMessage( { From c850b9ce1effa227f9dcd1c5b0799b7ec205a7ba Mon Sep 17 00:00:00 2001 From: ljpark Date: Tue, 13 Oct 2026 15:48:24 +0900 Subject: [PATCH 04/15] =?UTF-8?q?sn:=20=ED=95=B4=EB=8B=B9=20=EB=8F=84?= =?UTF-8?q?=EC=8B=9C=EC=B9=A8=EC=88=98=EC=A7=80=EB=8F=84=EA=B0=80=20?= =?UTF-8?q?=EC=97=86=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx b/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx index 9f437b19e..5f213234a 100644 --- a/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx +++ b/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx @@ -369,7 +369,7 @@ export default function FldngPredcPrstPopupContent() { ); // null·생략된 data는 정상적인 위치 부재이며 부모 지도 이동·강조를 요청하지 않는다. if (extentData == null) { - alert("위치 정보가 없습니다"); + alert("해당 도시침수지도가 없습니다."); return; } const extent = [ From 728a66bac2b0d2eb9cf9b4e6b1763b77e5961e67 Mon Sep 17 00:00:00 2001 From: ljpark Date: Tue, 13 Oct 2026 15:55:31 +0900 Subject: [PATCH 05/15] sn: apigateway.onasys.co.kr --- apps/sn/.env.local | 2 +- apps/sn/.env.mindone | 2 +- apps/sn/src/shared/config/env.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sn/.env.local b/apps/sn/.env.local index dce96bf40..256b4daac 100644 --- a/apps/sn/.env.local +++ b/apps/sn/.env.local @@ -11,7 +11,7 @@ NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver #NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&layer=korean_map&style=korean&tilematrixset=EPSG:5179&TileMatrix={z}&TileCol={x}&TileRow={y} NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y} -SERVER_API_URL = http://dev.onasys.co.kr:8000 +SERVER_API_URL = https://apigateway.onasys.co.kr SEWERAGE_API_URL = http://localhost:8883 # SEWERAGE_API_URL = http://con.mind-one.co.kr:11083 DC_URL = https://apigateway.apps.rsonesoft.com diff --git a/apps/sn/.env.mindone b/apps/sn/.env.mindone index 84202b57b..fb477b3bb 100644 --- a/apps/sn/.env.mindone +++ b/apps/sn/.env.mindone @@ -6,7 +6,7 @@ NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11302 NEXT_PUBLIC_SUB_DOMAIN = http://con.mind-one.co.kr:13000 -SERVER_API_URL = http://dev.onasys.co.kr:8000 +SERVER_API_URL = https://apigateway.onasys.co.kr NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr diff --git a/apps/sn/src/shared/config/env.ts b/apps/sn/src/shared/config/env.ts index 788fdf36f..0ec1c73ea 100644 --- a/apps/sn/src/shared/config/env.ts +++ b/apps/sn/src/shared/config/env.ts @@ -1,7 +1,7 @@ export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "SN"; export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; -export const SERVER_API_URL = process.env.SERVER_API_URL||'http://dev.onasys.co.kr:8000'; +export const SERVER_API_URL = process.env.SERVER_API_URL||'https://apigateway.onasys.co.kr'; export const AUTH_LOGIN_REDIRECT_URL = process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login"; From f709dc99f69f177e78d49cb171e0a7519a426702 Mon Sep 17 00:00:00 2001 From: mind-one001 Date: Thu, 15 Oct 2026 11:21:44 +0900 Subject: [PATCH 06/15] chore: stop tracking local VS Code settings --- .gitignore | 3 +- .vscode/launch.json | 88 ------------------------------------------- .vscode/settings.json | 5 --- 3 files changed, 2 insertions(+), 94 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 871b209f0..0e21f20e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # turbo .turbo/ -.vscode/**/* +**/.vscode/ +*.code-workspace # dependencies node_modules/ diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index ea1cd8dc1..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - // IntelliSense를 사용하여 가능한 특성에 대해 알아보세요. - // 기존 특성에 대한 설명을 보려면 가리킵니다. - // 자세한 내용을 보려면 https://go.microsoft.com/fwlink/?linkid=830387을(를) 방문하세요. - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "dev:as 디버그", - "runtimeExecutable": "pnpm", - "runtimeArgs": [ - "run", - "dev:as" - ], - "skipFiles": [ - "/**" - ], - "cwd": "${workspaceFolder}", - "console": "integratedTerminal", - "sourceMaps": true, - "env": { - "NODE_OPTIONS": "--inspect" - } - }, - { - "type": "node", - "request": "launch", - "name": "dev:cs 디버그", - "runtimeExecutable": "pnpm", - "runtimeArgs": [ - "run", - "dev:cs" - ], - "skipFiles": [ - "/**" - ], - "cwd": "${workspaceFolder}", - "console": "integratedTerminal", - "sourceMaps": true, - "env": { - "NODE_OPTIONS": "--inspect" - } - }, - { - "type": "node", - "request": "attach", - "name": "dev:cs 서버 연결 (attach)", - "port": 9229, - "restart": true, - "skipFiles": [ - "/**" - ], - "sourceMaps": true, - "resolveSourceMapLocations": [ - "${workspaceFolder}/apps/cs/**", - "!**/node_modules/**" - ] - }, - { - "type": "chrome", - "request": "launch", - "name": "dev:cs 클라이언트 디버그 (Chrome)", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}/apps/cs", - "sourceMaps": true - }, - { - "type": "node", - "request": "launch", - "name": "dev:st 디버그", - "runtimeExecutable": "pnpm", - "runtimeArgs": [ - "run", - "dev:st" - ], - "skipFiles": [ - "/**" - ], - "cwd": "${workspaceFolder}", - "console": "integratedTerminal", - "sourceMaps": true, - "env": { - "NODE_OPTIONS": "--inspect" - } - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f60bf453a..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "chat.tools.terminal.autoApprove": { - "pnpm": true - } -} \ No newline at end of file From 63c2901ae376b3251803ceb116224ba24d36a128 Mon Sep 17 00:00:00 2001 From: mind-one001 Date: Fri, 16 Oct 2026 10:09:44 +0900 Subject: [PATCH 07/15] feat(sn): allow dredging records without a mapped pipe --- .../src/features/dredging/api/dredging-api.ts | 14 ++-- .../dredging/api/response-adapters.ts | 2 +- .../src/features/dredging/model/disclosure.ts | 2 + .../dredging/model/dredging-list-view.ts | 32 +++++---- .../dredging/model/registration-request.ts | 4 +- apps/sn/src/features/dredging/model/types.ts | 32 +++++---- .../ui/dredging-detail-popup-page.tsx | 71 ++++++++++--------- .../dredging/ui/dredging-list-page.tsx | 34 ++++----- .../dredging/ui/dredging-location-map.tsx | 12 +++- .../dredging/ui/dredging-register-modal.tsx | 21 +++--- .../ui/dredging-registration-body.tsx | 19 ++--- .../window-popup/model/popup-definitions.ts | 2 +- .../window-popup/ui/dredging-popup-record.ts | 10 +-- 13 files changed, 146 insertions(+), 109 deletions(-) create mode 100644 apps/sn/src/features/dredging/model/disclosure.ts diff --git a/apps/sn/src/features/dredging/api/dredging-api.ts b/apps/sn/src/features/dredging/api/dredging-api.ts index 60f8c2fe4..ea8293e97 100644 --- a/apps/sn/src/features/dredging/api/dredging-api.ts +++ b/apps/sn/src/features/dredging/api/dredging-api.ts @@ -1,3 +1,4 @@ +import { DREDGING_EXTERNAL_MODE } from "../model/disclosure"; /** 준설 조회·등록·수정·삭제와 최근접 관로 HTTP 계약을 담당한다. */ import { deleteData, @@ -110,12 +111,13 @@ export const fetchDredgingRecords = async (params: { /** 등록된 준설사진의 공통 첨부파일 참조를 조회한다. */ export const fetchDredgingPhotos = async ( dredgingJobSn: string | number, - pipeFid: string, + pipeFid: string | null, + targetSn?: number, ): Promise => { if (!isDredgingBackendEnabled) return []; try { const body = await getRawJson( - `v1/dredging/records/${encodeURIComponent(dredgingJobSn)}/photos?fid=${encodeURIComponent(pipeFid)}`, + `v1/dredging/records/${encodeURIComponent(dredgingJobSn)}/photos?${targetSn ? `targetSn=${targetSn}` : `fid=${encodeURIComponent(pipeFid ?? "")}`}`, ); return toDredgingPhotos(body); } catch (error) { @@ -137,18 +139,18 @@ export const updateDredgingRecord = async ( try { await putData(`v1/dredging/records/${encodeURIComponent(dredgingJobSn)}`, { dredgingDate: update.dredgingDate, - pipeFid: update.originalPipeFid, + targetSn: update.targetSn, registrationMode: update.registrationMode, location: { source: update.locationSource, longitude: update.location.longitude, latitude: update.location.latitude, }, - expectedPipe: { + expectedPipe: update.pipe && !DREDGING_EXTERNAL_MODE ? { fid: update.pipe.fid, mngInstCd: update.pipe.mngInstCd, fcltMngNo: update.pipe.fcltMngNo, - }, + } : null, retainedPhotoSns: update.retainedPhotoSns, newPhotos: update.newPhotos.map((photo) => { const attachment = attachments.get(photo.key); @@ -242,7 +244,7 @@ const mockPipeForLocations = (locations: LocationRequest[]): PipeMatch => { export const findNearestPipeMatches = async ( locations: LocationRequest[], ): Promise> => { - if (locations.length === 0) return new Map(); + if (DREDGING_EXTERNAL_MODE || locations.length === 0) return new Map(); if (!isDredgingBackendEnabled) { const mockPipe = mockPipeForLocations(locations); diff --git a/apps/sn/src/features/dredging/api/response-adapters.ts b/apps/sn/src/features/dredging/api/response-adapters.ts index c3b23b48f..df9bbf4f2 100644 --- a/apps/sn/src/features/dredging/api/response-adapters.ts +++ b/apps/sn/src/features/dredging/api/response-adapters.ts @@ -55,7 +55,7 @@ export const toNearestPipeMatches = (body: any): Map => { for (const item of items ?? []) { if (!item.selected) { - failureMessages.push(nearestPipeFailureMessage(item)); + if (item.failureCode !== "PIPE_NOT_FOUND") failureMessages.push(nearestPipeFailureMessage(item)); continue; } matches.set(item.locationKey, { diff --git a/apps/sn/src/features/dredging/model/disclosure.ts b/apps/sn/src/features/dredging/model/disclosure.ts new file mode 100644 index 000000000..fa20becf3 --- /dev/null +++ b/apps/sn/src/features/dredging/model/disclosure.ts @@ -0,0 +1,2 @@ +/** 외부용 앱은 관망 정보를 요청하거나 화면에 표시하지 않는다. */ +export const DREDGING_EXTERNAL_MODE = false; diff --git a/apps/sn/src/features/dredging/model/dredging-list-view.ts b/apps/sn/src/features/dredging/model/dredging-list-view.ts index f75820b67..f2a7f45d7 100644 --- a/apps/sn/src/features/dredging/model/dredging-list-view.ts +++ b/apps/sn/src/features/dredging/model/dredging-list-view.ts @@ -1,3 +1,4 @@ +import { DREDGING_EXTERNAL_MODE } from "./disclosure"; /** 준설 목록·집계 표의 컬럼·표시 변환. 서버 조회/React 상태와 분리한다. */ import type { DredgingRecord } from "./types"; import { MS_SWAGE_PPL_CODES } from "@/entities/facility/model/facility-code-fallbacks"; @@ -103,6 +104,8 @@ export const buildDredgingPopupParams = (record: DredgingRecord) => ({ sourcePath: window.location.pathname, id: record.id, dredgingJobSn: record.dredgingJobSn, + targetSn: record.targetSn, + mappingStatus: record.mappingStatus, pipeFid: record.pipeFid, mngInstCd: record.mngInstCd, ctpv: record.ctpv, @@ -140,7 +143,8 @@ export type TableHeadDefinition = { align?: "left" | "center" | "right"; }; -export const municipalityHeadlist: TableHeadDefinition[] = [ +const fullMunicipalityHeadlist: TableHeadDefinition[] = [ + { id: "mappingStatusLabel", title: "관로 지정" }, { id: "rowNumber", title: "순번" }, { id: "ctpv", title: "시도" }, { id: "sgg", title: "시군구" }, @@ -156,6 +160,10 @@ export const municipalityHeadlist: TableHeadDefinition[] = [ { id: "registeredDate", title: "등록일자" }, ]; +export const municipalityHeadlist = DREDGING_EXTERNAL_MODE + ? fullMunicipalityHeadlist.filter(column => ["rowNumber", "ctpv", "sgg", "dredgingDate", "photoCount", "registeredDate"].includes(column.id)) + : fullMunicipalityHeadlist; + export const diameterHeadlist: TableHeadDefinition[] = [ { id: "ctpv", title: "시도" }, { id: "totalLengthKm", title: "관로연장(km)", align: "right" }, @@ -202,33 +210,33 @@ export const buildDiameterRows = (records: DredgingRecord[]): TableRow[] => roundKm( items .filter(predicate) - .reduce((sum, record) => sum + record.pipeLengthM, 0), + .reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0), ); return { ctpv, totalLengthKm: roundKm( - items.reduce((sum, record) => sum + record.pipeLengthM, 0), + items.reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0), ), - diameter450: sumLength((record) => record.pipeDiameterMm <= 450), + diameter450: sumLength((record) => (record.pipeDiameterMm ?? 0) <= 450), diameter600: sumLength( - (record) => record.pipeDiameterMm > 450 && record.pipeDiameterMm <= 600, + (record) => (record.pipeDiameterMm ?? 0) > 450 && (record.pipeDiameterMm ?? 0) <= 600, ), diameter700: sumLength( - (record) => record.pipeDiameterMm > 600 && record.pipeDiameterMm <= 700, + (record) => (record.pipeDiameterMm ?? 0) > 600 && (record.pipeDiameterMm ?? 0) <= 700, ), diameter900: sumLength( - (record) => record.pipeDiameterMm > 700 && record.pipeDiameterMm <= 900, + (record) => (record.pipeDiameterMm ?? 0) > 700 && (record.pipeDiameterMm ?? 0) <= 900, ), diameter1000: sumLength( (record) => - record.pipeDiameterMm > 900 && record.pipeDiameterMm <= 1000, + (record.pipeDiameterMm ?? 0) > 900 && (record.pipeDiameterMm ?? 0) <= 1000, ), diameter1200: sumLength( (record) => - record.pipeDiameterMm > 1000 && record.pipeDiameterMm <= 1200, + (record.pipeDiameterMm ?? 0) > 1000 && (record.pipeDiameterMm ?? 0) <= 1200, ), - diameter1500: sumLength((record) => record.pipeDiameterMm >= 1500), + diameter1500: sumLength((record) => (record.pipeDiameterMm ?? 0) >= 1500), recordIds: items.map((record) => record.id), }; }); @@ -239,7 +247,7 @@ export const buildElapsedYearRows = (records: DredgingRecord[]): TableRow[] => roundKm( items .filter(predicate) - .reduce((sum, record) => sum + record.pipeLengthM, 0), + .reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0), ); const elapsedYears = (record: DredgingRecord) => record.installationYear === null @@ -249,7 +257,7 @@ export const buildElapsedYearRows = (records: DredgingRecord[]): TableRow[] => return { ctpv, totalLengthKm: roundKm( - items.reduce((sum, record) => sum + record.pipeLengthM, 0), + items.reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0), ), under10: sumLength((record) => { const elapsed = elapsedYears(record); diff --git a/apps/sn/src/features/dredging/model/registration-request.ts b/apps/sn/src/features/dredging/model/registration-request.ts index b24813293..fbb906e09 100644 --- a/apps/sn/src/features/dredging/model/registration-request.ts +++ b/apps/sn/src/features/dredging/model/registration-request.ts @@ -19,11 +19,11 @@ const buildRegistrationBody = ( longitude: group.location.longitude, latitude: group.location.latitude, }, - expectedPipe: { + expectedPipe: group.pipe ? { fid: group.pipe.fid, mngInstCd: group.pipe.mngInstCd, fcltMngNo: group.pipe.fcltMngNo, - }, + } : null, photos: group.photos.map((photo) => { const attachment = attachments.get(photo.key); if (!attachment && requireAttachments) { diff --git a/apps/sn/src/features/dredging/model/types.ts b/apps/sn/src/features/dredging/model/types.ts index 8fbe9d815..fedc48178 100644 --- a/apps/sn/src/features/dredging/model/types.ts +++ b/apps/sn/src/features/dredging/model/types.ts @@ -59,32 +59,35 @@ export type DredgingDraftGroup = { export type DredgingRecord = { id: string; dredgingJobSn: number | string; - pipeFid: string; - mngInstCd: string; + targetSn?: number; + mappingStatus?: "AUTO" | "UNMAPPED"; + pipeFid: string | null; + mngInstCd: string | null; ctpv: string; sgg: string; - treatmentZone: string; - fcltMngNo: string; - pipeDiameterMm: number; - pipeLengthM: number; - pipeMaterial: string; + treatmentZone: string | null; + fcltMngNo: string | null; + pipeDiameterMm: number | null; + pipeLengthM: number | null; + pipeMaterial: string | null; installationYear: number | null; dredgingDate: string; photoCount: number; registeredDate: string; longitude: number; latitude: number; - snappedLongitude: number; - snappedLatitude: number; + snappedLongitude: number | null; + snappedLatitude: number | null; registrationMode: DredgingRegistrationMode; registrationSource: DredgingLocationSource; - matchDistanceMeters: number; + matchDistanceMeters: number | null; isMock: boolean; }; export type DredgingPhoto = { + targetSn?: number; photoSn: number; - pipeFid: string; + pipeFid?: string | null; atflCd: string; atflId: string; originalFileName: string; @@ -97,18 +100,19 @@ export type DredgingSaveGroup = { dredgingDate: string; location: GeoPoint; locationSource: DredgingLocationSource; - pipe: PipeMatch; + pipe: PipeMatch | null; photos: DredgingPhotoDraft[]; }; /** 상세 기반 수정 팝업에서 선택한 한 관로의 위치·사진을 함께 갱신한다. */ export type DredgingRecordUpdate = { dredgingDate: string; - originalPipeFid: string; + originalPipeFid: string | null; + targetSn: number; registrationMode: DredgingRegistrationMode; location: GeoPoint; locationSource: DredgingLocationSource; - pipe: PipeMatch; + pipe: PipeMatch | null; retainedPhotoSns: number[]; newPhotos: DredgingPhotoDraft[]; }; diff --git a/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx b/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx index 5223ffcb8..9bad04c3a 100644 --- a/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx +++ b/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx @@ -1,4 +1,5 @@ "use client"; +import { DREDGING_EXTERNAL_MODE } from "../model/disclosure"; import dynamic from "next/dynamic"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; @@ -134,25 +135,25 @@ const DredgingDetailPopupPage = ({ const [statusMessage, setStatusMessage] = useState(""); const uploadedAttachmentsRef = useRef(new Map()); - const initialMatchedPipe = useMemo( - () => ({ + const initialMatchedPipe = useMemo( + () => !record.pipeFid || DREDGING_EXTERNAL_MODE ? null : ({ fid: record.pipeFid, - mngInstCd: record.mngInstCd, - fcltMngNo: record.fcltMngNo, - pipeDiameterMm: record.pipeDiameterMm, - pipeLengthM: record.pipeLengthM, - pipeMaterial: record.pipeMaterial, + mngInstCd: record.mngInstCd ?? "", + fcltMngNo: record.fcltMngNo ?? "", + pipeDiameterMm: record.pipeDiameterMm ?? 0, + pipeLengthM: record.pipeLengthM ?? 0, + pipeMaterial: record.pipeMaterial ?? "", installationYear: record.installationYear, swtrmtZoneFid: null, - swtrmtZoneNm: record.treatmentZone, - distanceMeters: record.matchDistanceMeters, + swtrmtZoneNm: record.treatmentZone ?? "", + distanceMeters: record.matchDistanceMeters ?? 0, snappedLongitude: record.snappedLongitude || record.longitude, snappedLatitude: record.snappedLatitude || record.latitude, isMock: record.isMock, }), [record], ); - const [matchedPipe, setMatchedPipe] = useState(initialMatchedPipe); + const [matchedPipe, setMatchedPipe] = useState(initialMatchedPipe); useEffect(() => { let cancelled = false; @@ -168,6 +169,7 @@ const DredgingDetailPopupPage = ({ const items = await fetchDredgingPhotos( record.dredgingJobSn, record.pipeFid, + record.targetSn, ); const previews = await Promise.all( items.map(async (photo): Promise => { @@ -214,7 +216,7 @@ const DredgingDetailPopupPage = ({ cancelled = true; createdUrls.forEach((url) => URL.revokeObjectURL(url)); }; - }, [record.dredgingJobSn, record.pipeFid]); + }, [record.dredgingJobSn, record.pipeFid, record.targetSn]); useEffect( () => () => { @@ -258,19 +260,22 @@ const DredgingDetailPopupPage = ({ } }; - const pipeInfo = [ + const pipeInfo = DREDGING_EXTERNAL_MODE ? [ + ["시도", record.ctpv || "확인불가"], ["시군구", record.sgg || "확인불가"], + ["위치등록", locationSourceLabels[locationSource] || locationSource], + ] : [ ["시도", record.ctpv || "확인불가"], ["시군구", record.sgg || "확인불가"], [ "처리구역", - matchedPipe.swtrmtZoneNm || record.treatmentZone || "확인불가", + matchedPipe?.swtrmtZoneNm || record.treatmentZone || "확인불가", ], - ["관경", `${matchedPipe.pipeDiameterMm.toLocaleString()} mm`], - ["관로연장", `${matchedPipe.pipeLengthM.toLocaleString()} m`], - ["관재질", resolvePipeMaterialName(matchedPipe.pipeMaterial)], + ["관경", matchedPipe ? `${matchedPipe.pipeDiameterMm.toLocaleString()} mm` : "미산정"], + ["관로연장", matchedPipe ? `${matchedPipe.pipeLengthM.toLocaleString()} m` : "미산정"], + ["관재질", matchedPipe ? resolvePipeMaterialName(matchedPipe.pipeMaterial) : "미확인"], [ "매설년도", - matchedPipe.installationYear + matchedPipe?.installationYear ? `${matchedPipe.installationYear}년` : "확인불가", ], @@ -280,7 +285,7 @@ const DredgingDetailPopupPage = ({ { length: Math.ceil(pipeInfo.length / 2) }, (_, index) => pipeInfo.slice(index * 2, index * 2 + 2), ); - const automaticMappingValue = `${matchedPipe.distanceMeters.toFixed(2)} m`; + const automaticMappingValue = DREDGING_EXTERNAL_MODE ? "위치 등록" : matchedPipe ? `${matchedPipe.distanceMeters.toFixed(2)} m` : "관로 미지정"; const totalPhotoCount = photos.length + newPhotos.length; const handleNewFiles = async (files: File[] | null) => { @@ -407,17 +412,12 @@ const DredgingDetailPopupPage = ({ { locationKey, ...locationDraft }, ]); const nextPipe = matches.get(locationKey); - if (!nextPipe) { - throw new Error( - "50m 이내에 관로가 없습니다. 지도에서 준설위치를 다시 확인해주세요.", - ); - } setLocation(locationDraft); setLocationSource("MANUAL"); - setMatchedPipe(nextPipe); + setMatchedPipe(nextPipe ?? null); setLocationDraft(null); setIsEditingLocation(false); - setStatusMessage("변경한 위치와 최근접 관로를 확인했습니다."); + setStatusMessage(DREDGING_EXTERNAL_MODE ? "변경한 준설위치를 적용했습니다." : nextPipe ? "변경한 위치와 최근접 관로를 확인했습니다." : "관로 미지정으로 저장할 수 있습니다."); } catch (matchFailure) { setMatchError( matchFailure instanceof Error @@ -449,6 +449,8 @@ const DredgingDetailPopupPage = ({ alert("선택한 지도 위치를 먼저 적용해주세요."); return; } + if (!record.targetSn) { alert("화면을 새로고침해 준설대상번호를 확인해주세요."); return; } + if (!DREDGING_EXTERNAL_MODE && !matchedPipe && !(await confirm("관로 미지정 상태로 수정 내용을 저장하시겠습니까?"))) return; setIsSaving(true); try { setStatusMessage( @@ -466,6 +468,7 @@ const DredgingDetailPopupPage = ({ { dredgingDate, originalPipeFid: record.pipeFid, + targetSn: record.targetSn, registrationMode, location, locationSource: @@ -524,9 +527,9 @@ const DredgingDetailPopupPage = ({
diff --git a/apps/sn/src/features/dredging/ui/dredging-location-map.tsx b/apps/sn/src/features/dredging/ui/dredging-location-map.tsx index 3473d513d..f3c4a377b 100644 --- a/apps/sn/src/features/dredging/ui/dredging-location-map.tsx +++ b/apps/sn/src/features/dredging/ui/dredging-location-map.tsx @@ -1,4 +1,5 @@ "use client"; +import { DREDGING_EXTERNAL_MODE } from "../model/disclosure"; import Feature from "ol/Feature.js"; import type { FeatureLike } from "ol/Feature.js"; @@ -265,6 +266,12 @@ const LocationInteraction = ({ previousValueRef.current = value; }, [gisMap, interactive, value?.latitude, value?.longitude]); + if (DREDGING_EXTERNAL_MODE) { + return
+ {interactive ? "지도를 클릭해 준설위치를 선택하세요." : "등록된 준설위치를 확인합니다."} +
; + } + const pipeStatusText = (() => { if (pipeLayerSettings.length === 0) return "관망도 레이어 설정을 불러오는 중입니다."; @@ -329,6 +336,7 @@ const DredgingLocationMap = ({ ); useEffect(() => { + if (DREDGING_EXTERNAL_MODE) return; // 외부에서는 관망 레이어 설정과 도형을 요청하지 않는다. let cancelled = false; const load = async () => { try { @@ -370,8 +378,8 @@ const DredgingLocationMap = ({ value={value} onChange={onChange} interactive={interactive} - matchedPipe={matchedPipe} - pipeLayerSettings={pipeLayerSettings} + matchedPipe={DREDGING_EXTERNAL_MODE ? null : matchedPipe} + pipeLayerSettings={DREDGING_EXTERNAL_MODE ? [] : pipeLayerSettings} /> } defaultZoom={value ? 17 : 7.5} diff --git a/apps/sn/src/features/dredging/ui/dredging-register-modal.tsx b/apps/sn/src/features/dredging/ui/dredging-register-modal.tsx index 34146bdb9..da2b96a96 100644 --- a/apps/sn/src/features/dredging/ui/dredging-register-modal.tsx +++ b/apps/sn/src/features/dredging/ui/dredging-register-modal.tsx @@ -1,8 +1,10 @@ +"use client"; +import { DREDGING_EXTERNAL_MODE } from "../model/disclosure"; /** * 화면명: 준설실적 등록 * 준설 사진을 등록하고 촬영정보·위치·인접 관로를 확인하여 준설실적을 저장하는 화면이다. */ -"use client"; + import dynamic from "next/dynamic"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; @@ -196,7 +198,9 @@ const DredgingRegisterModal = ({ try { const matches = await findNearestPipeMatches(locations); if (matches.size !== locations.length) { - throw new Error("탐색거리 안에서 관로를 찾지 못한 위치가 있습니다."); + commitGroups(current => current.map(item => item.id === group.id + ? { ...item, matchedPipe: null, isMatching: false, matchError: DREDGING_EXTERNAL_MODE ? null : "관로 미지정으로 저장할 수 있습니다." } : item)); + return; } const matchedPipes = Array.from(matches.values()); @@ -453,18 +457,15 @@ const DredgingRegisterModal = ({ if (!location) { throw new Error(`${label}: 지도에서 준설위치를 등록해주세요.`); } - if (!group.matchedPipe) { - throw new Error( - `${label}: ${group.matchError ?? "최근접 관로 자동 매핑을 완료해주세요."}`, - ); - } + if (group.matchError?.startsWith("사진 위치가 서로 다른 관로")) throw new Error(group.matchError); + if (group.isMatching) throw new Error(`${label}: 위치 확인이 끝난 뒤 저장해주세요.`); saveGroups.push({ localId: group.id, registrationMode: group.registrationMode, dredgingDate: representativeDredgingDate, location, locationSource: group.manualLocation ? "MANUAL" : "EXIF", - pipe: group.matchedPipe, + pipe: DREDGING_EXTERNAL_MODE ? null : group.matchedPipe, photos: group.photos, }); } @@ -476,6 +477,8 @@ const DredgingRegisterModal = ({ if (savingRef.current) return; try { const saveGroups = validateAndBuildSaveGroups(); + if (!DREDGING_EXTERNAL_MODE && saveGroups.some(group => !group.pipe) + && !(await confirm("관로가 지정되지 않은 대상이 있습니다. 사진과 위치를 저장하시겠습니까?"))) return; savingRef.current = true; setIsSaving(true); setStatusMessage( @@ -491,7 +494,7 @@ const DredgingRegisterModal = ({ uploadedAttachmentsRef.current = attachments; setStatusMessage( isDredgingBackendEnabled - ? "준설 결과와 관로 매핑을 저장하고 있습니다." + ? (DREDGING_EXTERNAL_MODE ? "준설 결과를 저장하고 있습니다." : "준설 결과와 관로 매핑을 저장하고 있습니다.") : "준설 결과 등록을 완료했습니다.", ); const session = diff --git a/apps/sn/src/features/dredging/ui/dredging-registration-body.tsx b/apps/sn/src/features/dredging/ui/dredging-registration-body.tsx index 4a2c93b2d..fa6956697 100644 --- a/apps/sn/src/features/dredging/ui/dredging-registration-body.tsx +++ b/apps/sn/src/features/dredging/ui/dredging-registration-body.tsx @@ -1,4 +1,6 @@ "use client"; + +import { DREDGING_EXTERNAL_MODE } from "../model/disclosure"; import type { ComponentType } from "react"; import { FileUpload } from "@common/business_components/ui/file-upload"; import { @@ -96,7 +98,7 @@ export const DredgingRegistrationBody = ({ >
등록 안내 - 관로별 사진은 최대 5장까지 등록할 수 있습니다. + {DREDGING_EXTERNAL_MODE ? "등록 건별" : "관로별"} 사진은 최대 5장까지 등록할 수 있습니다. 모든 등록 건은 하나의 준설작업이며 동일한 준설일자를 사용합니다. @@ -114,7 +116,7 @@ export const DredgingRegistrationBody = ({

등록 정보

@@ -151,7 +153,7 @@ export const DredgingRegistrationBody = ({ - 등록 관로 + {DREDGING_EXTERNAL_MODE ? "등록 대상" : "등록 관로"}
-

위치 및 관로

+

{DREDGING_EXTERNAL_MODE ? "준설 위치" : "위치 및 관로"}

{isEditingActiveLocation ? ( <> @@ -446,8 +448,9 @@ export const DredgingRegistrationBody = ({ + {!DREDGING_EXTERNAL_MODE && ( - 자동 연결 관로 + 자동 연결 관로 {activeGroup.isMatching @@ -473,7 +476,7 @@ export const DredgingRegistrationBody = ({ "위치를 지정하면 50m 이내 최근접 관로를 자동으로 연결합니다."} - + )}
@@ -484,7 +487,7 @@ export const DredgingRegistrationBody = ({ data-testid="dredging-registration-status" > {statusMessage || - "사진·준설일자·위치와 자동 연결 관로를 확인해주세요."} + (DREDGING_EXTERNAL_MODE ? "사진·준설일자·준설위치를 확인해주세요." : "사진·준설일자·위치를 확인해주세요. 관로 미지정 상태도 저장할 수 있습니다.")}
@@ -504,7 +507,7 @@ export const DredgingRegistrationBody = ({ statusText={ isEditingActiveLocation ? hasUnappliedLocationDraft - ? "새 위치를 선택했습니다. 관로 다시 찾기를 눌러 적용해주세요." + ? (DREDGING_EXTERNAL_MODE ? "새 위치를 선택했습니다. 선택 위치 적용을 눌러주세요." : "새 위치를 선택했습니다. 관로 다시 찾기를 눌러 적용해주세요.") : "지도에서 새 준설위치를 클릭해주세요." : activeResolvedLocation ? "등록된 준설위치를 표시하고 있습니다." diff --git a/apps/sn/src/features/window-popup/model/popup-definitions.ts b/apps/sn/src/features/window-popup/model/popup-definitions.ts index 6aace497f..e29bad85f 100644 --- a/apps/sn/src/features/window-popup/model/popup-definitions.ts +++ b/apps/sn/src/features/window-popup/model/popup-definitions.ts @@ -39,7 +39,7 @@ export const windowPopupDefinitions = { title: "준설결과 수정", width: 1280, height: 900, - requiredParams: ["dredgingJobSn", "pipeFid", "longitude", "latitude"], + requiredParams: ["dredgingJobSn", "targetSn", "longitude", "latitude"], }, exmnPipeReport: { key: "exmnPipeReport", diff --git a/apps/sn/src/features/window-popup/ui/dredging-popup-record.ts b/apps/sn/src/features/window-popup/ui/dredging-popup-record.ts index 43061f6f7..9706786f8 100644 --- a/apps/sn/src/features/window-popup/ui/dredging-popup-record.ts +++ b/apps/sn/src/features/window-popup/ui/dredging-popup-record.ts @@ -23,14 +23,16 @@ export const buildDredgingPopupRecord = ( ): DredgingRecord => ({ id: getParam(searchParams.id), dredgingJobSn: getParam(searchParams.dredgingJobSn), - pipeFid: getParam(searchParams.pipeFid), + targetSn: getNumber(searchParams.targetSn), + mappingStatus: getParam(searchParams.mappingStatus) === "UNMAPPED" ? "UNMAPPED" : "AUTO", + pipeFid: getParam(searchParams.pipeFid) || null, mngInstCd: getParam(searchParams.mngInstCd), ctpv: getParam(searchParams.ctpv), sgg: getParam(searchParams.sgg), treatmentZone: getParam(searchParams.treatmentZone), fcltMngNo: getParam(searchParams.fcltMngNo), - pipeDiameterMm: getNumber(searchParams.pipeDiameterMm), - pipeLengthM: getNumber(searchParams.pipeLengthM), + pipeDiameterMm: getNullableNumber(searchParams.pipeDiameterMm), + pipeLengthM: getNullableNumber(searchParams.pipeLengthM), pipeMaterial: getParam(searchParams.pipeMaterial), installationYear: getNullableNumber(searchParams.installationYear), dredgingDate: getParam(searchParams.dredgingDate), @@ -44,6 +46,6 @@ export const buildDredgingPopupRecord = ( getParam(searchParams.registrationMode) === "MANUAL" ? "MANUAL" : "PHOTO", registrationSource: getParam(searchParams.registrationSource) === "MANUAL" ? "MANUAL" : "EXIF", - matchDistanceMeters: getNumber(searchParams.matchDistanceMeters), + matchDistanceMeters: getNullableNumber(searchParams.matchDistanceMeters), isMock: getParam(searchParams.isMock) === "true", }); From f5b6effeb3a4eb1e76cbb643481a65e4a3d65ef0 Mon Sep 17 00:00:00 2001 From: mind-one001 Date: Fri, 16 Oct 2026 11:00:13 +0900 Subject: [PATCH 08/15] =?UTF-8?q?=EC=A4=80=EC=84=A4=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A1=9C=20=EB=AF=B8=ED=91=9C=EC=B6=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=EA=B8=B0=EB=8A=A5=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/dredging-detail-popup-page.tsx | 33 ++++------------ .../ui/dredging-registration-body.tsx | 39 ++++--------------- .../dredging-registration-mode-selector.tsx | 38 ++++++++++++++++++ 3 files changed, 53 insertions(+), 57 deletions(-) create mode 100644 apps/sn/src/features/dredging/ui/dredging-registration-mode-selector.tsx diff --git a/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx b/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx index 9bad04c3a..73558eab8 100644 --- a/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx +++ b/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx @@ -1,4 +1,5 @@ "use client"; +import { DredgingRegistrationModeSelector } from "./dredging-registration-mode-selector"; import { DREDGING_EXTERNAL_MODE } from "../model/disclosure"; import dynamic from "next/dynamic"; @@ -607,34 +608,16 @@ const DredgingDetailPopupPage = ({ className="card card-border !m-0 flex !min-h-[40rem] flex-1 flex-col overflow-hidden !p-0" data-testid="dredging-detail-photo-panel" > -
+

준설 사진

{editable ? ( - <> - - - + void changeRegistrationMode(mode)} + testIdPrefix="dredging-edit" + /> ) : null} {totalPhotoCount}/5장 diff --git a/apps/sn/src/features/dredging/ui/dredging-registration-body.tsx b/apps/sn/src/features/dredging/ui/dredging-registration-body.tsx index fa6956697..f970b5ce0 100644 --- a/apps/sn/src/features/dredging/ui/dredging-registration-body.tsx +++ b/apps/sn/src/features/dredging/ui/dredging-registration-body.tsx @@ -1,5 +1,6 @@ "use client"; +import { DredgingRegistrationModeSelector } from "./dredging-registration-mode-selector"; import { DREDGING_EXTERNAL_MODE } from "../model/disclosure"; import type { ComponentType } from "react"; import { FileUpload } from "@common/business_components/ui/file-upload"; @@ -239,38 +240,12 @@ export const DredgingRegistrationBody = ({ 촬영일과 GPS 정보를 확인합니다.

-
- - -
+ void changeRegistrationMode(activeGroup.id, mode)} + testIdPrefix="dredging-register" + />
{activeGroup.registrationMode === "PHOTO" ? ( diff --git a/apps/sn/src/features/dredging/ui/dredging-registration-mode-selector.tsx b/apps/sn/src/features/dredging/ui/dredging-registration-mode-selector.tsx new file mode 100644 index 000000000..758fff42c --- /dev/null +++ b/apps/sn/src/features/dredging/ui/dredging-registration-mode-selector.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useId } from "react"; +import type { DredgingRegistrationMode } from "../model/types"; + +type Props = { + value: DredgingRegistrationMode; + labels: Record; + onChange: (mode: DredgingRegistrationMode) => void; + testIdPrefix: string; +}; + +/** 업로드 실행 버튼과 구분되는 등록 방식 선택. 기존 전환 처리는 호출 화면이 담당한다. */ +export const DredgingRegistrationModeSelector = ({ + value, labels, onChange, testIdPrefix, +}: Props) => { + const name = useId(); + + return ( +
+ 등록 방식 + {(["PHOTO", "MANUAL"] as const).map((mode) => ( + + ))} +
+ ); +}; From fb82cadc82abbdb0ebd5caa0c7f08c17e14c0640 Mon Sep 17 00:00:00 2001 From: mind-one001 Date: Fri, 16 Oct 2026 11:10:47 +0900 Subject: [PATCH 09/15] =?UTF-8?q?=EC=A4=80=EC=84=A4=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/dredging/ui/dredging-detail-popup-page.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx b/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx index 73558eab8..fac29f542 100644 --- a/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx +++ b/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx @@ -529,9 +529,11 @@ const DredgingDetailPopupPage = ({

{DREDGING_EXTERNAL_MODE ? "준설 정보" : "관로 정보"}

- - {DREDGING_EXTERNAL_MODE ? `준설대상 ${record.targetSn ?? ""}` : `관로번호 ${matchedPipe?.fcltMngNo || "관로 미지정"}`} - + {!DREDGING_EXTERNAL_MODE && ( + + {`관로번호 ${matchedPipe?.fcltMngNo || "관로 미지정"}`} + + )}
From fe501965bf1490ba16360270a85c84880b951a1d Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 16 Oct 2026 16:51:47 +0900 Subject: [PATCH 10/15] =?UTF-8?q?sn:=20=EC=A1=B0=EC=82=AC=EA=B4=80?= =?UTF-8?q?=EB=A6=AC>>=EC=A1=B0=EC=82=AC=EC=82=AC=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(map-layout)/map/components/exmn/exmn-map-types.ts | 2 +- apps/sn/app/(map-layout)/map/lib/right-panel-features.ts | 2 +- apps/sn/app/(map-layout)/map/ui/map-page-client.tsx | 2 +- apps/sn/src/features/exmn/lib/exmn-business-validation.ts | 4 ++-- apps/sn/src/features/exmn/ui/exmn-registration-page.tsx | 6 +++--- apps/sn/src/features/exmn/ui/exmn-standard-data-table.tsx | 2 +- apps/sn/src/features/exmn/ui/trtdst-management-page.tsx | 2 +- .../facility-detail/ui/feature-click-event-popup.tsx | 2 +- apps/sn/src/features/facility-detail/ui/ms-swage-ppl.tsx | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/exmn/exmn-map-types.ts b/apps/sn/app/(map-layout)/map/components/exmn/exmn-map-types.ts index fdfca5390..2425d856a 100644 --- a/apps/sn/app/(map-layout)/map/components/exmn/exmn-map-types.ts +++ b/apps/sn/app/(map-layout)/map/components/exmn/exmn-map-types.ts @@ -127,7 +127,7 @@ const normalizeControlNode = ( ...node, lyrSn: nodeId, id: nodeId, - // DB에 저장된 과거 명칭과 무관하게 조사관리 표준 화면 명칭으로 표시한다. + // DB에 저장된 과거 명칭과 무관하게 조사사업 표준 화면 명칭으로 표시한다. lyrNm: fallbackName, upperLyrSn: upperId, upId: upperId, diff --git a/apps/sn/app/(map-layout)/map/lib/right-panel-features.ts b/apps/sn/app/(map-layout)/map/lib/right-panel-features.ts index 5413de7ef..95199ddfc 100644 --- a/apps/sn/app/(map-layout)/map/lib/right-panel-features.ts +++ b/apps/sn/app/(map-layout)/map/lib/right-panel-features.ts @@ -104,7 +104,7 @@ export const getRightPanelFeatureDisplayLabel = ( ? "관로조사" : jobType === "MNHL" ? "맨홀조사" - : "조사관리"; + : "조사사업"; return `${surveyLabel}(${managementNo})`; } diff --git a/apps/sn/app/(map-layout)/map/ui/map-page-client.tsx b/apps/sn/app/(map-layout)/map/ui/map-page-client.tsx index a5943dede..fc1317895 100644 --- a/apps/sn/app/(map-layout)/map/ui/map-page-client.tsx +++ b/apps/sn/app/(map-layout)/map/ui/map-page-client.tsx @@ -3126,7 +3126,7 @@ const MapPageClient = () => { // 위험/정상 배지는 통수위험 상태를 즉시 구분하는 업무 의미색이므로 고정색을 유지한다.
diff --git a/apps/sn/src/features/exmn/lib/exmn-business-validation.ts b/apps/sn/src/features/exmn/lib/exmn-business-validation.ts index 9e423bdc8..e728c4f2b 100644 --- a/apps/sn/src/features/exmn/lib/exmn-business-validation.ts +++ b/apps/sn/src/features/exmn/lib/exmn-business-validation.ts @@ -9,12 +9,12 @@ export const getExmnTemplatePrerequisiteMessage = ( detail: Pick, ): string | null => { if (!detail.districts?.length) { - return "조사관리에서 해당 사업 행을 선택하고 ‘수정’을 눌러 처리분구를 등록한 뒤 ‘조사양식 받기’를 실행해 주세요. 이후 결과등록을 다시 진행해 주세요."; + return "조사사업에서 해당 사업 행을 선택하고 ‘수정’을 눌러 처리분구를 등록한 뒤 ‘조사양식 받기’를 실행해 주세요. 이후 결과등록을 다시 진행해 주세요."; } if (detail.districts.some(district => !district.pplExmnSheetNm?.trim() || !district.mnhlExmnSheetNm?.trim(), )) { - return "조사관리에서 해당 사업 행 선택 → ‘수정’ → ‘조사양식 받기’를 먼저 실행한 후 결과등록을 다시 진행해 주세요."; + return "조사사업에서 해당 사업 행 선택 → ‘수정’ → ‘조사양식 받기’를 먼저 실행한 후 결과등록을 다시 진행해 주세요."; } return null; }; diff --git a/apps/sn/src/features/exmn/ui/exmn-registration-page.tsx b/apps/sn/src/features/exmn/ui/exmn-registration-page.tsx index 603362547..52792c93b 100644 --- a/apps/sn/src/features/exmn/ui/exmn-registration-page.tsx +++ b/apps/sn/src/features/exmn/ui/exmn-registration-page.tsx @@ -287,7 +287,7 @@ export function ExmnRegistrationPage() { if (!popup) { setNotice({ type: "error", - text: "사업등록 팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.", + text: "조사사업 등록 팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.", }); } }; @@ -660,7 +660,7 @@ export function ExmnRegistrationPage() {
- 사업등록 + 조사사업등록 + +
+ ); +} diff --git a/apps/sn/app/(map-layout)/map/lib/use-water-risk-layer.ts b/apps/sn/app/(map-layout)/map/lib/use-water-risk-layer.ts index 4af91499d..808f661dc 100644 --- a/apps/sn/app/(map-layout)/map/lib/use-water-risk-layer.ts +++ b/apps/sn/app/(map-layout)/map/lib/use-water-risk-layer.ts @@ -127,7 +127,7 @@ export function useWaterRiskLayer({ const [pixelX = 0, pixelY = 0] = map.getPixelFromCoordinate?.(waterRiskInfoAnchor) ?? []; const cardWidth = 244; - const cardHeight = 180; + const cardHeight = 190; const left = pixelX + 22; const top = pixelY - cardHeight / 2; @@ -148,6 +148,12 @@ export function useWaterRiskLayer({ setWaterRiskInfoCardStyle({ left, top }); }, [waterRiskInfoAnchor]); + /** 정보 카드만 닫고 지도 결과·범례는 유지하며, 다음 결과 선택 시 다시 표시한다. */ + const dismissWaterRiskInfoCard = useCallback(() => { + setWaterRiskInfoAnchor(null); + setWaterRiskInfoCardStyle(null); + }, []); + const clearWaterRiskPopupCloseWatcher = useCallback(() => { if (waterRiskPopupCloseWatcherRef.current !== null) { window.clearInterval(waterRiskPopupCloseWatcherRef.current); @@ -470,6 +476,7 @@ export function useWaterRiskLayer({ waterRiskInfoCardStyle, selectedWaterRiskMapView, selectedWaterRiskDisplayStatus, + dismissWaterRiskInfoCard, watchPopup, }; } diff --git a/apps/sn/app/(map-layout)/map/ui/map-control-styles.tsx b/apps/sn/app/(map-layout)/map/ui/map-control-styles.tsx index 37b05ee4b..52bab67d1 100644 --- a/apps/sn/app/(map-layout)/map/ui/map-control-styles.tsx +++ b/apps/sn/app/(map-layout)/map/ui/map-control-styles.tsx @@ -1,6 +1,6 @@ "use client"; -/** 지도 route에만 적용하는 기존 전역 컨트롤 CSS. 선택자와 스타일 값은 유지한다. */ +/** 지도 route의 기존 배치를 유지하고 SN 컨트롤의 쌓임 순서를 맞춘다. */ export default function MapControlStyles() { return (