From 31caee87dbbf55ace46440dc1999f216599a32ff Mon Sep 17 00:00:00 2001 From: kiminchan Date: Mon, 29 Jun 2026 17:45:31 +0900 Subject: [PATCH] =?UTF-8?q?GIS=20=ED=8E=B8=EC=A7=91=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A1=9C=20=EC=8A=A4=EB=83=85=20=EC=A7=80=EC=9B=90?= =?UTF-8?q?=20=EB=B0=8F=20=EC=84=B8=EC=85=98=20=EC=B7=A8=EC=86=8C/?= =?UTF-8?q?=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gisEdit/GisEditSessionPanel.tsx | 13 ++- .../map/components/gisEdit/useGisEdit.ts | 33 +++++++ apps/sn/app/(map-layout)/map/page.tsx | 88 ++++++++++++++++--- .../002-qa-SFR-012-003-하수관로편집-통합검증결과.md | 15 ++++ .../001-fe-SFR-012-003-하수관로편집-작업카드.md | 5 ++ .../001-fe-SFR-012-003-하수관로편집-통합구현기록.md | 18 ++++ .../src/features/windowPopup/model/types.ts | 1 + .../ui/GisEditSessionPopupContent.tsx | 3 + 8 files changed, 163 insertions(+), 13 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditSessionPanel.tsx b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditSessionPanel.tsx index 80fda2db1..9d9d692ea 100644 --- a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditSessionPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditSessionPanel.tsx @@ -3,6 +3,7 @@ import type { GisEditSessionSnapshot, GisEditTool } from "./gisEditTypes"; type Props = GisEditSessionSnapshot & { + onRemoveFocusedChange: () => void; onRemoveLastChange: () => void; onClearChanges: () => void; onFocusChange: (changeId: string) => void; @@ -71,6 +72,7 @@ export default function GisEditSessionPanel({ changes, canUndo, focusedChangeId, + onRemoveFocusedChange, onRemoveLastChange, onClearChanges, onFocusChange, @@ -87,13 +89,21 @@ export default function GisEditSessionPanel({
+
- ); } diff --git a/apps/sn/app/(map-layout)/map/components/gisEdit/useGisEdit.ts b/apps/sn/app/(map-layout)/map/components/gisEdit/useGisEdit.ts index 9da002d74..62d1b20d1 100644 --- a/apps/sn/app/(map-layout)/map/components/gisEdit/useGisEdit.ts +++ b/apps/sn/app/(map-layout)/map/components/gisEdit/useGisEdit.ts @@ -1061,6 +1061,38 @@ export const useGisEdit = (treeData: any[]) => { setStatusMessage("가장 최근 편집 변경분을 세션에서 제거했습니다."); }, [focusedChangeId, pendingInteraction]); + const removeFocusedChange = useCallback(() => { + const targetChangeId = focusedChangeId ?? pendingInteraction?.changeId ?? null; + if (!targetChangeId) { + setStatusMessage("??? ?? ??? ?? ?????."); + return; + } + + const removedChange = changes.find((change) => change.changeId === targetChangeId) ?? null; + if (!removedChange) { + setStatusMessage("??? ?? ??? ?? ?????."); + return; + } + + setLastSavedPayload(null); + setChanges((previous) => previous.filter((change) => change.changeId !== targetChangeId)); + + if (focusedChangeId === targetChangeId) { + const nextFocusedChange = changes.find((change) => change.changeId !== targetChangeId) ?? null; + setFocusedChangeId(nextFocusedChange?.changeId ?? null); + } + if (pendingInteraction?.changeId === targetChangeId) { + setPendingInteraction(null); + } + + latestSelectionRef.current = null; + setStatusMessage( + removedChange.attributeOnly + ? `${removedChange.layerName} ${removedChange.objectLabel} ???? ??? ??????.` + : `${removedChange.layerName} ${removedChange.objectLabel} ??? ???? ??????.`, + ); + }, [changes, focusedChangeId, pendingInteraction]); + const clearChanges = useCallback(() => { setLastSavedPayload(null); setChanges([]); @@ -1191,6 +1223,7 @@ export const useGisEdit = (treeData: any[]) => { registerFeatureSelection, registerPipeDetailSelection, save, + removeFocusedChange, removeLastChange, clearChanges, discardSession, diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 10cd8b688..2a32b5ee8 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -2855,7 +2855,12 @@ const Map = () => { } if (!gisEditDetailChangeId) return; - if (!gisEditDetailChange) return; + if (!gisEditDetailChange) { + setGisEditDetailChangeId(null); + setFeatureData(null); + setIsOpen(false); + return; + } if (gisEditDetailChange.layerId !== "tb_ms_swage_ppl" || gisEditDetailChange.editType === "DELETE") { setGisEditDetailChangeId(null); @@ -2890,6 +2895,11 @@ const Map = () => { return; } + if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.GIS_EDIT_SESSION_REMOVE_FOCUSED_CHANGE) { + gisEdit.removeFocusedChange(); + return; + } + if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.GIS_EDIT_SESSION_REMOVE_LAST_CHANGE) { gisEdit.removeLastChange(); return; @@ -5253,7 +5263,36 @@ const Map = () => { gisEditPipeSnapSourceRef.current?.clear(); }, []); - const loadGisEditPipeSnapSource = useCallback(async (excludedFid?: string | null) => { + const buildGisEditPipeSnapPreviewFeatures = useCallback((excludedChangeId?: string | null) => { + const normalizedExcludedChangeId = String(excludedChangeId ?? "").trim(); + const geoJson = new GeoJSON(); + + return gisEdit.changes + .filter((change) => change.layerId === "tb_ms_swage_ppl") + .filter((change) => !change.attributeOnly && change.editType !== "DELETE" && Boolean(change.afterGeometry)) + .filter((change) => change.changeId !== normalizedExcludedChangeId) + .map((change) => { + const feature = geoJson.readFeature( + { + type: "Feature", + geometry: change.afterGeometry, + properties: { + fid: change.fid ?? change.featureRef?.fid ?? change.objectId ?? change.changeId, + layerId: change.layerId, + changeId: change.changeId, + }, + }, + { + dataProjection: FLOW_DIRECTION_SRS, + featureProjection: GisApp.TO_PROJ, + }, + ); + feature.setId(`gis-edit-snap:${change.changeId}`); + return feature; + }); + }, [gisEdit.changes]); + + const loadGisEditPipeSnapSource = useCallback(async (options?: { excludedFid?: string | null; excludedChangeId?: string | null }) => { const map = gisMaps.current; const source = ensureGisEditPipeSnapSource(); if (!map || !source) return; @@ -5262,11 +5301,29 @@ const Map = () => { const view = map.getView?.(); if (!size || !view?.calculateExtent) return; + const normalizedExcludedFid = normalizeGisEditFid(options?.excludedFid ?? ""); + const normalizedExcludedChangeId = String(options?.excludedChangeId ?? "").trim(); + const hiddenPipeFidSet = new Set( + gisEdit.changes + .filter((change) => change.layerId === "tb_ms_swage_ppl") + .filter((change) => !change.attributeOnly && change.editType !== "DELETE") + .map((change) => normalizeGisEditFid(change.fid ?? change.featureRef?.fid ?? change.objectId)) + .filter((fid) => /^\d+$/.test(fid)), + ); + if (/^\d+$/.test(normalizedExcludedFid)) { + hiddenPipeFidSet.add(normalizedExcludedFid); + } + + const previewSnapFeatures = buildGisEditPipeSnapPreviewFeatures(normalizedExcludedChangeId); + source.clear(); + if (previewSnapFeatures.length > 0) { + source.addFeatures(previewSnapFeatures); + } + const screenExtent = view.calculateExtent(size); const requestExtent = expandGisEditPipeSnapExtent(screenExtent); const [minX, minY, maxX, maxY] = requestExtent; const bboxFilter = `BBOX(geom, ${minX}, ${minY}, ${maxX}, ${maxY}, '${GisApp.TO_PROJ}')`; - const normalizedExcludedFid = normalizeGisEditFid(excludedFid ?? ""); const cqlFilter = bboxFilter; const requestSeq = gisEditPipeSnapRequestSeqRef.current + 1; @@ -5297,23 +5354,26 @@ const Map = () => { const features = new GeoJSON().readFeatures(featureCollection, { dataProjection: GisApp.TO_PROJ, featureProjection: GisApp.TO_PROJ, - }).filter((feature: any) => { - if (!/^\d+$/.test(normalizedExcludedFid)) return true; - return getGisEditFeatureFid(feature) !== normalizedExcludedFid; - }); + }).filter((feature: any) => !hiddenPipeFidSet.has(getGisEditFeatureFid(feature))); source.clear(); source.addFeatures(features); + if (previewSnapFeatures.length > 0) { + source.addFeatures(previewSnapFeatures); + } } catch (error) { if ((error as Error)?.name === "AbortError") return; - console.error("GIS 편집 하수관로 스냅 source 조회 실패:", error); + console.error("GIS ?? ???? ?? source ?? ??:", error); source.clear(); + if (previewSnapFeatures.length > 0) { + source.addFeatures(previewSnapFeatures); + } } finally { if (gisEditPipeSnapAbortControllerRef.current === abortController) { gisEditPipeSnapAbortControllerRef.current = null; } } - }, [ensureGisEditPipeSnapSource]); + }, [buildGisEditPipeSnapPreviewFeatures, ensureGisEditPipeSnapSource, gisEdit.changes]); const restoreGisEditOriginalPipeMask = useCallback(() => { gisEditOriginalLayerFilterRef.current.forEach((originalFilter, layer) => { @@ -6275,7 +6335,10 @@ const Map = () => { ) : ""; - void loadGisEditPipeSnapSource(excludedFid); + void loadGisEditPipeSnapSource({ + excludedFid, + excludedChangeId: gisEdit.pendingInteraction?.changeId ?? null, + }); }, [ clearGisEditPipeSnapSource, gisEdit.changes, @@ -6298,7 +6361,10 @@ const Map = () => { "", ) : ""; - void loadGisEditPipeSnapSource(excludedFid); + void loadGisEditPipeSnapSource({ + excludedFid, + excludedChangeId: gisEdit.pendingInteraction?.changeId ?? null, + }); }); return () => { diff --git a/apps/sn/docs/작업검증/SFR-012-003-하수관로편집/002-qa-SFR-012-003-하수관로편집-통합검증결과.md b/apps/sn/docs/작업검증/SFR-012-003-하수관로편집/002-qa-SFR-012-003-하수관로편집-통합검증결과.md index 65907124b..32949380a 100644 --- a/apps/sn/docs/작업검증/SFR-012-003-하수관로편집/002-qa-SFR-012-003-하수관로편집-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-012-003-하수관로편집/002-qa-SFR-012-003-하수관로편집-통합검증결과.md @@ -631,3 +631,18 @@ - 남은 수동 검증 - 브라우저에서 MOVE 후 상세 팝업 진입 시 Console Error 오버레이가 뜨지 않는지 확인이 필요하다. - Network 탭에서 `fcltppl` 조회가 실패해도 팝업 입력 흐름이 유지되는지 확인이 필요하다. + +### 2.19 2026-06-29 임시 관로 스냅 및 세션 버튼 재정렬 점검 + +- 코드 기준 점검 + - `page.tsx`의 pipe snap source는 이제 원본 하수관로 WFS만 담지 않고, 현재 세션의 `ADD`/`MOVE`/`VERTEX_EDIT` preview 관로도 함께 합친다. + - 기존 수정 관로의 원본 fid는 WFS snap source에서 제외해, 이미 옮겨진 preview 관로가 수정 전 원본 geometry에 다시 붙지 않도록 조정했다. + - `GIS 편집 세션` popup은 `취소`, `뒤로가기`, `초기화` 3버튼으로 바뀌었고, `취소`는 현재 focus된 change 제거, `뒤로가기`는 기존 마지막 변경 취소, `초기화`는 전체 세션 비우기 역할로 분리됐다. + - 현재 상세 패널이 열린 change를 `취소`로 제거하면 우측 상세 패널도 닫히도록 방어 로직을 추가했다. +- 정적 검증 + - 실행 명령: `node apps/sn/node_modules/typescript/bin/tsc -p apps/sn/tsconfig.json --noEmit --pretty false --ignoreDeprecations 5.0` + - 결과: 실패. 저장소 기존 오류(`components/common/index.ts`, `floodHistoryTypes.ts`, `page.tsx` 기존 null 가능성, html route, chart implicit any`)만 남았고, 이번 변경 파일 신규 오류는 출력되지 않았다. +- 남은 수동 검증 + - 신규 관로 A를 추가한 직후 신규 관로 B를 추가하거나 이동/꼭지점편집할 때 A preview에 실제로 스냅되는지 확인이 필요하다. + - `취소` 버튼이 현재 선택 change만 제거하고, `뒤로가기`는 가장 최근 change를 제거하는지 popup과 지도 focus 기준으로 확인이 필요하다. + - `취소`로 현재 상세 패널 대상 change를 제거했을 때 우측 패널이 자연스럽게 닫히는지 확인이 필요하다. \ No newline at end of file diff --git a/apps/sn/docs/작업계획/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-작업카드.md b/apps/sn/docs/작업계획/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-작업카드.md index 4c99acf00..556c1b453 100644 --- a/apps/sn/docs/작업계획/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-작업카드.md +++ b/apps/sn/docs/작업계획/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-작업카드.md @@ -16,6 +16,11 @@ - `COMMON_DOCS_ROOT/작업지시/SFR-012-003-하수관로편집/하수관망도편집_화면설계서_v1_1.png` ## 1-2. 최근 진행 갱신 +- 2026-06-29 + - 원본 하수관로 WFS snap source를 우선 사용할 때 사용자 임시 추가 관로 preview가 스냅 대상에서 빠지는 구조를 확인했다. + - `page.tsx`의 pipe snap source 로더를 보강해 현재 세션의 `ADD`/`MOVE`/`VERTEX_EDIT` preview 관로도 함께 병합하고, 기존 수정 관로의 원본 fid는 숨겨 stale geometry로 스냅되지 않게 했다. + - `GIS 편집 세션` popup 버튼은 `취소/뒤로가기/초기화` 3종으로 재정렬하고, `취소`는 현재 선택 change 제거, `뒤로가기`는 기존 마지막 변경 취소 의미로 분리했다. + - 정적 타입체크 결과 이번 변경 파일 신규 오류는 없었고, 저장소 기존 오류만 남았다. 브라우저 수동 검증은 다음 범위로 남긴다. - 2026-06-26 - `MOVE` 도구가 관로 1회 이동 후 자동 해제되는 현상을 요구사항 분석 범위로 확인했다. - 현재 원인은 `useGisEdit.updateChangeGeometry()` 기본 경로가 geometry 갱신 뒤 `activeTool`과 `pendingInteraction`을 모두 비우는 구조이고, `page.tsx`의 `MOVE translateend`가 이 함수를 옵션 없이 호출하는 데 있다. diff --git a/apps/sn/docs/작업내용/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-통합구현기록.md b/apps/sn/docs/작업내용/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-통합구현기록.md index a4c8ed08f..374b01eb0 100644 --- a/apps/sn/docs/작업내용/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-통합구현기록.md @@ -361,3 +361,21 @@ - 조회 실패 시 기존 WFS 속성/세션 draft 값을 유지하고 팝업 입력 흐름은 계속 진행한다. - 백엔드 연동 보강 - `PplService.findByFcltMngNo(fcltMngNo, mngInstCd)`에서 MS 관로 상세 보조속성 조회와 관리기관명 조회를 안전 래퍼로 감싸, 보조 조회 실패가 `GET /api/v1/fcltppl/...` 전체 500으로 전파되지 않게 했다. + +### 4.21 2026-06-29 임시 관로 스냅 및 세션 버튼 재정렬 + +- 사용자 요구사항 기준 + - 기존 관로에는 잘 붙지만, 사용자가 임시로 추가한 관로에는 스냅핑이 제대로 동작하지 않는 문제가 있었다. + - `GIS 편집 세션` popup 버튼도 `취소`, `뒤로가기`, `초기화` 3종 의미로 다시 나눠야 했다. +- 원인 정리 + - pipe snap source는 현재 화면 BBOX 기준 `tb_ms_swage_ppl` 원본 WFS를 다시 읽는 구조라서, FE 세션 preview로만 존재하는 신규 관로/수정 관로가 source 우선 경로에서는 빠질 수 있었다. + - 세션 popup은 `마지막 변경 취소`, `초기화`만 있어 현재 선택한 change 1건만 제거하는 동작을 구분해 제공하지 못했다. +- 이번 라운드 반영 + - `page.tsx`에 `buildGisEditPipeSnapPreviewFeatures()`를 추가해 현재 세션의 하수관로 preview geometry를 별도 snap feature로 만들고, pipe snap source에 원본 WFS와 함께 병합했다. + - 기존 관로를 이동/꼭지점편집한 change는 원본 fid를 WFS snap source에서 제외해 수정 전 geometry에 다시 붙는 현상을 줄였다. + - pipe snap source는 WFS 응답 전에도 preview snap feature를 먼저 채워, 신규 관로를 추가한 직후 다른 신규 관로에 연속으로 스냅할 수 있게 했다. + - `useGisEdit`에 `removeFocusedChange()`를 추가해 현재 선택 change 제거를 지원하고, popup message type과 세션 패널 버튼도 `취소/뒤로가기/초기화` 기준으로 재정렬했다. + - 현재 상세 패널이 열려 있던 change가 세션에서 제거되면 우측 상세 패널도 함께 닫히도록 `page.tsx`의 detail effect를 보강했다. +- 남긴 판단 + - 이번 수정으로 임시 preview 관로가 snap 대상에서 빠지는 구조적 누락은 해소했지만, 실제 QGIS 체감과 지도 시각 피드백은 브라우저 수동 검증이 아직 필요하다. + - 세션 `취소`는 현재 focus된 change 기준이므로, 사용자가 어떤 change를 지우는지 목록 highlight와 지도 focus를 함께 보는 UX를 유지한다. \ No newline at end of file diff --git a/apps/sn/src/features/windowPopup/model/types.ts b/apps/sn/src/features/windowPopup/model/types.ts index 6b55bee30..2a7fa905c 100644 --- a/apps/sn/src/features/windowPopup/model/types.ts +++ b/apps/sn/src/features/windowPopup/model/types.ts @@ -38,6 +38,7 @@ export const WINDOW_POPUP_MESSAGE_TYPE = { GIS_EDIT_SESSION_READY: "sn:popup:gisEditSessionReady", GIS_EDIT_SESSION_SYNC: "sn:popup:gisEditSessionSync", GIS_EDIT_SESSION_FOCUS_CHANGE: "sn:popup:gisEditSessionFocusChange", + GIS_EDIT_SESSION_REMOVE_FOCUSED_CHANGE: "sn:popup:gisEditSessionRemoveFocusedChange", GIS_EDIT_SESSION_REMOVE_LAST_CHANGE: "sn:popup:gisEditSessionRemoveLastChange", GIS_EDIT_SESSION_CLEAR_CHANGES: "sn:popup:gisEditSessionClearChanges", WATER_RISK_READY: "sn:popup:waterRiskReady", diff --git a/apps/sn/src/features/windowPopup/ui/GisEditSessionPopupContent.tsx b/apps/sn/src/features/windowPopup/ui/GisEditSessionPopupContent.tsx index 782792865..a6e9671ae 100644 --- a/apps/sn/src/features/windowPopup/ui/GisEditSessionPopupContent.tsx +++ b/apps/sn/src/features/windowPopup/ui/GisEditSessionPopupContent.tsx @@ -57,6 +57,9 @@ export default function GisEditSessionPopupContent(_props: WindowPopupContentPro { + postMessageToOpener(WINDOW_POPUP_MESSAGE_TYPE.GIS_EDIT_SESSION_REMOVE_FOCUSED_CHANGE); + }} onRemoveLastChange={() => { postMessageToOpener(WINDOW_POPUP_MESSAGE_TYPE.GIS_EDIT_SESSION_REMOVE_LAST_CHANGE); }}