diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 9009e71c2..45765f19f 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -168,14 +168,23 @@ const WATER_RISK_GRID_LAYER_ID = "SN_WATER_RISK_GRID"; const WATER_RISK_PIPE_LAYER_ID = "SN_WATER_RISK_PIPE"; const FLOOD_HISTORY_SELECTION_PADDING: [number, number, number, number] = [100, 380, 120, 120]; const RIGHT_PANEL_TAB_LIMIT = 5; +const RIGHT_PANEL_PIPE_TAB_ID = "feature:tb_ms_swage_ppl:singleton"; type RightPanelTabKind = "feature" | "sinkhole" | "gisEdit"; +type RightPanelTabCategory = + | "sinkhole" + | "pipe" + | "treatmentFacility" + | "treatmentZone" + | "defaultFeature" + | "gisEdit"; type RightPanelTab = { id: string; kind: RightPanelTabKind; - title: string; - subTitle?: string; + category: RightPanelTabCategory; + displayLabel: string; + parentTabId?: string | null; featureData: any; sinkholeDetail?: SinkholeDetail | null; nearbyPipes?: SinkholeNearbyPipe[]; @@ -183,10 +192,47 @@ type RightPanelTab = { lastActiveAt: number; }; +type RightPanelTabStyle = { + active: string; + inactive: string; +}; + type RightPanelBookmarkTabsProps = { tabs: RightPanelTab[]; activeTabId: string | null; onActivate: (tabId: string) => void; + onBack: (tabId: string) => void; +}; + +// 유형별 의미가 색상과 탭 이름에 함께 드러나도록 정적 class를 사용한다. +const RIGHT_PANEL_TAB_STYLE_BY_CATEGORY: Record< + RightPanelTabCategory, + RightPanelTabStyle +> = { + sinkhole: { + active: "border-[#d97706] bg-[#d97706] text-white", + inactive: "border-[#fdba74] bg-[#fff7ed] text-[#9a3412] hover:bg-[#ffedd5]", + }, + pipe: { + active: "border-[#2563eb] bg-[#2563eb] text-white", + inactive: "border-[#93c5fd] bg-[#eff6ff] text-[#1e40af] hover:bg-[#dbeafe]", + }, + treatmentFacility: { + active: "border-[#15803d] bg-[#15803d] text-white", + inactive: "border-[#86efac] bg-[#f0fdf4] text-[#166534] hover:bg-[#dcfce7]", + }, + treatmentZone: { + active: "border-[#7e22ce] bg-[#7e22ce] text-white", + inactive: "border-[#d8b4fe] bg-[#faf5ff] text-[#6b21a8] hover:bg-[#f3e8ff]", + }, + gisEdit: { + active: "border-[#0f766e] bg-[#0f766e] text-white", + inactive: "border-[#5eead4] bg-[#f0fdfa] text-[#115e59] hover:bg-[#ccfbf1]", + }, + defaultFeature: { + active: "border-[#475569] bg-[#475569] text-white", + inactive: "border-[#cbd5e1] bg-[#f8fafc] text-[#334155] hover:bg-[#f1f5f9]", + }, }; // 탭 제한을 넘으면 새 탭과 현재 탭을 보존하고 오래된 일반 상세부터 정리한다. @@ -196,6 +242,14 @@ const findRightPanelTabOverflowCandidate = ( activeTabId: string | null, ) => { const protectedTabIds = new Set([nextTabId, activeTabId].filter(Boolean) as string[]); + [nextTabId, activeTabId].forEach((tabId) => { + const pipeTab = tabs.find((tab) => tab.id === tabId && tab.category === "pipe"); + const parentTabId = pipeTab?.parentTabId; + if (parentTabId && tabs.some((tab) => tab.id === parentTabId)) { + protectedTabIds.add(parentTabId); + } + }); + const removableTabs = tabs .filter((tab) => !protectedTabIds.has(tab.id)) .sort((a, b) => a.lastActiveAt - b.lastActiveAt); @@ -210,34 +264,52 @@ const findRightPanelTabOverflowCandidate = ( }; // 오른쪽 상세 패널에 상세가 2개 이상 쌓였을 때만 책갈피형 전환 버튼을 보여준다. -const RightPanelBookmarkTabs = ({ tabs, activeTabId, onActivate }: RightPanelBookmarkTabsProps) => { +const RightPanelBookmarkTabs = ({ + tabs, + activeTabId, + onActivate, + onBack, +}: RightPanelBookmarkTabsProps) => { if (tabs.length < 2) return null; return ( -
+
{tabs.map((tab) => { const isActive = tab.id === activeTabId; + const style = RIGHT_PANEL_TAB_STYLE_BY_CATEGORY[tab.category]; + const parentTab = tab.parentTabId + ? tabs.find((candidate) => candidate.id === tab.parentTabId) + : null; + return ( - ) : null} - + +
); })}
@@ -860,23 +932,55 @@ const getRightPanelFeatureTitle = (data: any) => { } }; -const getRightPanelFeatureSubTitle = (data: any) => { +// OpenLayers Feature와 plain object 양쪽에서 탭 표시값 후보를 같은 순서로 읽는다. +const getRightPanelFeatureProperty = (data: any, keys: string[]) => { + for (const key of keys) { + const value = data?.get?.(key) ?? data?.values_?.[key] ?? data?.[key]; + const normalizedValue = String(value ?? "").trim(); + if (normalizedValue) return normalizedValue; + } + return ""; +}; + +const getRightPanelTabCategory = (data: any): RightPanelTabCategory => { const layerTypeName = getSelectedFeatureLayerTypeName(data); - if (layerTypeName !== "tb_ms_swage_ppl") return ""; + if (layerTypeName === "tb_ms_swage_ppl") return "pipe"; + if (layerTypeName === "tb_il_swage_prcs_fclt") return "treatmentFacility"; + if (layerTypeName === "tb_ms_swage_prcs_zone") return "treatmentZone"; + return "defaultFeature"; +}; - const pipeManagementNo = - data?.get?.("fclt_mng_no") ?? - data?.get?.("fcltMngNo") ?? - data?.get?.("pipeId") ?? - data?.values_?.fclt_mng_no ?? - data?.values_?.fcltMngNo ?? - data?.values_?.pipeId ?? - data?.fcltMngNo ?? - data?.pipeId ?? - data?.fid ?? - ""; +const getRightPanelFeatureDisplayLabel = (data: any, category: RightPanelTabCategory) => { + if (category === "pipe") { + const managementNo = + getRightPanelFeatureProperty(data, ["fclt_mng_no", "fcltMngNo", "pipeId", "fid"]) || + getRightPanelFeatureIdentifier(data) || + "-"; + return `관로(${managementNo})`; + } - return String(pipeManagementNo).trim(); + if (category === "treatmentFacility") { + const facilityName = + getRightPanelFeatureProperty(data, ["swtrmt_fclt_nm", "swtrmtFcltNm"]) || + getRightPanelFeatureProperty(data, ["swtrmt_fclt_cd", "swtrmtFcltCd"]) || + "-"; + return `처리시설(${facilityName})`; + } + + if (category === "treatmentZone") { + const zoneName = + getRightPanelFeatureProperty(data, ["swtrmt_zone_nm", "swtrmtZoneNm"]) || + getRightPanelFeatureProperty(data, [ + "swtrmt_zone_sn", + "swtrmtZoneSn", + "swtrmt_fclt_cd", + "swtrmtFcltCd", + ]) || + "-"; + return `처리구역(${zoneName})`; + } + + return getRightPanelFeatureTitle(data); }; const createFloodHistoryAreaStyle = (label: string) => @@ -2422,6 +2526,8 @@ const Map = () => { // 오른쪽 상세 패널에 열린 상세 컴포넌트를 책갈피 탭 단위로 보관한다. const [rightPanelTabs, setRightPanelTabs] = useState([]); const [activeRightPanelTabId, setActiveRightPanelTabId] = useState(null); + // 단일 관로 탭이 어느 지반침하 상세에서 열렸는지 비동기 피처 조회 사이에 보관한다. + const pipeParentTabIdRef = useRef(null); const [isWaterDepthLayerActive, setIsWaterDepthLayerActive] = useState(false); // 침수심 레이어 활성 여부 const [isPlaying, setIsPlaying] = useState(false); const [mapReady, setMapReady] = useState(false); @@ -2499,7 +2605,6 @@ const Map = () => { const sinkholeSelectedSourceRef = useRef(null); const sinkholeFocusedPipeLayerRef = useRef | null>(null); const sinkholeFocusedPipeSourceRef = useRef(null); - const restoreSinkholeDetailOnCloseRef = useRef(false); // 유하방향 레이어 체크 여부를 React 렌더링 흐름에 반영하기 위한 state이다. const [isFlowDirectionChecked, setIsFlowDirectionChecked] = useState(false); // 체크된 유하방향 트리 노드의 레이어 설정을 보관해 API 조회와 vector layer 스타일에 사용한다. @@ -3596,6 +3701,10 @@ const Map = () => { // SearchModal, GisContent 등에서 호출 const onFeatureSelect = (data: any) => { const clickedLayerTypeName = getSelectedFeatureLayerTypeName(data); + if (clickedLayerTypeName === "tb_ms_swage_ppl") { + pipeParentTabIdRef.current = null; + } + // 지형에 있는 피처를 직접 선택하면 지반침하 반경 내에서 만든 임시 관로 강조는 해제한다. clearSinkholeFocusedPipeLayer(); if (rdiiResultMap.handleOverlayFeatureSelect(data)) { @@ -3625,7 +3734,6 @@ const Map = () => { } if (clickedLayerTypeName === SINKHOLE_LAYER_TYPE) { - restoreSinkholeDetailOnCloseRef.current = false; const acdntNo = getSinkholeAccidentId(data); if (acdntNo) { void loadSinkholeDetail(acdntNo, false, getSinkholePointFromSelection(data), getSinkholeFallbackDetailFromSelection(data)); @@ -3836,8 +3944,6 @@ const Map = () => { console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); } - restoreSinkholeDetailOnCloseRef.current = - clickedLayerTypeName === "tb_ms_swage_ppl" && Boolean(selectedSinkholeDetail); setFeatureData(data); setIsOpen(true); @@ -4324,7 +4430,6 @@ const Map = () => { clearSinkholeSelectionLayer(); clearSinkholeFocusedPipeLayer(); - restoreSinkholeDetailOnCloseRef.current = false; if (getSelectedFeatureLayerTypeName(featureData) === SINKHOLE_LAYER_TYPE) { setFeatureData(null); setIsOpen(false); @@ -4530,6 +4635,11 @@ const Map = () => { return; } + const activeSinkholeTab = rightPanelTabs.find( + (tab) => tab.id === activeRightPanelTabId && tab.category === "sinkhole", + ); + pipeParentTabIdRef.current = activeSinkholeTab?.id ?? null; + try { const layerTypeName = String(pipe?.lyrTypeNm ?? "tb_ms_swage_ppl"); const params = new URLSearchParams({ @@ -4551,7 +4661,6 @@ const Map = () => { const firstFeature = features[0]; if (firstFeature) { syncRightPanelFeatureSelectionOnMap(firstFeature); - restoreSinkholeDetailOnCloseRef.current = false; setFeatureData(firstFeature); setIsOpen(true); setIsSearchModalOpen(false); @@ -4560,7 +4669,12 @@ const Map = () => { console.error("지반침하 반경 내 관로 지도 표시 실패:", error); alert("선택한 관로를 지도에서 표시하지 못했습니다."); } - }, [setIsSearchModalOpen, syncRightPanelFeatureSelectionOnMap]); + }, [ + activeRightPanelTabId, + rightPanelTabs, + setIsSearchModalOpen, + syncRightPanelFeatureSelectionOnMap, + ]); const buildRightPanelTab = useCallback( (data: any): RightPanelTab | null => { @@ -4577,8 +4691,9 @@ const Map = () => { return { id: `sinkhole:${acdntNo}`, kind: "sinkhole", - title: "지반침하", - subTitle: acdntNo, + category: "sinkhole", + displayLabel: `지반침하(${acdntNo})`, + parentTabId: null, featureData: { lyrTypeNm: SINKHOLE_LAYER_TYPE, acdntNo, @@ -4593,19 +4708,26 @@ const Map = () => { return { id: `gisEdit:${gisEditDetailChangeId}`, kind: "gisEdit", - title: "관로편집", + category: "gisEdit", + displayLabel: "관로편집", + parentTabId: null, featureData: data, gisEditChangeId: gisEditDetailChangeId, lastActiveAt: now, }; } + const category = getRightPanelTabCategory(data); const featureId = getRightPanelFeatureIdentifier(data) || `${layerTypeName || "feature"}:${now}`; return { - id: `feature:${layerTypeName || "unknown"}:${featureId}`, + id: + category === "pipe" + ? RIGHT_PANEL_PIPE_TAB_ID + : `feature:${layerTypeName || "unknown"}:${featureId}`, kind: "feature", - title: getRightPanelFeatureTitle(data), - subTitle: getRightPanelFeatureSubTitle(data), + category, + displayLabel: getRightPanelFeatureDisplayLabel(data, category), + parentTabId: category === "pipe" ? pipeParentTabIdRef.current : null, featureData: data, lastActiveAt: now, }; @@ -4644,6 +4766,9 @@ const Map = () => { currentTabs.map((tab) => (tab.id === tabId ? { ...tab, lastActiveAt: Date.now() } : tab)), ); setActiveRightPanelTabId(tabId); + if (targetTab.category === "pipe") { + pipeParentTabIdRef.current = targetTab.parentTabId ?? null; + } if (targetTab.kind === "sinkhole") { const detail = targetTab.sinkholeDetail ?? null; @@ -4665,14 +4790,22 @@ const Map = () => { setIsOpen(true); setIsSearchModalOpen(false); }, - [renderSelectedSinkhole, rightPanelTabs, setIsSearchModalOpen, sinkholeRadiusMeters, syncRightPanelFeatureSelectionOnMap], + [ + renderSelectedSinkhole, + rightPanelTabs, + setIsSearchModalOpen, + sinkholeRadiusMeters, + syncRightPanelFeatureSelectionOnMap, + ], ); const closeRightPanelTab = useCallback( (tabId: string | null) => { if (!tabId) return; const closingTab = rightPanelTabs.find((tab) => tab.id === tabId); - const remainingTabs = rightPanelTabs.filter((tab) => tab.id !== tabId); + const remainingTabs = rightPanelTabs + .filter((tab) => tab.id !== tabId) + .map((tab) => (tab.parentTabId === tabId ? { ...tab, parentTabId: null } : tab)); if (closingTab?.kind === "sinkhole") { setSelectedSinkholeDetail(null); @@ -4683,23 +4816,30 @@ const Map = () => { const select = GisApp.Module.select as any; select?.resetSelectedFeature?.(); clearSinkholeFocusedPipeLayer(); - } if (closingTab?.kind === "gisEdit") { setGisEditDetailChangeId(null); } + if (closingTab?.category === "pipe" || pipeParentTabIdRef.current === tabId) { + pipeParentTabIdRef.current = null; + } setRightPanelTabs(remainingTabs); if (remainingTabs.length === 0) { setActiveRightPanelTabId(null); - restoreSinkholeDetailOnCloseRef.current = false; + pipeParentTabIdRef.current = null; setFeatureData(null); setIsOpen(false); return; } - const fallbackTab = [...remainingTabs].sort((a, b) => b.lastActiveAt - a.lastActiveAt)[0]; + const parentFallbackTab = + closingTab?.category === "pipe" && closingTab.parentTabId + ? remainingTabs.find((tab) => tab.id === closingTab.parentTabId) + : null; + const fallbackTab = + parentFallbackTab ?? [...remainingTabs].sort((a, b) => b.lastActiveAt - a.lastActiveAt)[0]!; activateRightPanelTab(fallbackTab.id); }, [activateRightPanelTab, clearSinkholeFocusedPipeLayer, clearSinkholeSelectionLayer, rightPanelTabs], @@ -4713,6 +4853,7 @@ const Map = () => { useEffect(() => { if (isOpen) return; + pipeParentTabIdRef.current = null; setRightPanelTabs([]); setActiveRightPanelTabId(null); }, [isOpen]); @@ -5817,6 +5958,7 @@ const Map = () => { tabs={rightPanelTabs} activeTabId={activeRightPanelTabId} onActivate={activateRightPanelTab} + onBack={activateRightPanelTab} /> ); const closeActiveTab = () => { @@ -5867,7 +6009,6 @@ const Map = () => { featureData={panelFeatureData} onClose={() => { setGisEditDetailChangeId(null); - restoreSinkholeDetailOnCloseRef.current = false; closeActiveTab(); }} /> 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 4add72b3d..1921ed7b0 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 @@ -95,3 +95,12 @@ - 연도 범위 적용 후 지도 조회 요청의 `startYmd/endYmd`가 시작년도 `0101`, 종료년도 `1231`로 변환되는지 확인한다. - 목록 팝업을 열었을 때 현재 지도 연도 범위가 초기 조회조건으로 전달되는지 확인한다. - 잘못된 연도 입력(4자리 미만, 시작년도 > 종료년도, 2016년 이전, 현재년도 초과)이 차단되는지 확인한다. +### 3.5 2026-07-13 우측 상세 탭 유지보수 검증 계획 + +- 지반침하 A/B를 열었을 때 사고번호별 탭 2개가 유지되는지 확인한다. +- 지반침하에서 관로 1/2를 연속 선택해도 관로 탭은 1개이며 마지막 관리번호와 상세로 교체되는지 확인한다. +- 관로 표시명 옆의 소형 `←`가 직전 부모 지반침하로 이동하고 지도 관로 포커스와 반경 표시가 함께 복원되는지 확인한다. +- 지도에서 직접 선택한 관로에는 `←`가 표시되지 않는지 확인한다. +- 처리시설/처리구역 탭에 실제 시설명/구역명과 유형별 색상이 표시되는지 확인한다. +- 부모 지반침하를 닫으면 관로의 부모 링크가 사라지고, 최대 5개 자동 정리에서는 활성 관로의 부모가 보호되는지 확인한다. +- 자동 검증은 `diff --check`, TypeScript, Next build로 수행하고, 화면 상호작용은 로그인 가능한 `/map` 통합 환경에서 수동 수행한다. 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 00630e8a8..d8954d398 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 @@ -260,3 +260,36 @@ - JIS 우선 구현은 최신 사고번호 대응에는 유효하다. - 구 사고번호 공백을 줄이려면 주소 fallback을 함께 유지해야 한다. - 남는 110건은 단순 주소 정제로 해결되지 않아 별도 데이터 보강 또는 수기 좌표 보정 대상으로 분류한다. +### 2.25 2026-07-13 우측 상세 탭 유지보수 검증 결과 + +- `git diff --check`: 통과. +- TypeScript + - 기본 `tsc`는 기존 `ignoreDeprecations: "6.0"`과 TypeScript 5.9.3 불일치로 시작 단계에서 실패했다. + - `--ignoreDeprecations 5.0`으로 재실행해 변경된 `map/page.tsx` 신규 오류 0건을 확인했다. + - 전체 종료는 기존 공통 컴포넌트, floodHistory, `app/html/**`, 공통 chart 파일의 선행 오류 때문에 실패했다. +- Next build + - optimized production compile 28.1초 성공. + - 정적 페이지 19개 생성 성공. + - standalone trace 복사에서 Windows symlink 권한 `EPERM`이 발생해 최종 exit code 1. +- ESLint + - 파일 단위 실행 전 기존 `eslint.config.mjs` 로딩 TypeError가 발생해 미실행. +- 브라우저 + - 로컬 dev 서버는 6.2초에 Ready. + - `/map` 최초 컴파일이 1분 이상 완료되지 않아 브라우저 탐색과 실제 탭 클릭은 환경 차단. +- 정적 코드 확인 + - 관로 singleton ID, 부모 ID 저장/정리, 관로 표시명 옆 소형 뒤로가기 버튼, 유형별 색상 map, 괄호형 표시명 resolver가 구현된 것을 확인했다. + - BE API·DTO·DB 변경이 없음을 확인했다. +- 판정 + - 코드 구현 및 컴파일: 완료. + - 실브라우저 상태 전이: 후속 수동 검증 필요. + +### 2.26 2026-07-14 부모 복귀 화살표 및 처리구역 API 회귀 검증 + +- `RightPanelBookmarkTabs`에서 화살표 조건이 `pipe && isActive && parentTab`으로 제한됐고 크기가 `h-6 w-6 text-sm`로 변경된 것을 확인했다. +- 부모 지반침하 탭 활성화 시 관로 탭의 `isActive`가 false가 되어 화살표가 숨겨지는 상태 전이를 정적으로 확인했다. +- 탭 최대 수는 5개이며 제거 후보 함수가 보호 ID와 `lastActiveAt` 및 탭 종류를 함께 사용하므로 엄격한 FIFO가 아님을 확인했다. +- TypeScript compiler API로 `page.tsx`를 TSX 파싱한 결과 diagnostics 0건이었다. +- 프론트 `git diff --check`는 통과했다. +- 백엔드 `compileJava`는 성공했다. +- 사용자 요청에 따라 서버와 브라우저는 실행하지 않았다. +- 남은 수동 검증은 실행 중 통합 환경에서 관로→부모 지반침하 복귀 후 화살표 숨김, 처리구역 클릭 200 응답과 분석 표 표시를 확인하는 것이다. 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 8bcb32366..a16aab3ca 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 @@ -206,3 +206,18 @@ - 관리자용 동기화 버튼/최근 동기화 요약이 “JIS backfill 결과” 중심 문구로 재정의되는 경우 - 상세 실패 메시지를 주소 geocoding 실패가 아니라 사고번호 기반 조회 실패 문구로 바꾸는 경우 - 결론적으로 이번 라운드의 FE 기본 입장은 `계약 유지 시 무코드 대응`, `표시 문구/운영성만 후속 점검`이다. +## 12. 2026-07-13 우측 상세 탭 유지보수 구현 범위 + +- `map/page.tsx`의 기존 공통 우측 탭을 확장해 지반침하·관로·처리시설·처리구역을 유형별 색상과 명칭으로 구분한다. +- 지반침하는 사고번호별 다중 탭, 관로는 전역 단일 탭으로 관리한다. +- 지반침하 반경 내 관로에서 연 경우에만 `관로(관리번호)` 표시명 옆에 부모 지반침하로 돌아가는 소형 화살표를 노출한다. +- 탭 이름은 `지반침하(사고번호)`, `관로(관리번호)`, `처리시설(시설명)`, `처리구역(구역명)` 형식을 사용한다. +- 기존 응답과 GeoServer 피처 속성으로 구현 가능하므로 BE API·DTO·DB 변경은 하지 않는다. +- 완료 기준은 변경 파일 타입 오류 0건, Next 컴파일 성공, 부모/단일 탭 상태 전이 정적 확인이며 실브라우저 상호작용은 로그인 가능한 통합 환경에서 후속 확인한다. + +## 13. 처리시설·처리구역 탭 정책 후속 검토 + +- 현재 처리시설·처리구역 탭은 `feature:{layerType}:{featureId}` ID를 사용해 피처별로 여러 개 열릴 수 있다. +- 후속 결정 후보는 `현행 다중 탭 유지`와 `처리시설 1개 + 처리구역 1개 유형별 singleton`이다. +- singleton을 선택하면 동일 유형의 다음 선택은 새 탭을 추가하지 않고 기존 탭의 표시명, feature payload, 활성 시각을 최신 대상으로 교체한다. +- 이 정책은 아직 미확정이며 현재 코드 변경 대상이 아니다. 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 f01a7a5a1..7773978e7 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 @@ -424,3 +424,32 @@ - 이 오류는 JIS 프론트 지도 이동 스크립트 문제로 보이며, 우리 FE가 받는 백엔드 DTO 실패와는 직접 원인이 다르다. - 실제 잔여 실패 159건은 대부분 JIS 구 사고번호 응답 자체가 `fail`이거나 주소 fallback도 실패하는 케이스로 확인됐기 때문에, 현재 FE 수정 포인트는 없다. - FE 후속 액션은 백엔드가 `JIS 우선 + 주소 fallback`을 최종 운영안으로 채택할 때도 기존 `coordStatus`, `geocodeErrorMessage` 표시만 회귀 없이 유지되는지 확인하는 정도다. +### 4.50 2026-07-13 우측 상세 탭 유형·단일 관로·부모 복귀 구현 + +- `RightPanelTab`에 렌더링 종류와 별개인 `category`, 사용자 표시용 `displayLabel`, 관로의 `parentTabId`를 추가했다. +- 지반침하, 관로, 처리시설, 처리구역, GIS 편집, 기타 피처별 정적 Tailwind class map을 두어 활성/비활성 색상을 구분했다. +- 기존 제목/보조값 2줄 구조를 괄호형 단일 표시명으로 바꾸고, 처리시설명·처리구역명 후보도 OpenLayers Feature/plain object 양쪽에서 읽도록 통합했다. +- 지반침하 탭 ID는 사고번호별로 유지하고, 관로 탭 ID는 `feature:tb_ms_swage_ppl:singleton`으로 고정해 마지막 선택 관로로 교체 갱신한다. +- 반경 내 관로 클릭 시 현재 활성 지반침하 탭을 부모로 기록하고, 부모 탭이 남아 있을 때만 `관로(관리번호)` 표시명 바로 옆에 20px 소형 `←` 버튼을 표시한다. +- 화살표 클릭은 관로 탭을 유지한 채 부모 지반침하 탭을 활성화하고 지도 반경/선택 동기화 경로를 재사용한다. +- 관로 탭 닫기는 부모 지반침하를 MRU보다 우선하고, 부모 탭 닫기는 남은 관로의 부모 링크를 제거한다. +- 최대 5개 자동 정리 시 활성 관로 탭의 유효한 부모는 제거 후보에서 보호한다. +- 지도에서 관로를 직접 선택하면 부모 ref를 비워 이전 지반침하 복귀 링크가 재사용되지 않게 했다. +- 읽히지 않던 `restoreSinkholeDetailOnCloseRef`는 제거했다. +- 변경 파일은 `FE_APP_ROOT/app/(map-layout)/map/page.tsx` 1개이며 BE API·DTO·Repository·DB 변경은 없다. + +### 4.51 2026-07-14 부모 복귀 화살표 및 처리구역 API 후속 유지보수 + +- 관로 탭 내부의 부모 복귀 화살표를 20px에서 24px로 키워 현재 배치 안에서 시인성과 클릭 영역을 소폭 확대했다. +- 화살표 표시 조건에 `isActive`를 추가했다. 따라서 부모가 연결된 관로 탭을 보고 있을 때만 화살표가 보이고, 화살표를 눌러 부모 지반침하 탭이 활성화되면 관로 탭의 화살표는 숨겨진다. +- 최대 5개 탭의 overflow 로직을 재확인했다. 현재 정책은 FIFO가 아니라 신규/활성/관로 부모 탭을 보호하고 `lastActiveAt`이 오래된 일반 feature 탭을 우선 제거하는 보호형 LRU다. +- 하수처리구역 클릭 500 오류의 FE 호출 경로는 변경하지 않았다. `swtrmtPrcsZoneAnalysisApi.ts`의 기존 API 계약은 정상이며, 삭제된 백엔드 Controller/Service를 복구해 계약을 다시 연결했다. +- 프론트 변경 파일은 `FE_APP_ROOT/app/(map-layout)/map/page.tsx` 1개다. +- DB 변경은 없다. + +### 4.52 2026-07-14 현재 완료 범위 및 후속 탭 정책 메모 + +- 현재 완료 범위는 유형별 색상·괄호형 탭 이름, 지반침하 다중 탭, 관로 singleton, 부모 지반침하 복귀, 활성 관로에서만 보이는 24px 화살표, 최대 5개 보호형 LRU 정리다. +- 처리시설과 처리구역은 현재 피처별 다중 탭으로 동작한다. +- 후속 운영 결과에 따라 처리시설 1개와 처리구역 1개를 각각 singleton으로 바꾸는 안을 검토한다. +- 정책 확정 전에는 현행 동작을 유지하며 이번 라운드에서 추가 코드 변경은 하지 않았다.