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 (
-
+ onActivate(tab.id)}
+ >
+ {tab.displayLabel}
+
+
);
})}
@@ -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으로 바꾸는 안을 검토한다.
+- 정책 확정 전에는 현행 동작을 유지하며 이번 라운드에서 추가 코드 변경은 하지 않았다.