지반침하 연계 관로 탭의 부모 종속 제거 처리

This commit is contained in:
kiminchan
2026-07-15 11:34:06 +09:00
parent fdfb37d4d0
commit d062b8a7d4
5 changed files with 63 additions and 37 deletions
+21 -37
View File
@@ -170,7 +170,8 @@ 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";
// Only pipes opened from sinkhole details share one slot within the parent-return flow.
const RIGHT_PANEL_SINKHOLE_PIPE_TAB_ID = "feature:tb_ms_swage_ppl:sinkhole-singleton";
type RightPanelTabKind = "feature" | "sinkhole" | "gisEdit";
type RightPanelTabCategory =
@@ -237,34 +238,20 @@ const RIGHT_PANEL_TAB_STYLE_BY_CATEGORY: Record<
},
};
// 탭 제한을 넘으면 새 탭과 현재 탭을 보존하고 오래된 일반 상세부터 정리한다.
// 공용 우측 탭은 유형과 활성 상태에 관계없이 먼저 열린 탭부터 제거한다.
const findRightPanelTabOverflowCandidate = (
tabs: RightPanelTab[],
nextTabId: string,
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);
return (
removableTabs.find((tab) => tab.kind === "feature") ??
removableTabs.find((tab) => tab.kind !== "gisEdit") ??
removableTabs[0] ??
tabs.filter((tab) => tab.id !== nextTabId).sort((a, b) => a.lastActiveAt - b.lastActiveAt)[0] ??
null
);
return tabs.find((tab) => tab.id !== nextTabId) ?? null;
};
// 부모 지반침하가 제거되면 그 부모에서 열린 연계 관로도 같은 탭 그룹으로 제거한다.
const removeRightPanelTabGroup = (tabs: RightPanelTab[], removedTabId: string) =>
tabs.filter(
(tab) => tab.id !== removedTabId && tab.parentTabId !== removedTabId,
);
// 오른쪽 상세 패널에 상세가 2개 이상 쌓였을 때만 책갈피형 전환 버튼을 보여준다.
const RightPanelBookmarkTabs = ({
tabs,
@@ -291,7 +278,7 @@ const RightPanelBookmarkTabs = ({
isActive ? style.active : style.inactive,
].join(" ")}
>
{tab.category === "pipe" && isActive && parentTab ? (
{tab.category === "pipe" && parentTab ? (
<button
type="button"
className="mr-1 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-sm font-bold transition-colors hover:bg-black/10 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
@@ -4750,15 +4737,17 @@ const Map = () => {
const category = getRightPanelTabCategory(data);
const featureId = getRightPanelFeatureIdentifier(data) || `${layerTypeName || "feature"}:${now}`;
// Direct map selections accumulate per pipe; only a pipe with a sinkhole parent replaces the linked slot.
const pipeParentTabId = category === "pipe" ? pipeParentTabIdRef.current : null;
return {
id:
category === "pipe"
? RIGHT_PANEL_PIPE_TAB_ID
category === "pipe" && pipeParentTabId
? RIGHT_PANEL_SINKHOLE_PIPE_TAB_ID
: `feature:${layerTypeName || "unknown"}:${featureId}`,
kind: "feature",
category,
displayLabel: getRightPanelFeatureDisplayLabel(data, category),
parentTabId: category === "pipe" ? pipeParentTabIdRef.current : null,
parentTabId: pipeParentTabId,
featureData: data,
lastActiveAt: now,
};
@@ -4779,14 +4768,13 @@ const Map = () => {
const overflowCandidate = findRightPanelTabOverflowCandidate(
mergedTabs,
nextTab.id,
activeRightPanelTabId,
);
if (!overflowCandidate) return mergedTabs.slice(-RIGHT_PANEL_TAB_LIMIT);
return mergedTabs.filter((item) => item.id !== overflowCandidate.id);
return removeRightPanelTabGroup(mergedTabs, overflowCandidate.id);
});
setActiveRightPanelTabId(tab.id);
}, [activeRightPanelTabId]);
}, []);
const activateRightPanelTab = useCallback(
(tabId: string) => {
@@ -4834,9 +4822,7 @@ const Map = () => {
(tabId: string | null) => {
if (!tabId) return;
const closingTab = rightPanelTabs.find((tab) => tab.id === tabId);
const remainingTabs = rightPanelTabs
.filter((tab) => tab.id !== tabId)
.map((tab) => (tab.parentTabId === tabId ? { ...tab, parentTabId: null } : tab));
const remainingTabs = removeRightPanelTabGroup(rightPanelTabs, tabId);
if (closingTab?.kind === "sinkhole") {
setSelectedSinkholeDetail(null);
@@ -4886,11 +4872,9 @@ const Map = () => {
if (hasSinkholeTab && activeRightPanelTabId === sinkholeTabId) {
closeRightPanelTab(sinkholeTabId);
} else {
// 비활성 지반침하 탭은 현재 활성 탭을 바꾸지 않고 제거하며 종속 관로의 부모 연결만 해제한다.
// 비활성 지반침하 탭은 현재 활성 탭을 바꾸지 않고 종속 관로와 함께 제거한다.
setRightPanelTabs((currentTabs) =>
currentTabs
.filter((tab) => tab.id !== sinkholeTabId)
.map((tab) => (tab.parentTabId === sinkholeTabId ? { ...tab, parentTabId: null } : tab)),
removeRightPanelTabGroup(currentTabs, sinkholeTabId),
);
setSelectedSinkholeDetail(null);
setSelectedSinkholeNearbyPipes([]);
@@ -128,3 +128,15 @@
7. 해제 후 다른 지반침하 포인트를 다시 선택해 상세와 반경 표시가 정상적으로 열리는지 확인한다.
- 이 절차는 지도 포인트 선택 후 해제를 검증한다. 목록 행 선택만 한 상태는 지도 선택 모듈의 빈 선택 이벤트 대상이 아니므로 별도 시나리오로 구분한다.
### 3.8 2026-07-15 관로 탭 진입 경로 분리 검증 계획
1. 지반침하와 무관한 지도 관로 A, B를 차례로 직접 선택해 관로 탭 2개가 각각 유지되는지 확인한다.
2. 같은 일반 관로를 다시 선택하면 중복 탭을 만들지 않고 기존 탭이 활성화되는지 확인한다.
3. 지반침하 상세에서 반경 내 관로 A, B를 차례로 선택해 부모 화살표가 있는 관로 탭 1개의 표시명과 상세만 B로 교체되는지 확인한다.
4. 화살표를 클릭하면 직전 부모 지반침하 탭으로 복귀하는지 확인한다.
5. 일반 관로 탭을 활성화해도 비활성 지반침하 연계 관로 탭의 화살표가 계속 표시되는지 확인한다.
6. 지도에서 직접 선택한 일반 관로 탭에는 화살표가 표시되지 않는지 확인한다.
7. `지반침하 + 연계 관로 + 일반 관로 3개` 상태에서 새 일반 관로를 열면 가장 오래된 부모 지반침하와 연계 관로가 함께 제거되고 일반 관로 4개가 남는지 확인한다.
8. 연계 관로 탭만 먼저 FIFO 또는 수동으로 제거할 때 부모 지반침하 탭은 유지되는지 확인한다.
9. 정적 검사는 TSX 구문 진단과 `git diff --check`로 수행하고, 상호작용은 로그인 가능한 `/map`에서 수동 확인한다.
@@ -321,3 +321,15 @@
- 지도에서 선택한 지반침하가 빈 지도 영역 클릭으로 정상 해제되고 관련 반경·강조 표시도 함께 정리됨을 확인했다.
- 지반침하발생현황 목록 첫 열에 사고번호가 표시되고 화면 `NO` 열이 제거된 것을 확인했다.
- 이번 유지보수 요구사항 전체가 성공했으므로 최종 판정은 `완료`다.
### 2.30 2026-07-15 관로 탭 진입 경로 분리 정적 검증 결과
- `buildRightPanelTab`에서 부모가 있는 관로만 `RIGHT_PANEL_SINKHOLE_PIPE_TAB_ID`를 사용하고 일반 관로는 피처 식별값 기반 ID를 사용하는 것을 확인했다.
- 지도 직접 선택 시 `pipeParentTabIdRef.current = null`로 부모 연결을 해제하는 기존 경로를 확인했다.
- 부모 탭이 있는 관로는 활성 여부와 무관하게 `←` 버튼을 렌더링하도록 변경된 것을 확인했다.
- 자동 정리 후보가 탭 배열의 첫 항목을 선택해 유형·활성 상태와 관계없이 FIFO로 동작하는 것을 확인했다.
- `removeRightPanelTabGroup`이 제거 대상 ID와 그 ID를 `parentTabId`로 가진 탭을 함께 제외해 부모 지반침하 제거 시 연계 관로도 사라지는 것을 확인했다.
- 연계 관로 자체를 제거 대상으로 전달하면 부모 탭은 조건에 걸리지 않아 유지되는 단방향 종속 구조를 확인했다.
- TypeScript compiler API의 `transpileModule`로 `page.tsx`를 TSX 파싱한 결과 error diagnostics 0건이었다.
- 프론트 변경 파일 대상 `git diff --check`는 통과했다.
- 로그인 세션이 필요한 실제 탭 누적·교체·복귀 상호작용은 수동 검증이 남아 있다.
@@ -264,3 +264,12 @@
- `packages/common/business_components/src/ui/map/lib/gis.select.js`
- BE API·DTO·Repository·DB는 변경하지 않았다.
## 15. 2026-07-15 관로 탭 누적 정책 후속 변경
- 기존의 관로 전역 단일 탭 정책을 진입 경로별 정책으로 변경한다.
- 지도에서 직접 선택한 일반 관로는 관로 식별값별 탭으로 누적한다.
- 지반침하 상세의 반경 내 관로에서 선택한 관로는 부모 지반침하가 있는 단일 슬롯을 공유하며, 다음 관로 선택 시 같은 탭의 상세를 교체한다.
- 부모가 있는 관로 탭은 활성 여부와 무관하게 `←` 버튼을 항상 표시해 일반 관로와 구분하고 지반침하 상세로 복귀한다.
- 공용 우측 탭은 최대 5개 FIFO를 적용하되, 제거 대상이 부모 지반침하이면 그 부모에서 열린 연계 관로도 함께 제거한다. 연계 관로만 제거될 때는 부모 지반침하를 유지한다.
- 변경 범위는 `app/(map-layout)/map/page.tsx`이며 BE API·DTO·DB 계약은 변경하지 않는다.
@@ -490,3 +490,12 @@
- 지도 지반침하 선택 후 빈 지도 영역 클릭 시 선택 상세, 반경 원·라벨, 종속 강조가 정상 해제되고 다른 탭과 기본 레이어는 유지됐다.
- 지반침하발생현황 목록은 사고번호가 첫 열로 표시되고 화면 `NO` 열은 제거된 상태로 정상 동작했다.
- BE API·DTO·Repository·DB 계약 변경 없이 최종 상태를 `완료`로 전환했다.
### 4.57 2026-07-15 일반 관로 다중·지반침하 연계 관로 단일 탭 분리
- 원인은 `buildRightPanelTab`이 모든 하수관로에 전역 singleton ID를 부여해 일반 지도 클릭도 기존 관로 탭을 덮어쓰던 것이었다.
- 일반 지도 선택은 `onFeatureSelect`에서 부모 참조를 비우는 기존 흐름을 유지하고, 관로 식별값 기반 탭 ID를 사용하도록 변경했다.
- 지반침하 상세의 반경 내 관로 선택은 `parentTabId`가 있을 때만 `sinkhole-singleton` ID를 사용해 관련 관로 탭 하나를 교체한다.
- 부모가 있는 지반침하 연계 관로는 비활성 상태에서도 `←`를 항상 표시해 일반 관로와 구분되도록 후속 보완했다.
- 탭 최대 5개 초과 시 가장 먼저 열린 탭을 제거하는 FIFO를 유지하고, 제거 대상이 부모 지반침하이면 그 부모를 참조하는 연계 관로 탭도 함께 제거하도록 공통 그룹 제거 함수를 적용했다. 연계 관로 단독 제거는 부모에 영향을 주지 않는다.
- BE API·DTO·Repository·DB 변경은 없다.