diff --git a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx index f811806e2..e1f41b952 100644 --- a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx @@ -38,16 +38,16 @@ type SinkholeTableRow = SinkholeListItem & { }; const HEADLIST = [ - { id: "rowNo", title: "NO", widthPercent: 6 }, - { id: "ocrnYmd", title: "발생일자", widthPercent: 11 }, - { id: "fullAddress", title: "주소", widthPercent: 22 }, - { id: "sizeWidthDisplay", title: "발생규모폭", widthPercent: 10 }, - { id: "sizeLengthDisplay", title: "발생규모연장", widthPercent: 10 }, - { id: "sizeDepthDisplay", title: "발생규모깊이", widthPercent: 10 }, - { id: "detailCauseDisplay", title: "상세발생원인", widthPercent: 12 }, - { id: "restoreStatus", title: "복구상태", widthPercent: 9 }, - { id: "coordStatusDisplay", title: "좌표보정상태", widthPercent: 10 }, - { id: "restoreCompletedYmdDisplay", title: "복구완료일자", widthPercent: 10 }, + { id: "acdntNo", title: "사고번호", widthPercent: 17 }, + { id: "ocrnYmd", title: "발생일자", widthPercent: 10 }, + { id: "fullAddress", title: "주소", widthPercent: 20 }, + { id: "sizeWidthDisplay", title: "발생규모폭", widthPercent: 8 }, + { id: "sizeLengthDisplay", title: "발생규모연장", widthPercent: 8 }, + { id: "sizeDepthDisplay", title: "발생규모깊이", widthPercent: 8 }, + { id: "detailCauseDisplay", title: "상세발생원인", widthPercent: 11 }, + { id: "restoreStatus", title: "복구상태", widthPercent: 8 }, + { id: "coordStatusDisplay", title: "좌표보정상태", widthPercent: 9 }, + { id: "restoreCompletedYmdDisplay", title: "복구완료일자", widthPercent: 11 }, ] as const; const toDisplay = (value: unknown) => { diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 45765f19f..845ea1102 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -144,6 +144,8 @@ const AGED_PIPE_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT"; const AGED_PIPE_POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE"; const AGED_PIPE_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC"; const SINKHOLE_DEFAULT_POINT_LIMIT = 2000; +// 지반침하 지도 포인트와 같은 파란 계열의 기존 레이어 트리 심볼 자산을 재사용한다. +const SINKHOLE_TREE_ICON_NAME = "SN_SINKHOLE_SPOT_BLUE.png"; const SOUTH_KOREA_VIEW_EXTENT: [number, number, number, number] = [ 600000, 1300000, @@ -1197,6 +1199,26 @@ const createSinkholeRadiusStyle = (variant: SinkholePointVariant = "blue") => }), zIndex: 9993, }); +// 반경 원 상단에 실제 적용 거리값을 표시하는 선택 전용 지도 라벨 스타일이다. +const createSinkholeRadiusLabelStyle = ( + radiusMeters: number, + variant: SinkholePointVariant = "blue", +) => + new Style({ + text: new Text({ + text: `반경 ${radiusMeters.toLocaleString("ko-KR")}m`, + font: "600 13px Pretendard, sans-serif", + textAlign: "center", + offsetY: -14, + padding: [4, 7, 4, 7], + fill: new Fill({ color: SINKHOLE_POINT_PALETTE[variant].strokeSelected }), + stroke: new Stroke({ color: "rgba(255, 255, 255, 0.98)", width: 3 }), + backgroundFill: new Fill({ color: "rgba(255, 255, 255, 0.9)" }), + backgroundStroke: new Stroke({ color: SINKHOLE_POINT_PALETTE[variant].radiusStroke, width: 1.5 }), + }), + zIndex: 9998, + }); + const createSinkholeFocusedPipeStyle = () => [ new Style({ @@ -2426,7 +2448,7 @@ const normalizeTreeRowSpacing = (data: any[]) => { return { ...item, - iconNm: null, + iconNm: item.iconNm || SINKHOLE_TREE_ICON_NAME, gobjTy: "", }; }); @@ -2463,7 +2485,7 @@ const appendSinkholeControlNode = (data: any[]) => { lyrSn: existingRootId, id: existingRootId, gobjTy: "", - iconNm: null, + iconNm: existingRoot.iconNm || SINKHOLE_TREE_ICON_NAME, clickPsblYn: "N", actlLyrYn: "N", children: null, @@ -2481,7 +2503,7 @@ const appendSinkholeControlNode = (data: any[]) => { lyrTyNm: SINKHOLE_LAYER_TYPE, useYn: "Y", minViewSize: null, - iconNm: null, + iconNm: SINKHOLE_TREE_ICON_NAME, iconSize: null, lblAtrbNm: null, fontNm: null, @@ -4198,7 +4220,14 @@ const Map = () => { }); radiusFeature.setStyle(createSinkholeRadiusStyle(pointVariant)); - source.addFeatures([radiusFeature, pointFeature]); + // 원의 북쪽 경계에 라벨을 두고 같은 source에서 관리해 선택 해제 시 함께 정리한다. + const radiusLabelFeature = new Feature({ + geometry: new Point([detail.x, detail.y + radiusMeters]), + }); + radiusLabelFeature.set("sinkholeRadiusLabel", true); + radiusLabelFeature.setStyle(createSinkholeRadiusLabelStyle(radiusMeters, pointVariant)); + + source.addFeatures([radiusFeature, radiusLabelFeature, pointFeature]); }, [clearSinkholeSelectionLayer, ensureSinkholeSelectionLayer], ); @@ -4642,11 +4671,13 @@ const Map = () => { try { const layerTypeName = String(pipe?.lyrTypeNm ?? "tb_ms_swage_ppl"); + const storageNm = String(GisApp.Module.layer?.storageNm ?? "").trim(); + if (!storageNm) throw new Error("GeoServer 저장소 이름을 확인할 수 없습니다."); const params = new URLSearchParams({ SERVICE: "WFS", VERSION: "1.1.0", REQUEST: "GetFeature", - TYPENAME: `${GisApp.Module.layer.storageNm}${layerTypeName}`, + TYPENAME: `${storageNm}${layerTypeName}`, SRSNAME: GisApp.TO_PROJ, OUTPUTFORMAT: "application/json", FEATUREID: fid, @@ -4845,6 +4876,48 @@ const Map = () => { [activateRightPanelTab, clearSinkholeFocusedPipeLayer, clearSinkholeSelectionLayer, rightPanelTabs], ); + // 선택 피처가 없는 지도 본체 클릭을 기존 지반침하 상세 닫기 흐름과 연결한다. + const handleSinkholeMapBlankClick = useCallback(() => { + if (gisEdit.isEditMode || !selectedSinkholeDetail?.acdntNo) return; + + const sinkholeTabId = `sinkhole:${selectedSinkholeDetail.acdntNo}`; + const hasSinkholeTab = rightPanelTabs.some((tab) => tab.id === sinkholeTabId); + + if (hasSinkholeTab && activeRightPanelTabId === sinkholeTabId) { + closeRightPanelTab(sinkholeTabId); + } else { + // 비활성 지반침하 탭은 현재 활성 탭을 바꾸지 않고 제거하며 종속 관로의 부모 연결만 해제한다. + setRightPanelTabs((currentTabs) => + currentTabs + .filter((tab) => tab.id !== sinkholeTabId) + .map((tab) => (tab.parentTabId === sinkholeTabId ? { ...tab, parentTabId: null } : tab)), + ); + setSelectedSinkholeDetail(null); + setSelectedSinkholeNearbyPipes([]); + clearSinkholeSelectionLayer(); + if (pipeParentTabIdRef.current === sinkholeTabId) { + pipeParentTabIdRef.current = null; + } + if (!hasSinkholeTab && getSelectedFeatureLayerTypeName(featureData) === SINKHOLE_LAYER_TYPE) { + setFeatureData(null); + setIsOpen(false); + } + } + + const select = GisApp.Module.select as any; + select?.resetSelectedFeature?.(); + clearSinkholeFocusedPipeLayer(); + }, [ + activeRightPanelTabId, + clearSinkholeFocusedPipeLayer, + clearSinkholeSelectionLayer, + closeRightPanelTab, + featureData, + gisEdit.isEditMode, + rightPanelTabs, + selectedSinkholeDetail, + ]); + useEffect(() => { if (!isOpen || !featureData) return; const nextTab = buildRightPanelTab(featureData); @@ -7260,6 +7333,8 @@ const Map = () => { detailView={GisEditAwareDetailView} canSelectFeature={canSelectFeatureDuringGisEdit} onFeatureSelect={onFeatureSelect} + onMapBlankClick={handleSinkholeMapBlankClick} + sinkholeLegendIconVisible={true} featureData={featureData} setFeatureData={setFeatureData} isOpen={isOpen} diff --git a/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/001-qa-SFR-010-010-지반침하정보-통합검증계획.md b/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/001-qa-SFR-010-010-지반침하정보-통합검증계획.md index 1921ed7b0..bddad6760 100644 --- a/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/001-qa-SFR-010-010-지반침하정보-통합검증계획.md +++ b/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/001-qa-SFR-010-010-지반침하정보-통합검증계획.md @@ -104,3 +104,27 @@ - 처리시설/처리구역 탭에 실제 시설명/구역명과 유형별 색상이 표시되는지 확인한다. - 부모 지반침하를 닫으면 관로의 부모 링크가 사라지고, 최대 5개 자동 정리에서는 활성 관로의 부모가 보호되는지 확인한다. - 자동 검증은 `diff --check`, TypeScript, Next build로 수행하고, 화면 상호작용은 로그인 가능한 `/map` 통합 환경에서 수동 수행한다. + +### 3.6 2026-07-14 지도 UX 현행 유지보수 검증 계획 + +- 반경이 다른 사고를 각각 선택해 지도 라벨 `반경 {값}m`와 상세 패널 값이 일치하는지 확인한다. +- 새 사고 선택, 탭 닫기, 빈 지도 클릭, 레이어 해제 시 반경 원·라벨·중심점이 함께 제거되는지 확인한다. +- 지반침하 루트에 기존 파란 심볼이 보이고 다른 앱/서비스의 공통 트리 노출은 바뀌지 않는지 확인한다. +- 빈 지도 클릭은 현재 선택 지반침하와 종속 관로 강조만 해제하고 다른 지반침하·처리시설·처리구역·GIS 편집 탭은 유지하는지 확인한다. +- 다른 피처 클릭과 GIS 편집 핸들/중간점/미리보기 클릭이 빈 영역 해제로 오인되지 않는지 확인한다. +- 비활성 지반침하를 정리할 때 현재 활성 탭이 바뀌지 않는지 확인한다. +- 지반침하발생현황 그리드 첫 열에 사고번호가 표시되고 화면 NO 열은 없으며 행 선택·페이징·정렬이 회귀하지 않는지 확인한다. +- 정적 검사는 변경 파일 구문/타입, `git diff --check`, Next build로 수행하고 상호작용은 로그인 가능한 `/map`에서 수동 확인한다. +- BE는 기존 목록·상세·반경 응답의 `acdntNo`, `radiusMeters` 유지 여부만 회귀 확인한다. + +### 3.7 2026-07-14 빈 지도 클릭 선택 해제 수동 검증 절차 + +1. 로그인 후 `/map`에서 지반침하정보 레이어를 켠다. +2. 목록이 아니라 지도 위 지반침하 포인트를 직접 선택하고 우측 상세 탭, 반경 원, `반경 {값}m` 라벨이 표시되는지 확인한다. +3. 다른 피처·지도 컨트롤·GIS 편집 핸들이 없는 빈 지도 영역을 클릭한다. +4. 선택한 지반침하 상세 탭 또는 현재 선택 상태, 반경 원·라벨·중심점, 종속 관로 강조가 함께 해제되는지 확인한다. 기본 지반침하 레이어의 포인트는 남아 있어야 한다. +5. 다른 업무 탭을 열어 둔 상태에서 반복해 다른 탭은 유지되고 활성 탭이 정상적으로 복귀하는지 확인한다. +6. GIS 편집 모드에서는 빈 영역을 클릭해도 지반침하 선택 해제가 오동작하지 않는지 확인한다. +7. 해제 후 다른 지반침하 포인트를 다시 선택해 상세와 반경 표시가 정상적으로 열리는지 확인한다. + +- 이 절차는 지도 포인트 선택 후 해제를 검증한다. 목록 행 선택만 한 상태는 지도 선택 모듈의 빈 선택 이벤트 대상이 아니므로 별도 시나리오로 구분한다. diff --git a/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md b/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md index d8954d398..3592c35c6 100644 --- a/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md @@ -293,3 +293,31 @@ - 백엔드 `compileJava`는 성공했다. - 사용자 요청에 따라 서버와 브라우저는 실행하지 않았다. - 남은 수동 검증은 실행 중 통합 환경에서 관로→부모 지반침하 복귀 후 화살표 숨김, 처리구역 클릭 200 응답과 분석 표 표시를 확인하는 것이다. + +### 2.27 2026-07-14 지도 UX 유지보수 구현 확인 및 사용자 검증 대기 + +- 구현 파일의 변경 diff에서 반경 라벨 피처, 트리 심볼 fallback/선택 prop, 빈 선택 결과 콜백, 목록 사고번호 첫 열을 정적으로 확인했다. +- 공통 빈 선택 결과는 `GisSelect` 한 곳에서만 소비하며 콜백 미전달 화면은 기존처럼 아무 동작도 하지 않는 구조를 확인했다. +- `pnpm --filter sn exec tsc --noEmit --pretty false --ignoreDeprecations 5.0`은 실행했으나 기존 common/floodHistory/html/chart 오류로 전체 성공하지 않았다. +- 검사에서 함께 확인된 `page.tsx`의 GeoServer storage null 가능성은 같은 지반침하 관로 조회 흐름에서 방어 처리했다. +- 개발 서버가 실행 중이어서 저장소 규칙에 따라 Next 및 `business_components` 빌드는 수행하지 않았다. +- 사용자 요청에 따라 브라우저·API·화면 상호작용 검증은 수행하지 않았다. +- 현재 판정은 `구현 완료 / 사용자 수동 검증 대기`다. +- 사용자 확인 항목은 반경 라벨 값과 시인성, 트리 파란 심볼, 빈 지도 클릭 시 선택 해제와 다른 탭 보존, 목록 첫 열 사고번호다. + +### 2.28 2026-07-14 목록 NO 열 제거 및 수동 검증 이관 + +- `SinkholePopupPage.tsx`의 `HEADLIST`에서 `acdntNo`가 첫 열이고 `rowNo/NO` 열이 없는 것을 정적으로 확인했다. +- 내부 `rowNo` 생성과 다운로드 순번은 기존 호환을 위해 유지했다. +- 지도 선택 해제의 구체적인 수동 절차와 기대 결과를 통합검증계획 3.7에 추가했다. +- 브라우저·API·빌드는 사용자 요청에 따라 실행하지 않았으며 현재 판정은 `구현 완료 / 사용자 수동 검증 대기`다. + +### 2.29 2026-07-14 사용자 통합 검증 결과 + +- 검증 주체: 사용자 +- 검증 환경: 실행 중인 통합 화면 +- 반경 표시 영역에 실제 반경 값이 정상 표시됨을 확인했다. +- 지반침하 레이어 트리에 심볼이 정상 표시됨을 확인했다. +- 지도에서 선택한 지반침하가 빈 지도 영역 클릭으로 정상 해제되고 관련 반경·강조 표시도 함께 정리됨을 확인했다. +- 지반침하발생현황 목록 첫 열에 사고번호가 표시되고 화면 `NO` 열이 제거된 것을 확인했다. +- 이번 유지보수 요구사항 전체가 성공했으므로 최종 판정은 `완료`다. diff --git a/apps/sn/docs/작업계획/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-작업카드.md b/apps/sn/docs/작업계획/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-작업카드.md index a16aab3ca..5e21bf1c4 100644 --- a/apps/sn/docs/작업계획/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-작업카드.md +++ b/apps/sn/docs/작업계획/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-작업카드.md @@ -3,8 +3,8 @@ ## 1. 작업 개요 - 작업 ID: SFR-010-010-FE -- 작업명: 하수관망도 지반침하정보 프론트엔드 구현 계획 -- 상태: 설계 +- 작업명: 하수관망도 지반침하정보 프론트엔드 유지보수 +- 상태: 완료 - 작성일: 2026-06-02 - 담당 역할: FE - 관련 역할: DE, BE, QA @@ -27,9 +27,10 @@ ## 1-2. 최신 해석 메모 -- 현재 `apps/sn`에는 `지반침하정보` 실제 기능 구현이 없고, HTML 샘플 수준 흔적만 확인됐다. -- 목록 팝업은 `노후관로` popup 패턴을, 우측 상세 패널은 `FeatureClickEventPopup`의 세로 패널 패턴을 우선 재사용한다. -- 좌표 보정과 반경 내 관로 탐색은 프론트에서 계산하지 않고 `sewerage-service` 응답 기준으로만 처리한다. +- 현재 `apps/sn`에는 레이어 on/off, 목록 팝업, 지도 포인트 선택, 사고별 우측 상세 탭, 반경 원과 반경 내 관로 표시가 구현되어 있다. +- 이번 작업은 신규 화면 구축이 아니라 반경 값 라벨, 트리 심볼, 빈 지도 클릭 선택 해제, 목록 사고번호 첫 열을 보완하는 현행 유지보수다. +- 목록 팝업은 `SinkholePopupPage`, 우측 상세와 선택 오버레이는 `map/page.tsx`의 기존 상태 흐름을 유지해 확장한다. +- 목록의 `acdntNo`와 상세/반경의 `radiusMeters`가 이미 제공되므로 백엔드 계약은 변경하지 않는다. ## 2. 작업 목적 @@ -92,10 +93,10 @@ | 기능 | Method | URL | 요청 | 응답 | 확인 상태 | | --- | --- | --- | --- | --- | --- | | 지도 포인트 조회 | `GET` | `/api/v1/sinkholes/geometry` | 기간/행정구역/limit | GeoJSON `FeatureCollection` | 현재 BE 선반영 | -| 목록 조회 | `GET` | `/api/v1/sinkholes` | 기간, 시도, 시군구, 복구상태, 좌표보정상태, page, size | `items[]`, `total` | BE 신규 필요 | -| 상세 조회 | `GET` | `/api/v1/sinkholes/{acdntNo}` | 사고번호 | 상세 DTO | BE 신규 필요 | -| 반경 내 관로 조회 | `GET` | `/api/v1/sinkholes/{acdntNo}/nearbyPipes` | 사고번호, 반경 | 관로 목록 | BE 신규 필요 | -| 동기화 | `POST` | `/api/v1/sinkholes/sync` | 관리자 요청 body | 처리 결과 | BE 신규 필요 | +| 목록 조회 | `GET` | `/api/v1/sinkholes` | 기간, 시도, 시군구, 복구상태, 좌표보정상태, page, size | `items[]`, `total` | 구현 확인 | +| 상세 조회 | `GET` | `/api/v1/sinkholes/{acdntNo}` | 사고번호 | 상세 DTO | 구현 확인 | +| 반경 내 관로 조회 | `GET` | `/api/v1/sinkholes/{acdntNo}/nearbyPipes` | 사고번호, 반경 | 관로 목록 | 구현 확인 | +| 동기화 | `POST` | `/api/v1/sinkholes/sync` | 관리자 요청 body | 처리 결과 | 구현 확인 | | 다운로드 | `GET` | `/api/v1/sinkholes` | 현재 조회조건 + `includeCoordStatus=false` | FE xlsx 생성 | 목록 재조회 기반 | - FE 메모 @@ -221,3 +222,45 @@ - 후속 결정 후보는 `현행 다중 탭 유지`와 `처리시설 1개 + 처리구역 1개 유형별 singleton`이다. - singleton을 선택하면 동일 유형의 다음 선택은 새 탭을 추가하지 않고 기존 탭의 표시명, feature payload, 활성 시각을 최신 대상으로 교체한다. - 이 정책은 아직 미확정이며 현재 코드 변경 대상이 아니다. + +## 14. 2026-07-14 현행 유지보수 설계 범위 + +### 14.1 구현 원칙 + +- 현행 지도/우측 탭/API 흐름을 유지하고 네 요구사항에 필요한 최소 분기만 추가한다. +- 지도 반경 라벨은 원 상단 Point 피처로 추가해 원·중심점과 같은 생명주기로 관리한다. +- 트리 심볼은 기존 파란 지반침하 자산을 사용하고 공통 트리 영향은 기본 숨김인 선택 prop으로 격리한다. +- 빈 지도 클릭은 현재 선택 지반침하와 종속 관로 강조만 정리하며 다른 탭과 GIS 편집 상태는 보존한다. +- 목록 팝업은 사고번호를 첫 데이터 열로 두고 화면 NO 열은 표시하지 않는다. + +### 14.2 예상 코드 대상 + +- `app/(map-layout)/map/page.tsx` +- `app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx` +- `packages/common/business_components/src/ui/map/Gis.js` +- `packages/common/business_components/src/ui/map/GisTree.js` +- `packages/common/business_components/src/ui/map/GisSelect.js` +- `packages/common/business_components/src/ui/map/lib/gis.select.js` + +### 14.3 제외 범위 + +- BE API·DTO·Repository·DB 변경 +- 엑셀 다운로드 열 순서 변경 +- 기존 탭 최대 개수, 처리시설·처리구역 탭 정책 변경 + +### 14.4 완료 기준 + +- 구현·정적 검사·빌드 후 로그인 가능한 `/map`에서 반경 라벨, 트리 심볼, 빈 클릭 해제, 목록 첫 열을 수동 확인한다. +- 구현 결과는 통합구현기록과 통합검증결과, 2026-07-14 공통 작업기록에 반영했다. +- 사용자가 통합 화면에서 반경 라벨, 트리 심볼, 빈 지도 클릭 해제, 사고번호 첫 열 및 화면 NO 열 제거를 모두 확인했으며 최종 완료로 판정했다. + +### 14.5 실제 구현 파일 + +- `app/(map-layout)/map/page.tsx` +- `app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx` +- `packages/common/business_components/src/ui/map/Gis.js` +- `packages/common/business_components/src/ui/map/GisTree.js` +- `packages/common/business_components/src/ui/map/GisSelect.js` +- `packages/common/business_components/src/ui/map/lib/gis.select.js` + +- BE API·DTO·Repository·DB는 변경하지 않았다. diff --git a/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md b/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md index 7773978e7..2ef622471 100644 --- a/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md @@ -453,3 +453,40 @@ - 처리시설과 처리구역은 현재 피처별 다중 탭으로 동작한다. - 후속 운영 결과에 따라 처리시설 1개와 처리구역 1개를 각각 singleton으로 바꾸는 안을 검토한다. - 정책 확정 전에는 현행 동작을 유지하며 이번 라운드에서 추가 코드 변경은 하지 않았다. + +### 4.53 2026-07-14 반경 라벨·트리 심볼·지도 선택 해제·목록 사고번호 유지보수 설계 + +- 현행 구현을 기준으로 유지보수 변경 지점을 확정했다. 이번 단계에서는 FE 코드를 수정하지 않았다. +- `map/page.tsx`에는 반경 원 상단 라벨 피처와 빈 지도 클릭 시 현재 지반침하/종속 관로 강조 정리 로직을 추가할 예정이다. +- `SinkholePopupPage.tsx` 목록은 사고번호를 첫 데이터 열로 조정하고 화면 NO 열은 표시하지 않을 예정이다. +- 공통 `Gis.js`, `GisTree.js`에는 기본값이 현행 숨김인 선택 prop을 추가하고 `sn` 지반침하 루트에서만 기존 파란 심볼을 노출할 예정이다. +- 기존 `acdntNo`, `radiusMeters` 계약으로 구현 가능하므로 BE·DB 변경은 없다. +- 실제 코드, 검사 및 브라우저 결과는 다음 구현 단계에서 같은 통합 기록의 후속 절로 추가한다. + +### 4.54 2026-07-14 지도 반경 라벨·트리 심볼·빈 클릭 해제·목록 사고번호 구현 + +- `map/page.tsx`의 지반침하 선택 source에 반경 원 상단 Point 라벨을 추가했다. 문구는 `반경 {radiusMeters}m`이며 원·중심점과 함께 정리된다. +- 지반침하 루트 `iconNm`은 기존 값 우선, 값이 없으면 `SN_SINKHOLE_SPOT_BLUE.png`를 사용하도록 변경했다. +- 공통 `GisTree`의 `sinkholeLegendIconVisible` 기본값을 false로 두고 `sn` 지도에서만 true를 전달해 타 앱 노출을 격리했다. +- 공통 선택 모듈은 선택 결과가 없을 때 빈 배열을 전달하고, `GisSelect`가 선택적 `onMapBlankClick` 콜백을 호출하도록 확장했다. +- `sn` 지도는 GIS 편집 중 빈 클릭을 무시하고 현재 지반침하를 기존 닫기 흐름으로 정리한다. 비활성 지반침하 탭은 활성 탭을 바꾸지 않고 제거하며 종속 관로 부모 링크를 비운다. +- `SinkholePopupPage.tsx` 그리드 첫 데이터 열에 `사고번호(acdntNo)`를 배치하고 화면 `NO` 열은 제거했다. 내부 `rowNo`와 다운로드 순번은 유지했다. +- 반경 내 관로 GeoServer 조회의 기존 storage nullable 타입 경고는 저장소 이름 확인 후 요청하도록 방어 처리했다. +- BE API·DTO·Repository·DB 변경은 없다. +- 브라우저·API·빌드 검증은 사용자 요청에 따라 수행하지 않았으며 사용자 검증 대기 상태다. + +### 4.55 2026-07-14 목록 NO 열 제거 후속 반영 + +- 사용자 후속 결정에 따라 `HEADLIST`에서 `rowNo/NO` 열을 제거했다. +- 사고번호를 첫 화면 열로 유지하고 열 너비를 17로 조정했다. +- `SinkholeTableRow.rowNo`, `toTableRows`, `buildDownloadRows`는 다운로드 순번 호환을 위해 유지했다. +- BE API·DTO·Repository·DB 변경은 없다. +- 브라우저·API·빌드 검증은 사용자 요청에 따라 수행하지 않았으며 사용자 수동 검증 대기 상태다. + +### 4.56 2026-07-14 사용자 통합 검증 완료 + +- 사용자가 통합 화면에서 이번 유지보수 요구사항 전체를 직접 검증했고 모두 정상 동작함을 확인했다. +- 반경 라벨은 실제 반경 값과 일치하고, 지반침하 레이어 트리 심볼은 의도한 위치에 정상 표시됐다. +- 지도 지반침하 선택 후 빈 지도 영역 클릭 시 선택 상세, 반경 원·라벨, 종속 강조가 정상 해제되고 다른 탭과 기본 레이어는 유지됐다. +- 지반침하발생현황 목록은 사고번호가 첫 열로 표시되고 화면 `NO` 열은 제거된 상태로 정상 동작했다. +- BE API·DTO·Repository·DB 계약 변경 없이 최종 상태를 `완료`로 전환했다. diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 2eea996df..04bf3a504 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -49,7 +49,9 @@ function Gis({ searchView: SearchView, detailView: DetailView, onFeatureSelect, + onMapBlankClick, canSelectFeature, + sinkholeLegendIconVisible = false, setIsSearchModalOpen, onCheckedLayersChange, searchBar: SearchBar, @@ -311,6 +313,7 @@ function Gis({ }} imageVisible={imgVisble} layerIconVisible={layerIconVisible} + sinkholeLegendIconVisible={sinkholeLegendIconVisible} isPlaying={isPlaying} setIsPlaying={setIsPlaying} targetLayerTypes={targetLayerTypes} @@ -359,7 +362,11 @@ function Gis({ )} - + diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js index ac862c2f1..1ed788398 100644 --- a/packages/common/business_components/src/ui/map/GisSelect.js +++ b/packages/common/business_components/src/ui/map/GisSelect.js @@ -3,15 +3,21 @@ import { GisContext } from "./Gis"; import GisApp from "./lib/gis.core"; import GisOlSelect from "./lib/gis.select"; -export default React.memo(function GisSelect({ onFeatureSelect, canSelectFeature }) { +export default React.memo(function GisSelect({ onFeatureSelect, onMapBlankClick, canSelectFeature }) { const { gisMap } = useContext(GisContext); const onFeatureSelectRef = useRef(onFeatureSelect); + // 빈 지도 클릭 콜백도 interaction 재생성 없이 최신 화면 상태를 참조하도록 ref로 보관한다. + const onMapBlankClickRef = useRef(onMapBlankClick); const canSelectFeatureRef = useRef(canSelectFeature); useEffect(() => { onFeatureSelectRef.current = onFeatureSelect; }, [onFeatureSelect]); + useEffect(() => { + onMapBlankClickRef.current = onMapBlankClick; + }, [onMapBlankClick]); + useEffect(() => { canSelectFeatureRef.current = canSelectFeature; }, [canSelectFeature]); @@ -31,6 +37,8 @@ export default React.memo(function GisSelect({ onFeatureSelect, canSelectFeature return; } onFeatureSelectRef.current?.(features[0]); + } else { + onMapBlankClickRef.current?.(); } }); diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index cdf67af25..fd37d244c 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -46,6 +46,7 @@ export default React.memo(function GisTree({ onProgressChange = () => { }, layerSettingView: LayerSettingView = null, layerIconVisible, + sinkholeLegendIconVisible = false, inlineLayerSettingView, treeSectionClassName = "", toggleButtonPosition = "center", @@ -561,7 +562,8 @@ export default React.memo(function GisTree({ nodeLabel === "지반침하정보"; const isFloodHistoryRootTreeNode = nodeLabel === "침수흔적도"; const hideTreeLegendIcon = - isSinkholeRootTreeNode || isFloodHistoryRootTreeNode; + (isSinkholeRootTreeNode && !sinkholeLegendIconVisible) || + isFloodHistoryRootTreeNode; const showLayerActionButton = layerIconVisible?.(node); const showLayerActionIcon = depth > 0 && imageVisible; diff --git a/packages/common/business_components/src/ui/map/lib/gis.select.js b/packages/common/business_components/src/ui/map/lib/gis.select.js index e05edb9af..58fdc2592 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.select.js +++ b/packages/common/business_components/src/ui/map/lib/gis.select.js @@ -388,7 +388,11 @@ GisOlSelect.prototype = { this.selectInteraction.on('select', function (e) { if (!e.auto) { let features = me.selectInteraction.getFeatures(); - if(features.getArray().length == 0) return; + if(features.getArray().length == 0) { + // 기존 소비자는 빈 배열을 무시하고, 필요한 화면만 빈 지도 클릭으로 해석할 수 있게 전달한다. + callback([], []); + return; + } let layers = []; for (let i in features.array_) { let feature = features.array_[0];