diff --git a/B05_Profile/B05_Profile_UI_Page.ts b/B05_Profile/B05_Profile_UI_Page.ts index d545d0fe..ba1a5267 100644 --- a/B05_Profile/B05_Profile_UI_Page.ts +++ b/B05_Profile/B05_Profile_UI_Page.ts @@ -27,7 +27,11 @@ import { createRouteProfilePanel } from "./B05_Profile_UI_Profile_Panel"; import { leaveForDashboard } from "../A00_Common/b_missing_data_guard"; import { navigateTo } from "../A00_Common/router"; import { createSelectionSync } from "./B05_Profile_UI_Selection"; -import { wireStructurePick } from "./B05_Profile_UI_Structure_Pick_Session"; +import { + restoreStructurePick, + wireStructurePick, + writeStructurePick, +} from "./B05_Profile_UI_Structure_Pick_Session"; import { createStructuresBridge } from "./B05_Profile_UI_Page_Structures"; import { createRouteViewer } from "./B05_Profile_UI_Viewer"; import { irregularStationId, isPipeStation } from "./B05_Profile_UI_IrregularStations"; @@ -57,7 +61,9 @@ import { DEFAULT_ROAD_WIDTHS, fetchRoadWidths, interpolateIrregularStations, + loadUphillOverrides, restorePoints, + saveUphillOverrides, toBounds, toGradeClass, } from "./B05_Profile_UI_Page_Helpers"; @@ -113,6 +119,7 @@ export async function renderB05Route(root: HTMLElement): Promise { options: pipe.options, })), ); + restorePick(); // 관 목록이 실린 뒤라야 폼이 그 시설을 찾는다. }, // 그래프 측점선은 이제 배관(계곡 통과 시설) 투영뿐이다 — 수동 구조물은 // 서클마크·구조물 배치 목록이 담당한다(2026-08-17 컨테이너 병합). @@ -175,21 +182,8 @@ export async function renderB05Route(root: HTMLElement): Promise { /* ── 측점 상단측(=측구 방향) 사용자 변경분 ───────────────────────────── * solve가 자동 판정한 uphill_side를 3D 램프 클릭으로 바꾼 값. 세션에 보관했다가 * 경로 확정 시 uphill_overrides로 백엔드/DB(종단 정본)에 병합한다. */ - const uphillSessionKey = `b05:uphill:${activeProjectId}`; - const uphillOverrides = new Map(); + const uphillOverrides = loadUphillOverrides(activeProjectId); const uphillKey = (chainage: number): string => chainage.toFixed(3); - try { - const raw = window.sessionStorage.getItem(uphillSessionKey); - if (raw) { - Object.entries(JSON.parse(raw) as Record).forEach( - ([chainage, side]) => { - if (side === "left" || side === "right") uphillOverrides.set(chainage, side); - }, - ); - } - } catch { - /* 손상된 세션 값은 무시 — 자동 판정값으로 재시작. */ - } const { syncIrregularSelection, syncBasinHighlight, selectStationOfPipe } = createSelectionSync({ stations: () => bridge.irregularStations(), @@ -208,15 +202,16 @@ export async function renderB05Route(root: HTMLElement): Promise { selectStructure3D: (chainageM) => viewer.structurePick.selectAtChainage(chainageM), }); + /** 세션에 남은 구조물 선택을 되살린다 — B06에서 고른 것도 그대로 이어 받는다 + * (2026-09-04). 이미 무언가 골라져 있으면 건드리지 않는다. */ + function restorePick(): void { + if (panel.structures.hasSelection()) return; + const pick = viewer.structurePick; + restoreStructurePick(pick, activeProjectId, panel.structures, selectStationOfPipe); + } + function persistUphillOverrides(): void { - try { - window.sessionStorage.setItem( - uphillSessionKey, - JSON.stringify(Object.fromEntries(uphillOverrides)), - ); - } catch { - /* 세션 저장 실패는 무시 — 값은 메모리에 유지된다. */ - } + saveUphillOverrides(activeProjectId, uphillOverrides); } /* ── 최신 경로/설정값 세션 캐시 ──────────────────────────────────────── @@ -291,6 +286,7 @@ export async function renderB05Route(root: HTMLElement): Promise { profilePanel.drainage.updatePipeFacility(from, to, attributes), onPipeFacilityRemove: (chainage) => profilePanel.drainage.removePipe(chainage), onPipeFacilitySelect: (chainage) => { + writeStructurePick(activeProjectId, chainage); // 목록 선택도 B06으로 이어진다. if (selectionSyncing) return; selectionSyncing = true; try { @@ -312,6 +308,8 @@ export async function renderB05Route(root: HTMLElement): Promise { selectionSyncing = false; } }, + // 3D·종단·유역도에서 고른 것이 폼에 실릴 때 좌측 패널을 펼친다(2026-09-04 사용자). + onStructureReveal: () => layout.setOptionsOpen(true), onStructureSelect: (structure) => { if (selectionSyncing) return; selectionSyncing = true; @@ -323,7 +321,6 @@ export async function renderB05Route(root: HTMLElement): Promise { }, }); - // 3D 코리도에서 부재를 고르면 좌측 「구조물 배치」 폼이 그 시설을 연다(2026-09-04). wireStructurePick(viewer.structurePick, activeProjectId, panel.structures, selectStationOfPipe); /** 입력·마커 변경 시 측점 라인만 다시 그린다 — 확정 게이트는 폐지(B06 통합 확정). @@ -692,5 +689,6 @@ export async function renderB05Route(root: HTMLElement): Promise { } finally { progress.remove(); restoring = false; + restorePick(); // 관·구조물 목록 중 늦게 오는 쪽이 있어 여기서 한 번 더. } } diff --git a/B05_Profile/B05_Profile_UI_Page_Helpers.ts b/B05_Profile/B05_Profile_UI_Page_Helpers.ts index 729ebf91..a623b345 100644 --- a/B05_Profile/B05_Profile_UI_Page_Helpers.ts +++ b/B05_Profile/B05_Profile_UI_Page_Helpers.ts @@ -242,3 +242,40 @@ export const FACILITY_NAMES: Record = { ford_bridge: "세월교", revetment: "기슭막이", }; + +/* ── 측점 상단측(=측구 방향) 사용자 변경분 세션 보관 ───────────────────── + * 3D 램프 클릭으로 바꾼 값. 경로 확정 때 uphill_overrides로 백엔드에 병합한다. + * 화면 본체가 700줄에 닿아 읽기·쓰기만 여기로 뺐다(2026-09-04, 동작 불변). */ +const uphillSessionKey = (projectId: string): string => `b05:uphill:${projectId}`; + +/** 세션에 남은 상단측 변경분을 읽는다. 손상된 값은 무시하고 빈 것으로 시작한다. */ +export function loadUphillOverrides(projectId: string): Map { + const overrides = new Map(); + try { + const raw = window.sessionStorage.getItem(uphillSessionKey(projectId)); + if (!raw) return overrides; + Object.entries(JSON.parse(raw) as Record).forEach( + ([chainage, side]) => { + if (side === "left" || side === "right") overrides.set(chainage, side); + }, + ); + } catch { + /* 손상된 세션 값은 무시 — 자동 판정값으로 재시작. */ + } + return overrides; +} + +/** 상단측 변경분을 세션에 적는다. 저장 실패는 무시한다(값은 메모리에 남는다). */ +export function saveUphillOverrides( + projectId: string, + overrides: ReadonlyMap, +): void { + try { + window.sessionStorage.setItem( + uphillSessionKey(projectId), + JSON.stringify(Object.fromEntries(overrides)), + ); + } catch { + /* 세션 저장 실패는 무시 — 값은 메모리에 유지된다. */ + } +} diff --git a/B05_Profile/B05_Profile_UI_Panel.ts b/B05_Profile/B05_Profile_UI_Panel.ts index 21ecbd2e..a9450ea7 100644 --- a/B05_Profile/B05_Profile_UI_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Panel.ts @@ -116,6 +116,8 @@ interface PanelCallbacks { onStructuresChange: (structures: StructureInstance[]) => void; /** 구조물을 목록에서 선택/해제할 때 해당 구조물(또는 null). */ onStructureSelect: (structure: StructureInstance | null) => void; + /** 바깥(3D·종단·유역도)에서 고른 것이 폼에 실릴 때 — 접힌 좌측 패널을 펼친다. */ + onStructureReveal?: () => void; /** 계곡 통과 시설 추가·수정·삭제·선택 — 관 지점 정본(배수유역 패널) 경유(2026-08-17 통합). */ onPipeFacilityAdd: (chainageM: number, attributes: FacilityAttributes) => void; onPipeFacilityUpdate: ( @@ -480,6 +482,7 @@ export function createRoutePanel(callbacks: PanelCallbacks) { onPipeUpdate: callbacks.onPipeFacilityUpdate, onPipeRemove: callbacks.onPipeFacilityRemove, onPipeSelect: callbacks.onPipeFacilitySelect, + onReveal: () => callbacks.onStructureReveal?.(), }); /** 지금 적용되는 종단기울기 상한(%) — 사용자가 값을 넣었으면 그 값, 비웠으면 diff --git a/B05_Profile/B05_Profile_UI_Profile_Table.ts b/B05_Profile/B05_Profile_UI_Profile_Table.ts index d0e79abc..581345f4 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Table.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Table.ts @@ -245,15 +245,18 @@ function buildSegmentRows( x: (chainage: number) => number, fontPx: number, rowHeight: number, - /** 구조물(비정규) 측점 승격으로 생긴 변화점 chainage 집합(소수 3자리 키). */ - structureChainages: ReadonlySet, + /** 구조물(비정규) 측점 승격으로 생긴 변화점 chainage 목록(m). */ + structureChainages: ReadonlyArray, /** 현재 선택된 측점 chainage — 구조물 파생 블록은 선택됐을 때만 값을 보인다. */ selectedChainageM: number | null, ): HTMLElement[] { // 구조물 배치로 갈라진 구간인지 — 양 끝 중 하나라도 구조물 변화점이면 파생 블록이다. + // 자리 비교는 **허용오차**로 한다(2026-09-04) — 소수 3자리 문자열로 맞추면 같은 + // 자리인데도 끝자리가 갈려(85.590 ↔ 85.594) 구조물 구간으로 안 잡혔다. const isStructureSegment = (segment: AlignmentSegment): boolean => - structureChainages.has(segment.from_m.toFixed(3)) || - structureChainages.has(segment.to_m.toFixed(3)); + structureChainages.some( + (at) => Math.abs(segment.from_m - at) < 0.01 || Math.abs(segment.to_m - at) < 0.01, + ); const touchesSelected = (segment: AlignmentSegment): boolean => selectedChainageM !== null && (Math.abs(segment.from_m - selectedChainageM) < 0.01 || @@ -583,15 +586,17 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { // 구조물(비정규) 측점 승격 변화점 = 사용자 변화점 중 규칙 측점과 겹치지 않는 것. const stationKeys = new Set(alignment.stations.map((row) => row.chainage_m.toFixed(3))); - const structureChainages = new Set( - alignment.pvi - .filter((node) => node.source === "user" && !stationKeys.has(node.chainage_m.toFixed(3))) - .map((node) => node.chainage_m.toFixed(3)), - ); + const structureChainages = alignment.pvi + .filter((node) => node.source === "user" && !stationKeys.has(node.chainage_m.toFixed(3))) + .map((node) => node.chainage_m); // 확정 시 종단 정본에 병합된 구조물 측점은 alignment.stations에 규칙 측점처럼 끼어 있다. // 그 열의 기본값은 표기하지 않는다(2026-08-04 사용자 지시) — 사이드바 구조물 목록의 // chainage와 일치하는 측점이 대상이고, 값은 선택 시 값 열 오버레이(하이라이트)가 보여준다. const structureChainageList = (options.irregularStations ?? []).map((entry) => entry.chainage_m); + // 병합된 구조물 측점도 **구조물 구간 경계**다 — 확정으로 종단 정본에 들어간 뒤에는 + // 위 `alignment.pvi` 걸러내기에서 빠져, 고른 측점의 값 열이 강조되지 않았다 + // (2026-09-04 사용자 보고: 테이블만 함께 안 켜짐). + structureChainages.push(...structureChainageList); // 배관 구조물은 R이 필수라 곡선 행만은 기본 표기 예외다(2026-08-04 사용자 지시). const pipeChainageList = (options.irregularStations ?? []) .filter((entry) => isPipeStation(entry)) diff --git a/B05_Profile/B05_Profile_UI_Structure_Pick_Session.ts b/B05_Profile/B05_Profile_UI_Structure_Pick_Session.ts index 1caaad38..082e66b9 100644 --- a/B05_Profile/B05_Profile_UI_Structure_Pick_Session.ts +++ b/B05_Profile/B05_Profile_UI_Structure_Pick_Session.ts @@ -4,8 +4,9 @@ * 남겨 **B06 진입 때 같은 측점 카드가 열리게** 한다(2026-09-04 사용자 확정). * * B05·B06은 라우터가 따로 띄우는 화면이라 실시간 양방향이 아니다 — 조작 상태는 캐시 - * 몫이라는 데이터 3층 규칙대로 sessionStorage에 남긴다. 값은 읽는 즉시 지운다(한 번만 - * 따라간다 — 나중에 B06을 다시 열었을 때 옛 선택이 되살아나면 안 된다). + * 몫이라는 데이터 3층 규칙대로 sessionStorage 한 칸에 남긴다. 값은 **지우지 않고 남긴다** + * (2026-09-04 사용자: 두 화면이 한 페이지처럼 움직여야 함) — B06에서 고른 것도 같은 칸에 + * 적혀, 어느 쪽으로 오가든 마지막 선택이 그대로 살아 있다. * ========================================================================== */ import type { StructurePick, StructurePickControls } from "./B05_Profile_UI_Viewer_Structure_Pick"; @@ -35,13 +36,27 @@ export function rememberStructurePick(projectId: string | null, pick: StructureP } } -/** 세션에 남은 선택을 꺼내고 지운다. 없으면 null. */ -export function consumeStructurePick(projectId: string | null): StructurePickHandoff | null { +/** 화면에서 고른 것을 세션에 적는다 — B06 쪽 창구(측점만 고르면 부재키는 비운다). */ +export function writeStructurePick( + projectId: string | null, + at: number | null, + key?: string, +): void { + if (!projectId) return; + try { + if (at === null) window.sessionStorage.removeItem(sessionKey(projectId)); + else window.sessionStorage.setItem(sessionKey(projectId), JSON.stringify({ at, key })); + } catch { + /* 세션 저장소가 막힌 환경 — 화면 선택만 살고 넘김은 포기한다. */ + } +} + +/** 세션에 남은 선택을 읽는다(지우지 않는다). 없으면 null. */ +export function readStructurePick(projectId: string | null): StructurePickHandoff | null { if (!projectId) return null; try { const raw = window.sessionStorage.getItem(sessionKey(projectId)); if (!raw) return null; - window.sessionStorage.removeItem(sessionKey(projectId)); const value = JSON.parse(raw) as Partial; if (typeof value.at !== "number" || !Number.isFinite(value.at)) return null; return { at: value.at, key: typeof value.key === "string" ? value.key : undefined }; @@ -84,3 +99,24 @@ export function wireStructurePick( structures.selectById(hit?.structure_id ?? null); }; } + +/** + * 화면에 들어올 때 세션에 남은 선택을 되살린다 — B06에서 고른 것도 그대로 이어 받는다 + * (2026-09-04 사용자: 두 화면이 한 페이지처럼). 3D 강조는 코리도가 뜬 뒤 자동으로 다시 + * 칠해지므로 여기서는 선택만 세워 둔다. + * + * **되살렸으면 true.** 관 목록이 아직 안 실렸으면 폼이 그 시설을 못 찾으므로 false를 + * 돌려준다 — 부르는 쪽은 목록이 실릴 때마다 다시 부르면 된다. + */ +export function restoreStructurePick( + controls: StructurePickControls, + projectId: string | null, + structures: StructuresSection, + selectStation: (chainageM: number | null) => void, +): boolean { + const handoff = readStructurePick(projectId); + if (!handoff) return true; // 되살릴 것이 없으면 끝난 것으로 본다. + controls.select({ chainageM: handoff.at, kind: "", key: handoff.key }); + selectStation(handoff.at); + return structures.hasSelection(); +} diff --git a/B05_Profile/B05_Profile_UI_Structures_List.ts b/B05_Profile/B05_Profile_UI_Structures_List.ts index 9966affb..ea48714c 100644 --- a/B05_Profile/B05_Profile_UI_Structures_List.ts +++ b/B05_Profile/B05_Profile_UI_Structures_List.ts @@ -103,15 +103,19 @@ export function renderStructureList(params: StructureListParams): void { item.classList.toggle("is-selected", structure.structure_id === editingId); station.textContent = stationOfStructure(structure, intervalM); item.addEventListener("click", () => params.onSelectStructure(structure)); - if (structure.structure_id === editingId) item.scrollIntoView({ block: "nearest" }); + if (structure.structure_id === editingId) { + requestAnimationFrame(() => item.scrollIntoView({ block: "nearest" })); + } } else { const { pipe } = row; - item.classList.toggle( - "is-selected", - selectedPipeChainage !== null && Math.abs(selectedPipeChainage - pipe.chainage_m) < 0.05, - ); + const picked = + selectedPipeChainage !== null && Math.abs(selectedPipeChainage - pipe.chainage_m) < 0.05; + item.classList.toggle("is-selected", picked); station.textContent = formatStation(pipe.chainage_m, intervalM); item.addEventListener("click", () => params.onSelectPipe(pipe)); + // 하이라이트만으로는 목록 밖에 있으면 안 보인다 — 그 자리로 끌어온다(2026-09-04 + // 사용자). 패널이 막 펼쳐진 참이라 자리가 잡힌 다음 프레임에 민다. + if (picked) requestAnimationFrame(() => item.scrollIntoView({ block: "nearest" })); } item.append(station, name); list.append(item); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 3516d813..81fabbd1 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -584,6 +584,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu renderList(); }, selectPipeByChainage(chainageM) { + if (chainageM !== null) callbacks.onReveal?.(); if (chainageM === null) { // 외부 해제(빈 공간 클릭 등) — 임시 배치는 취소, 선택 중이던 시설도 구조물군· // 종류까지 완전 리셋(2026-08-18 사용자 지시). 아무것도 안 골랐으면 무시. @@ -597,6 +598,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu loadPipeForm(pipe); }, selectById(structureId) { + if (structureId !== null) callbacks.onReveal?.(); if (structureId === null) { // 그래프·3D 빈 공간 클릭 등 외부 해제 — 선택 중이었으면 구조물군·종류까지 // 완전 리셋(2026-08-18 사용자 지시). 선택 없는 신규 작성 중에는 두지 않는다. diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts index 7ee04506..a04f3f5f 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts @@ -91,4 +91,10 @@ export interface StructuresCallbacks { /** 계곡 통과 시설을 목록에서 골랐을 때(시설 폼·유역 동기화는 배수유역 패널 몫). * null = 재클릭 해제 — 전 화면(그래프·3D·배수유역도) 선택도 함께 푼다(2026-08-18). */ onPipeSelect: (chainageM: number | null) => void; + /** + * 바깥(3D·종단 그래프·배수유역도·횡단도)에서 고른 것이 폼에 실릴 때 — 좌측 패널이 + * 접혀 있으면 펼친다(2026-09-04 사용자: 선택하면 좌측 패널도 함께 활성화돼야 함). + * 목록에서 직접 고른 경우엔 이미 펼쳐져 있어 아무 일도 안 한다. + */ + onReveal?: () => void; } diff --git a/B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts b/B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts index 5e466ec5..3e2688e4 100644 --- a/B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts +++ b/B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts @@ -24,6 +24,8 @@ export interface StructurePickControls { onPick?: (pick: StructurePick | null) => void; /** 좌측 패널·종단 그래프에서 고른 것을 3D 강조에 반영한다(부재키 없이 측점만). */ selectAtChainage: (chainageM: number | null) => void; + /** 부재키까지 지정해 강조를 세운다 — 세션에 남은 선택을 되살릴 때 쓴다. */ + select: (pick: StructurePick | null) => void; /** 코리도를 다시 만든 뒤 강조를 되살린다 — 메쉬가 통째로 새 것이라 다시 칠해야 한다. */ reapply: () => void; dispose: () => void; @@ -152,6 +154,10 @@ export function bindStructurePick(options: { selected = chainageM === null ? null : { chainageM, kind: "" }; applyHighlight(); }, + select(pick) { + selected = pick; + applyHighlight(); + }, reapply: applyHighlight, dispose() { window.removeEventListener("pointerdown", handleWindowDown, true); diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index b68b0765..58a27434 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -30,7 +30,10 @@ import { } from "./B06_Section_UI_Page_Persist"; import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit"; import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit"; -import { consumeStructurePick } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session"; +import { + readStructurePick, + writeStructurePick, +} from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session"; import { applyStructurePick } from "./B06_Section_UI_Page_Structure_Pick"; import { type CrossDesignChange, createSectionView } from "./B06_Section_UI_Section_View"; import { hasStaleDesigns } from "./B06_Section_UI_Section_Common"; @@ -137,6 +140,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { // 하단 고정 dock 도 B05와 같은 구조: [구조물 목록][구분선][액션 버튼 행]. const structuresPanel = createB06StructuresPanel({ projectId, + // 횡단도·3D 넘김값에서 고른 것이 폼에 실릴 때 좌측 패널을 펼친다(2026-09-04 사용자). + reveal: () => layout.setOptionsOpen(true), detail: () => sectionDetail, // 폼 기본 높이 = 지금 도면에 그려진 순수 높이(조정창이 보여주던 값과 같은 계산). wallHeight: (chainageM, role) => { @@ -439,6 +444,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { stationControls, () => sectionDetail, structuresPanel, + // 횡단도에서 고른 부재도 같은 세션 칸에 남긴다(2026-09-04 — 두 화면이 한 페이지처럼). + (chainageM, key) => writeStructurePick(projectId, chainageM, key), ); // 횡단도(카드) 자체를 골라도 그 측점 구조물 정보를 폼에 올린다(2026-08-29 사용자). // 연동으로 옆에서 넘어온 카드는 **소유 측점** 시설을 보여 준다. @@ -448,10 +455,13 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { : null; if (!target) { structuresPanel.showAtChainage(null); + writeStructurePick(projectId, null); // 카드 해제 — 세션 선택도 비운다. return; } const owner = stationControls.structureSpan.ownerOf(target); structuresPanel.showAtChainage(owner?.chainage_m ?? target.chainage_m); + // 카드만 고른 것도 세션에 남긴다 — B05로 돌아가면 그 시설이 그대로 열린다. + if (!isWallSelecting()) writeStructurePick(projectId, owner?.chainage_m ?? target.chainage_m); structureSelection.syncInletStructure(); // 카드만 고른 경우엔 구조물(벽·구체) 선택을 비운다 — 조정창이 저절로 뜨면 안 된다 // (2026-08-29 사용자). 벽을 찍어 카드가 딸려 선택된 경우는 건드리지 않는다. @@ -515,9 +525,14 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { confirmButton.disabled = sectionDetail === null; } - /** B05 3D에서 고른 구조물을 이어받는다(2026-09-04) — 값은 읽는 즉시 지워져 한 번만 쓴다. */ + /** 세션에 남은 선택을 이어받는다(2026-09-04 — 두 화면이 한 페이지처럼). + * 진입당 한 번만 — 재렌더마다 다시 돌면 사용자가 옮긴 스크롤이 튄다. 관 목록이 + * 늦게 오는 몫은 패널 load()가 스스로 다시 세운다. */ + let pickRestored = false; function restoreStructurePick(): void { - applyStructurePick(consumeStructurePick(projectId), sectionDetail, { + if (pickRestored || !sectionDetail) return; + pickRestored = true; + applyStructurePick(readStructurePick(projectId), sectionDetail, { focusStation: (stationId) => sectionView.focusStation(stationId), refreshCard: (chainageM) => sectionView.refreshCard(chainageM), revetSelect: (chainageM, key) => stationControls.revetOffset.select(chainageM, key), @@ -626,7 +641,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { rockStore.load(); stationControls.load(); // 구조물 배치 데이터(타입·정본·관 지점) — 카드 로드와 병행, 화면을 잠그지 않는다. - void structuresPanel.load(); + void structuresPanel.load().then(restoreStructurePick); try { const existing = await getSections(projectId, context.route_id); if (!existing.longitudinal) { diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index fabc9eb7..e234da6b 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -23,6 +23,10 @@ import { type StructureInstance, } from "../B05_Profile/B05_Profile_Api_Structures"; import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; +import { + readStructurePick, + writeStructurePick, +} from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session"; import { showToast } from "@ui/ui_template_elements"; import { adjustDockRoot, @@ -56,6 +60,8 @@ export interface B06StructuresPanelDeps { stationInterval: () => number; /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */ focusChainage: (chainageM: number) => void; + /** 바깥(횡단도·3D 넘김값)에서 고른 것이 폼에 실릴 때 — 접힌 좌측 패널을 펼친다. */ + reveal?: () => void; /** 폼 [수정]으로 바뀐 관 옵션을 캐시에 예약한다 — [저장]·[확정]이 정본에 쓴다 * (2026-08-29: 조정창 구간값과 같은 경로). 없으면 안내만 한다. */ queuePipeOptions?: (chainageM: number, patch: Record) => void; @@ -227,6 +233,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc if (structure) deps.focusChainage(structureAnchorM(structure)); }, getInterval: deps.stationInterval, + onReveal: () => deps.reveal?.(), onPipeAdd: () => showToast(PIPE_ADD_GUIDE, "error"), // 값 수정은 B06에서도 받는다 — 조정창 구간값과 같은 저장 경로(캐시 예약 → // [저장]·[확정])로 보낸다. 기준점 이동만 배수유역 재분할이 걸려 B05 몫이다. @@ -269,6 +276,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc }, onPipeRemove: () => showToast(PIPE_ADD_GUIDE, "error"), onPipeSelect: (chainageM) => { + // 목록에서 고른 것도 세션에 남긴다 — B05로 돌아가면 그 시설이 그대로 열린다. + writeStructurePick(deps.projectId, chainageM); if (chainageM !== null) deps.focusChainage(chainageM); }, }); @@ -332,6 +341,11 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc )?.design_flow_m3s ?? null, })); section.setPipeFacilities(pipeFacilities); + // 목록이 늦게 도착하면 그 사이에 고른 시설은 강조될 자리가 없었다 — 다시 세운다 + // (2026-09-04 사용자 보고: B06 좌측 목록만 하이라이트가 안 붙음). 폼에 아직 아무것도 + // 없으면 세션에 남은 선택(카드형 — 부재키 없는 측점)도 같은 규칙으로 세운다. + const picked = currentChainageM ?? readStructurePick(deps.projectId)?.at ?? null; + if (picked !== null) section.selectPipeByChainage(picked); } catch (error) { showToast( error instanceof Error ? error.message : "구조물 정보를 불러오지 못했습니다.", @@ -435,6 +449,9 @@ export function wireStructureSelection( stationControls: StationControls, detail: () => SectionDetailResponse | null, panel: B06StructuresPanel, + /** 고른 부재를 세션에 남긴다(2026-09-04). 해제(null)는 부르지 않는다 — 조정창 닫기가 + * 카드(측점) 선택까지 지우면 안 된다. 측점 해제는 카드 선택 리스너가 따로 적는다. */ + remember?: (chainageM: number, key: string) => void, ): { syncInletStructure: () => void } { const ownerChainageOf = (chainageM: number): number => { const sectionAt = detail()?.cross_sections.find( @@ -463,6 +480,7 @@ export function wireStructureSelection( stationControls.revetOffset.select = (chainageM, key) => { markWallSelecting(); origRevet(chainageM, key); + if (key) remember?.(ownerChainageOf(chainageM), key); panel.showPipeAt(key ? ownerChainageOf(chainageM) : null); if (key) syncInletStructure(); // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 목적지 칸이 그제야 선다. @@ -473,6 +491,7 @@ export function wireStructureSelection( stationControls.ford.select = (chainageM, role) => { markWallSelecting(); origFord(chainageM, role); + if (role) remember?.(chainageM, role); panel.showPipeAt(role ? chainageM : null); // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다 // (배관 기슭막이와 같은 순서, 2026-08-30 사용자 지시 1). @@ -482,6 +501,7 @@ export function wireStructureSelection( stationControls.box.select = (chainageM, role) => { markWallSelecting(); origBox(chainageM, role); + if (role) remember?.(chainageM, role); panel.showPipeAt(role ? chainageM : null); // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다. refreshAdjustSlots();