feat(B05/B06): 구조물 선택 화면 전체 활성화 + 세션 상시 공유
- 바깥 선택이 폼에 실릴 때 접힌 좌측 패널 자동 펼침(onReveal 창구) - 계곡 통과 시설 목록 행도 보이는 자리로 스크롤(rAF) - 종단 테이블 구조물 구간 판정을 허용오차 비교로 교체 + 병합 측점 포함(강조 0건→6건) - 구조물 선택 세션을 소비형에서 유지형으로 — B06 카드·목록·부재 선택도 같은 칸에 기록, 양쪽 진입 시 복원(목록 지연 로드 대비 이중 복원) - 조정창 닫기가 세션 선택까지 지우던 결함 수정(부재 해제와 측점 해제 분리) - B05 페이지 700줄 유지 위해 상단측 세션 보관을 Page_Helpers로 이동(동작 불변) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<void> {
|
||||
options: pipe.options,
|
||||
})),
|
||||
);
|
||||
restorePick(); // 관 목록이 실린 뒤라야 폼이 그 시설을 찾는다.
|
||||
},
|
||||
// 그래프 측점선은 이제 배관(계곡 통과 시설) 투영뿐이다 — 수동 구조물은
|
||||
// 서클마크·구조물 배치 목록이 담당한다(2026-08-17 컨테이너 병합).
|
||||
@@ -175,21 +182,8 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
/* ── 측점 상단측(=측구 방향) 사용자 변경분 ─────────────────────────────
|
||||
* solve가 자동 판정한 uphill_side를 3D 램프 클릭으로 바꾼 값. 세션에 보관했다가
|
||||
* 경로 확정 시 uphill_overrides로 백엔드/DB(종단 정본)에 병합한다. */
|
||||
const uphillSessionKey = `b05:uphill:${activeProjectId}`;
|
||||
const uphillOverrides = new Map<string, "left" | "right">();
|
||||
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<string, "left" | "right">).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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
},
|
||||
});
|
||||
|
||||
// 3D 코리도에서 부재를 고르면 좌측 「구조물 배치」 폼이 그 시설을 연다(2026-09-04).
|
||||
wireStructurePick(viewer.structurePick, activeProjectId, panel.structures, selectStationOfPipe);
|
||||
|
||||
/** 입력·마커 변경 시 측점 라인만 다시 그린다 — 확정 게이트는 폐지(B06 통합 확정).
|
||||
@@ -692,5 +689,6 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
} finally {
|
||||
progress.remove();
|
||||
restoring = false;
|
||||
restorePick(); // 관·구조물 목록 중 늦게 오는 쪽이 있어 여기서 한 번 더.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,3 +242,40 @@ export const FACILITY_NAMES: Record<PipeFacility, string> = {
|
||||
ford_bridge: "세월교",
|
||||
revetment: "기슭막이",
|
||||
};
|
||||
|
||||
/* ── 측점 상단측(=측구 방향) 사용자 변경분 세션 보관 ─────────────────────
|
||||
* 3D 램프 클릭으로 바꾼 값. 경로 확정 때 uphill_overrides로 백엔드에 병합한다.
|
||||
* 화면 본체가 700줄에 닿아 읽기·쓰기만 여기로 뺐다(2026-09-04, 동작 불변). */
|
||||
const uphillSessionKey = (projectId: string): string => `b05:uphill:${projectId}`;
|
||||
|
||||
/** 세션에 남은 상단측 변경분을 읽는다. 손상된 값은 무시하고 빈 것으로 시작한다. */
|
||||
export function loadUphillOverrides(projectId: string): Map<string, "left" | "right"> {
|
||||
const overrides = new Map<string, "left" | "right">();
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(uphillSessionKey(projectId));
|
||||
if (!raw) return overrides;
|
||||
Object.entries(JSON.parse(raw) as Record<string, "left" | "right">).forEach(
|
||||
([chainage, side]) => {
|
||||
if (side === "left" || side === "right") overrides.set(chainage, side);
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
/* 손상된 세션 값은 무시 — 자동 판정값으로 재시작. */
|
||||
}
|
||||
return overrides;
|
||||
}
|
||||
|
||||
/** 상단측 변경분을 세션에 적는다. 저장 실패는 무시한다(값은 메모리에 남는다). */
|
||||
export function saveUphillOverrides(
|
||||
projectId: string,
|
||||
overrides: ReadonlyMap<string, "left" | "right">,
|
||||
): void {
|
||||
try {
|
||||
window.sessionStorage.setItem(
|
||||
uphillSessionKey(projectId),
|
||||
JSON.stringify(Object.fromEntries(overrides)),
|
||||
);
|
||||
} catch {
|
||||
/* 세션 저장 실패는 무시 — 값은 메모리에 유지된다. */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?.(),
|
||||
});
|
||||
|
||||
/** 지금 적용되는 종단기울기 상한(%) — 사용자가 값을 넣었으면 그 값, 비웠으면
|
||||
|
||||
@@ -245,15 +245,18 @@ function buildSegmentRows(
|
||||
x: (chainage: number) => number,
|
||||
fontPx: number,
|
||||
rowHeight: number,
|
||||
/** 구조물(비정규) 측점 승격으로 생긴 변화점 chainage 집합(소수 3자리 키). */
|
||||
structureChainages: ReadonlySet<string>,
|
||||
/** 구조물(비정규) 측점 승격으로 생긴 변화점 chainage 목록(m). */
|
||||
structureChainages: ReadonlyArray<number>,
|
||||
/** 현재 선택된 측점 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))
|
||||
|
||||
@@ -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<StructurePickHandoff>;
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 사용자 지시). 선택 없는 신규 작성 중에는 두지 않는다.
|
||||
|
||||
@@ -91,4 +91,10 @@ export interface StructuresCallbacks {
|
||||
/** 계곡 통과 시설을 목록에서 골랐을 때(시설 폼·유역 동기화는 배수유역 패널 몫).
|
||||
* null = 재클릭 해제 — 전 화면(그래프·3D·배수유역도) 선택도 함께 푼다(2026-08-18). */
|
||||
onPipeSelect: (chainageM: number | null) => void;
|
||||
/**
|
||||
* 바깥(3D·종단 그래프·배수유역도·횡단도)에서 고른 것이 폼에 실릴 때 — 좌측 패널이
|
||||
* 접혀 있으면 펼친다(2026-09-04 사용자: 선택하면 좌측 패널도 함께 활성화돼야 함).
|
||||
* 목록에서 직접 고른 경우엔 이미 펼쳐져 있어 아무 일도 안 한다.
|
||||
*/
|
||||
onReveal?: () => void;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void> {
|
||||
// 하단 고정 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<void> {
|
||||
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<void> {
|
||||
: 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<void> {
|
||||
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<void> {
|
||||
rockStore.load();
|
||||
stationControls.load();
|
||||
// 구조물 배치 데이터(타입·정본·관 지점) — 카드 로드와 병행, 화면을 잠그지 않는다.
|
||||
void structuresPanel.load();
|
||||
void structuresPanel.load().then(restoreStructurePick);
|
||||
try {
|
||||
const existing = await getSections(projectId, context.route_id);
|
||||
if (!existing.longitudinal) {
|
||||
|
||||
@@ -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<string, number | string>) => 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();
|
||||
|
||||
Reference in New Issue
Block a user