feat(B05,B06): 구조물 배치 컨테이너를 B06에 장착하고 [횡단 조정] 사본을 세운다
B05/B06 인터페이스 일원화 1차 (2026-08-29 사용자 확정 계획): - A00_Common/b_structures_section.ts: B05 구조물 배치 폼·목록 공용 진입점 (재수출 + 스타일 동봉) - B06 좌측 패널에 [구조물 배치] 섹션 + 하단 dock [목록][구분선][저장버튼] (B05 동일 템플릿). 구조물 조작은 세션 pending으로 쌓여 기존 flushPendingStructures([임시저장]·[확정])로 정본 반영 - 선택 연동: 목록·폼 → 최근접 카드 focusStation, 횡단도 벽·세월교·BOX → 좌측 폼에 소유 측점 시설 로드(wireStructureSelection) - [횡단 조정] 하위 컨테이너(Adjust_Dock): 오버레이 조정창을 같은 deps로 사본 인스턴스 생성해 세움 — 로직 이동 0, 카드 재렌더로 자동 동기. 9키·십자 행은 사본에서 숨김(사용자 지시 7), 방향키 이중 입력 방지 - A군 시설 추가·이동·삭제는 B06에서 안내 토스트(배수유역 체인은 B05 몫) 검증: 공용 브라우저 — 구간값 길이 사본 + 10.0→11.0m 양측 동기, 오버레이 - 원복, ✕ 닫기 시 사본 해제. B05 회귀 없음. typecheck 통과.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
/* =============================================================================
|
||||
* b_structures_section.ts
|
||||
* 「구조물 배치」 컨테이너 공용 진입점 — B05·B06 두 페이지가 같은 주소로 쓴다
|
||||
* (2026-08-29 사용자: B05/B06 인터페이스 일원화 — 배치 폼·하단 목록 템플릿 동일).
|
||||
*
|
||||
* 구현은 B05 모듈(`B05_Profile_UI_Structures_Panel` 외)에 있다. 이 파일은 재수출과
|
||||
* 스타일 동봉만 한다 — B06이 이 파일 하나만 import해도 B05와 같은 모양이 실린다.
|
||||
* ========================================================================== */
|
||||
import "../B05_Profile/B05_Profile_UI_Style.css";
|
||||
import "../B05_Profile/B05_Profile_UI_Style_Structures.css";
|
||||
|
||||
export {
|
||||
createStructuresSection,
|
||||
GROUP_LABELS,
|
||||
type PipeFacilityItem,
|
||||
type StructuresSection,
|
||||
} from "../B05_Profile/B05_Profile_UI_Structures_Panel";
|
||||
@@ -0,0 +1,60 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Adjust_Dock.ts
|
||||
* 좌측 [구조물 배치] 섹션 안 **[횡단 조정] 하위 컨테이너**(2026-08-29 사용자:
|
||||
* B05/B06 일원화 — 오버레이 조정창 항목을 좌측에도 보이되, 기존 폼과 구분되게
|
||||
* 접이식 하위 컨테이너에 담는다. 9키·십자 위치제어는 오버레이 전용).
|
||||
*
|
||||
* 카드가 조정창을 만들 때 같은 deps로 **사본 창**을 하나 더 만들어 이 슬롯에
|
||||
* 세운다(adopt). 값·동작이 같은 함수라 오버레이·좌측 어느 쪽을 만져도 카드
|
||||
* 재렌더에서 두 창이 함께 새 값으로 그려진다 — 별도 동기화 코드가 없다.
|
||||
* 카드가 여럿이어도 슬롯은 하나 — 마지막으로 고른 구조물의 사본만 선다.
|
||||
* ========================================================================== */
|
||||
|
||||
let dockRoot: HTMLDetailsElement | null = null;
|
||||
let slot: HTMLElement | null = null;
|
||||
let placeholder: HTMLElement | null = null;
|
||||
|
||||
function ensureDock(): void {
|
||||
if (dockRoot) return;
|
||||
dockRoot = document.createElement("details");
|
||||
dockRoot.className = "b06-adjust-dock";
|
||||
dockRoot.open = true;
|
||||
const summary = document.createElement("summary");
|
||||
summary.textContent = "횡단 조정";
|
||||
placeholder = document.createElement("p");
|
||||
placeholder.className = "b06-adjust-dock__empty";
|
||||
placeholder.textContent = "횡단도에서 구조물(벽·구체)을 고르면 조정 항목이 여기에 뜹니다.";
|
||||
slot = document.createElement("div");
|
||||
slot.className = "b06-adjust-dock__slot";
|
||||
dockRoot.append(summary, placeholder, slot);
|
||||
}
|
||||
|
||||
/** 하위 컨테이너 루트 — 페이지가 [구조물 배치] 섹션 본문에 붙인다. */
|
||||
export function adjustDockRoot(): HTMLElement {
|
||||
ensureDock();
|
||||
return dockRoot!;
|
||||
}
|
||||
|
||||
/** 페이지 진입 시 초기 상태(빈 슬롯 + 안내문)로 되돌린다. */
|
||||
export function resetAdjustDock(): void {
|
||||
ensureDock();
|
||||
slot!.replaceChildren();
|
||||
placeholder!.hidden = false;
|
||||
}
|
||||
|
||||
/** 조정창 사본을 슬롯에 세운다 — 이미 다른 사본이 있으면 갈아 끼운다. */
|
||||
export function adoptAdjustPanel(panelRoot: HTMLElement): void {
|
||||
ensureDock();
|
||||
panelRoot.classList.add("b06-structure-panel--dock");
|
||||
if (slot!.firstChild !== panelRoot || slot!.childNodes.length !== 1) {
|
||||
slot!.replaceChildren(panelRoot);
|
||||
}
|
||||
placeholder!.hidden = true;
|
||||
}
|
||||
|
||||
/** 이 사본이 슬롯에 서 있으면 내린다 — 다른 사본이 이미 대신 서 있으면 손대지 않는다. */
|
||||
export function releaseAdjustPanel(panelRoot: HTMLElement): void {
|
||||
if (!slot || !slot.contains(panelRoot)) return;
|
||||
slot.replaceChildren();
|
||||
placeholder!.hidden = false;
|
||||
}
|
||||
@@ -42,10 +42,12 @@ export interface BoxPanelHandle {
|
||||
/** 한 걸음(m) — 사용자 지정 0.1m. */
|
||||
const STEP_M = 0.1;
|
||||
|
||||
/** BOX암거 조정 오버레이 창을 만든다. 처음에는 숨어 있다. */
|
||||
export function buildBoxPanel(deps: BoxPanelDeps): BoxPanelHandle {
|
||||
/** BOX암거 조정 오버레이 창을 만든다. 처음에는 숨어 있다.
|
||||
* `opts.dock` — 좌측 [횡단 조정] 사본(2026-08-29): 흐름 배치 + 방향키 미장착. */
|
||||
export function buildBoxPanel(deps: BoxPanelDeps, opts?: { dock?: boolean }): BoxPanelHandle {
|
||||
const shell = buildPanelShell(() => deps.close());
|
||||
const { root, title } = shell;
|
||||
if (opts?.dock) root.classList.add("b06-structure-panel--dock");
|
||||
|
||||
const value = document.createElement("div");
|
||||
value.className = "b06-structure-panel__value";
|
||||
@@ -93,7 +95,9 @@ export function buildBoxPanel(deps: BoxPanelDeps): BoxPanelHandle {
|
||||
|
||||
root.append(value, moveRow.row);
|
||||
|
||||
const arrows = bindArrowKeys(root, () => [moveRow.controls]);
|
||||
const arrows = opts?.dock
|
||||
? { attach: (): void => undefined, detach: (): void => undefined }
|
||||
: bindArrowKeys(root, () => [moveRow.controls]);
|
||||
|
||||
function render(): void {
|
||||
if (!current) return;
|
||||
|
||||
@@ -65,10 +65,12 @@ const ANGLE_STEP_DEG = 5;
|
||||
/** 관경 선택지(mm) — B05 레지스트리 `ford_bridge.pipe_diameter_mm` choices와 같다. */
|
||||
const DIAMETER_CHOICES_MM = [800, 1000, 1200, 1500];
|
||||
|
||||
/** 세월교 측벽 조정 오버레이 창을 만든다. 처음에는 숨어 있다. */
|
||||
export function buildFordPanel(deps: FordPanelDeps): FordPanelHandle {
|
||||
/** 세월교 측벽 조정 오버레이 창을 만든다. 처음에는 숨어 있다.
|
||||
* `opts.dock` — 좌측 [횡단 조정] 사본(2026-08-29): 흐름 배치 + 방향키 미장착. */
|
||||
export function buildFordPanel(deps: FordPanelDeps, opts?: { dock?: boolean }): FordPanelHandle {
|
||||
const shell = buildPanelShell(() => deps.close());
|
||||
const { root, title } = shell;
|
||||
if (opts?.dock) root.classList.add("b06-structure-panel--dock");
|
||||
|
||||
const value = document.createElement("div");
|
||||
value.className = "b06-structure-panel__value";
|
||||
@@ -233,7 +235,9 @@ export function buildFordPanel(deps: FordPanelDeps): FordPanelHandle {
|
||||
moveRow.row,
|
||||
);
|
||||
|
||||
const arrows = bindArrowKeys(root, () => [moveRow.controls]);
|
||||
const arrows = opts?.dock
|
||||
? { attach: (): void => undefined, detach: (): void => undefined }
|
||||
: bindArrowKeys(root, () => [moveRow.controls]);
|
||||
|
||||
function render(): void {
|
||||
if (!current) return;
|
||||
|
||||
@@ -111,11 +111,17 @@ const LENGTH_STEP_M = SPAN_STEP_M * 2;
|
||||
*/
|
||||
const panelScrollTops = new Map<string, number>();
|
||||
|
||||
/** 구조물 조정 오버레이 창을 만든다. 처음에는 숨어 있다. */
|
||||
export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHandle {
|
||||
/** 구조물 조정 오버레이 창을 만든다. 처음에는 숨어 있다.
|
||||
* `opts.dock` — 좌측 [횡단 조정] 사본(2026-08-29 일원화): 흐름 배치 스타일을 입고,
|
||||
* 방향키 문서 리스너를 달지 않는다(오버레이 창과 이중으로 눌리는 것 방지). */
|
||||
export function buildStructurePanel(
|
||||
deps: StructurePanelDeps,
|
||||
opts?: { dock?: boolean },
|
||||
): StructurePanelHandle {
|
||||
const root = document.createElement("div");
|
||||
root.className = "b06-structure-panel";
|
||||
root.classList.add("is-hidden");
|
||||
if (opts?.dock) root.classList.add("b06-structure-panel--dock");
|
||||
root.addEventListener("scroll", () => {
|
||||
if (!root.classList.contains("is-hidden")) panelScrollTops.set(deps.scrollKey, root.scrollTop);
|
||||
});
|
||||
@@ -482,7 +488,7 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
root.classList.toggle("is-hidden", key === null);
|
||||
document.removeEventListener("keydown", onArrowKey);
|
||||
if (!key) return;
|
||||
document.addEventListener("keydown", onArrowKey);
|
||||
if (!opts?.dock) document.addEventListener("keydown", onArrowKey);
|
||||
// 다단 벽 — 유출측(extra0…)과 유입·집수정 계류측(bextra0…)이 같은 이름을 쓴다.
|
||||
const isExtra = key.startsWith("extra") || key.startsWith("bextra");
|
||||
const extraIndex = key.startsWith("bextra") ? Number(key.slice(6)) : Number(key.slice(5));
|
||||
|
||||
@@ -42,6 +42,7 @@ import type {
|
||||
StructureSpanControl,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { buildStructurePanel } from "./B06_Section_UI_Cross_Structure_Panel";
|
||||
import { adoptAdjustPanel, releaseAdjustPanel } from "./B06_Section_UI_Adjust_Dock";
|
||||
import { culvertCardState, structurePanelDeps } from "./B06_Section_UI_Cross_View_Structure";
|
||||
import { attachZoomPan, buildZoomControls } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import type { CrossWidthActions, ZoomPanState } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
@@ -685,36 +686,47 @@ export function createCrossSectionCard(
|
||||
const source = isLinkedCulvert && culvertLink ? culvertLink.source : section;
|
||||
return revetOffset?.adjustFor(source, key) ?? { x: 0, d: null, h: null, m: null };
|
||||
};
|
||||
const panel = buildStructurePanel(
|
||||
structurePanelDeps({
|
||||
section,
|
||||
scrollKey: section.station_id,
|
||||
adjustChainage: () => adjustTarget().chainage_m,
|
||||
isLinked: isLinkedCulvert,
|
||||
revetOffset,
|
||||
inletStructure,
|
||||
extraWalls,
|
||||
structureSpan,
|
||||
revetLink,
|
||||
adjustOf,
|
||||
outwardOf,
|
||||
heightOfWall,
|
||||
formOfWall,
|
||||
appliedD: () => culvertAppliedD,
|
||||
pipeLengthM: () => culvertPipeLengthM,
|
||||
inletIsBasin: () => culvertInletIsBasin,
|
||||
hiddenPipe: () => culvertHiddenPipe,
|
||||
inletOptions: () => culvertInletOptions,
|
||||
extraState: () => culvertExtraState,
|
||||
basinExtraState: () => culvertBasinExtraState,
|
||||
activeRevet: () => activeRevet,
|
||||
setActiveRevet: (key) => {
|
||||
activeRevet = key;
|
||||
},
|
||||
toggleRevet,
|
||||
}),
|
||||
const panelContext = {
|
||||
section,
|
||||
scrollKey: section.station_id,
|
||||
adjustChainage: () => adjustTarget().chainage_m,
|
||||
isLinked: isLinkedCulvert,
|
||||
revetOffset,
|
||||
inletStructure,
|
||||
extraWalls,
|
||||
structureSpan,
|
||||
revetLink,
|
||||
adjustOf,
|
||||
outwardOf,
|
||||
heightOfWall,
|
||||
formOfWall,
|
||||
appliedD: () => culvertAppliedD,
|
||||
pipeLengthM: () => culvertPipeLengthM,
|
||||
inletIsBasin: () => culvertInletIsBasin,
|
||||
hiddenPipe: () => culvertHiddenPipe,
|
||||
inletOptions: () => culvertInletOptions,
|
||||
extraState: () => culvertExtraState,
|
||||
basinExtraState: () => culvertBasinExtraState,
|
||||
activeRevet: () => activeRevet,
|
||||
setActiveRevet: (key: RevetKey | null) => {
|
||||
activeRevet = key;
|
||||
},
|
||||
toggleRevet,
|
||||
};
|
||||
const panel = buildStructurePanel(structurePanelDeps(panelContext));
|
||||
// 좌측 [횡단 조정] 사본 — 같은 deps라 어느 쪽 조작이든 카드 재렌더에서 두 창이
|
||||
// 함께 새 값으로 그려진다(2026-08-29 일원화). 스크롤 기억 키만 분리한다.
|
||||
const dockPanel = buildStructurePanel(
|
||||
structurePanelDeps({ ...panelContext, scrollKey: `dock:${section.station_id}` }),
|
||||
{ dock: true },
|
||||
);
|
||||
showRevetControl = (visible) => panel.show(visible ? activeRevet : null);
|
||||
showRevetControl = (visible) => {
|
||||
const key = visible ? activeRevet : null;
|
||||
panel.show(key);
|
||||
dockPanel.show(key);
|
||||
if (key) adoptAdjustPanel(dockPanel.root);
|
||||
else releaseAdjustPanel(dockPanel.root);
|
||||
};
|
||||
showRevetControl(activeRevet !== null);
|
||||
// 세월교 조정창 — 배수관 창과 조작 축이 달라 따로 만든다(2026-08-25 사용자).
|
||||
// 우측 상단 줌 버튼이 원배율에서 표시 반폭까지 다룬다(하단 ◀/▶/↺ 폐지, 2026-08-23).
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
import { buildBoxPanel } from "./B06_Section_UI_Cross_Box_Panel";
|
||||
import type { BoxControl, BoxPanelHandle } from "./B06_Section_UI_Cross_Box_Panel";
|
||||
import { boxPanelDeps, structureToggle } from "./B06_Section_UI_Cross_View_Box";
|
||||
import { adoptAdjustPanel, releaseAdjustPanel } from "./B06_Section_UI_Adjust_Dock";
|
||||
|
||||
export interface BodyWiringDeps {
|
||||
section: CrossSection;
|
||||
@@ -60,39 +61,53 @@ export function createBodyWiring(deps: BodyWiringDeps): BodyWiring {
|
||||
let fordHeights = new Map<FordWallRole, number>();
|
||||
let boxLayout: BoxLayout | null = null;
|
||||
|
||||
const fordPanel =
|
||||
const fordDeps =
|
||||
section.ford && ford
|
||||
? buildFordPanel(
|
||||
fordPanelDeps({
|
||||
section,
|
||||
ford,
|
||||
heightFor: (role) => fordHeights.get(role) ?? 0,
|
||||
slabLengthM: () => fordSlabLengthM,
|
||||
close: () => {
|
||||
if (fordRole) toggleFord(fordRole);
|
||||
},
|
||||
}),
|
||||
)
|
||||
? fordPanelDeps({
|
||||
section,
|
||||
ford,
|
||||
heightFor: (role) => fordHeights.get(role) ?? 0,
|
||||
slabLengthM: () => fordSlabLengthM,
|
||||
close: () => {
|
||||
if (fordRole) toggleFord(fordRole);
|
||||
},
|
||||
})
|
||||
: null;
|
||||
const boxPanel =
|
||||
const boxDeps =
|
||||
section.box && box
|
||||
? buildBoxPanel(
|
||||
boxPanelDeps({
|
||||
chainageM: chainage,
|
||||
box,
|
||||
layout: () => boxLayout,
|
||||
close: () => {
|
||||
if (boxRole) toggleBox(boxRole);
|
||||
},
|
||||
}),
|
||||
)
|
||||
? boxPanelDeps({
|
||||
chainageM: chainage,
|
||||
box,
|
||||
layout: () => boxLayout,
|
||||
close: () => {
|
||||
if (boxRole) toggleBox(boxRole);
|
||||
},
|
||||
})
|
||||
: null;
|
||||
const fordPanel = fordDeps ? buildFordPanel(fordDeps) : null;
|
||||
const boxPanel = boxDeps ? buildBoxPanel(boxDeps) : null;
|
||||
// 좌측 [횡단 조정] 사본 — 배수관 조정창과 같은 규칙(2026-08-29 일원화): 같은 deps로
|
||||
// 하나 더 만들어 고른 동안만 좌측 슬롯에 세운다.
|
||||
const fordDock = fordDeps ? buildFordPanel(fordDeps, { dock: true }) : null;
|
||||
const boxDock = boxDeps ? buildBoxPanel(boxDeps, { dock: true }) : null;
|
||||
const syncDock = (
|
||||
dock: FordPanelHandle | BoxPanelHandle | null,
|
||||
role: FordWallRole | BoxSideRole | null,
|
||||
): void => {
|
||||
if (!dock) return;
|
||||
(dock as { show: (role: FordWallRole | BoxSideRole | null) => void }).show(role);
|
||||
if (role) adoptAdjustPanel(dock.root);
|
||||
else releaseAdjustPanel(dock.root);
|
||||
};
|
||||
|
||||
const toggleFord = structureToggle<FordWallRole>({
|
||||
current: () => fordRole,
|
||||
setCurrent: (value) => (fordRole = value),
|
||||
setActive: (value) => setFordActive(value),
|
||||
showPanel: (visible) => fordPanel?.show(visible ? fordRole : null),
|
||||
showPanel: (visible) => {
|
||||
fordPanel?.show(visible ? fordRole : null);
|
||||
syncDock(fordDock, visible ? fordRole : null);
|
||||
},
|
||||
persist: (value) => ford?.select(chainage, value),
|
||||
isCardSelected: deps.isCardSelected,
|
||||
clearArea: deps.clearArea,
|
||||
@@ -102,7 +117,10 @@ export function createBodyWiring(deps: BodyWiringDeps): BodyWiring {
|
||||
current: () => boxRole,
|
||||
setCurrent: (value) => (boxRole = value),
|
||||
setActive: (value) => setBoxActive(value),
|
||||
showPanel: (visible) => boxPanel?.show(visible ? boxRole : null),
|
||||
showPanel: (visible) => {
|
||||
boxPanel?.show(visible ? boxRole : null);
|
||||
syncDock(boxDock, visible ? boxRole : null);
|
||||
},
|
||||
persist: (value) => box?.select(chainage, value),
|
||||
isCardSelected: deps.isCardSelected,
|
||||
clearArea: deps.clearArea,
|
||||
@@ -111,6 +129,8 @@ export function createBodyWiring(deps: BodyWiringDeps): BodyWiring {
|
||||
|
||||
fordPanel?.show(fordRole);
|
||||
boxPanel?.show(boxRole);
|
||||
syncDock(fordDock, fordRole);
|
||||
syncDock(boxDock, boxRole);
|
||||
|
||||
return {
|
||||
fordAdjust: () => ford?.adjustFor(chainage),
|
||||
@@ -125,12 +145,14 @@ export function createBodyWiring(deps: BodyWiringDeps): BodyWiring {
|
||||
if (fordRole) setter(fordRole);
|
||||
// 창은 그리기 전에 만들어져 값이 비어 있다 — 계산 결과가 들어온 뒤 다시 렌더한다.
|
||||
fordPanel?.show(fordRole);
|
||||
syncDock(fordDock, fordRole);
|
||||
},
|
||||
attachBox: (setter, layout) => {
|
||||
setBoxActive = setter;
|
||||
boxLayout = layout;
|
||||
if (boxRole) setter(boxRole);
|
||||
boxPanel?.show(boxRole);
|
||||
syncDock(boxDock, boxRole);
|
||||
},
|
||||
clearSelection: () => {
|
||||
if (fordRole !== null) toggleFord(fordRole);
|
||||
|
||||
@@ -44,6 +44,10 @@ import { computeHaulPlan } from "@util/common_util_mass_haul_balance";
|
||||
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||
import { staleDesignChainages } from "./B06_Section_UI_Section_Common";
|
||||
import { createStandardPanel, type StandardPanelController } from "./B06_Section_UI_Standard_Panel";
|
||||
import {
|
||||
createB06StructuresPanel,
|
||||
wireStructureSelection,
|
||||
} from "./B06_Section_UI_Page_Structures_Panel";
|
||||
import "./B06_Section_UI_Style.css";
|
||||
import "./B06_Section_UI_Style_Cross.css";
|
||||
import "./B06_Section_UI_Style_Cross_Controls.css";
|
||||
@@ -115,13 +119,36 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
confirmButton.disabled = true;
|
||||
const actionRow = document.createElement("div");
|
||||
// 사이드 최하단 고정(공용 ui-sidebar-actions) — 스크롤에서 제외(2026-08-05 사용자 지시).
|
||||
actionRow.className = "b06-profile__actions ui-sidebar-actions";
|
||||
actionRow.className = "b06-profile__actions";
|
||||
actionRow.append(goProfileButton, saveButton, confirmButton);
|
||||
|
||||
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤 — 가장 가까운 카드로 간다. */
|
||||
const focusStationAt = (chainageM: number): void => {
|
||||
const sections = sectionDetail?.cross_sections;
|
||||
if (!sections?.length) return;
|
||||
const nearest = sections.reduce((best, entry) =>
|
||||
Math.abs(entry.chainage_m - chainageM) < Math.abs(best.chainage_m - chainageM) ? entry : best,
|
||||
);
|
||||
sectionView.focusStation(nearest.station_id);
|
||||
};
|
||||
|
||||
// 「구조물 배치」 — B05와 같은 컨테이너·하단 목록 템플릿(2026-08-29 일원화).
|
||||
// 하단 고정 dock 도 B05와 같은 구조: [구조물 목록][구분선][액션 버튼 행].
|
||||
const structuresPanel = createB06StructuresPanel({
|
||||
projectId,
|
||||
stationInterval: () => stationInterval ?? 20,
|
||||
focusChainage: focusStationAt,
|
||||
});
|
||||
const dockDivider = document.createElement("hr");
|
||||
dockDivider.className = "b05-structure__divider";
|
||||
const actionDock = document.createElement("div");
|
||||
actionDock.className = "b05-route__dock ui-sidebar-actions";
|
||||
actionDock.append(structuresPanel.listRoot, dockDivider, actionRow);
|
||||
|
||||
const leftForm = document.createElement("div");
|
||||
leftForm.className = "b06-profile__form";
|
||||
leftForm.append(viewGroup, standardGroup, actionRow);
|
||||
// 순서: 보기 반폭 → 구조물 배치(표준 횡단면 위 — 2026-08-29 사용자 확정) → 표준.
|
||||
leftForm.append(viewGroup, structuresPanel.root, standardGroup, actionDock);
|
||||
// 그룹 제목 행 클릭 시 접기/펼치기(N-4-1). 액션 버튼 행은 collapsible 아님.
|
||||
attachCollapsible(leftForm);
|
||||
|
||||
@@ -437,6 +464,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
stationControls.ford,
|
||||
stationControls.box,
|
||||
);
|
||||
// 횡단도 벽·구체 선택 → 좌측 「구조물 배치」 폼에 그 시설 로드(2026-08-29 일원화).
|
||||
wireStructureSelection(stationControls, () => sectionDetail, structuresPanel);
|
||||
|
||||
// 메인 영역: 종·횡단 도면(sectionView) 또는 안내 메시지를 표시한다.
|
||||
const mainArea = document.createElement("div");
|
||||
@@ -659,6 +688,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
currentRouteId = context.route_id;
|
||||
loadRockOffsets();
|
||||
stationControls.load();
|
||||
// 구조물 배치 데이터(타입·정본·관 지점) — 카드 로드와 병행, 화면을 잠그지 않는다.
|
||||
void structuresPanel.load();
|
||||
try {
|
||||
const existing = await getSections(projectId, context.route_id);
|
||||
if (!existing.longitudinal) {
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Page_Structures_Panel.ts
|
||||
* B06 좌측 「구조물 배치」 — B05와 **같은 컨테이너·목록 템플릿**을 세운다
|
||||
* (2026-08-29 사용자: B05/B06 인터페이스 일원화. 페이지 본체는 700줄 제한으로
|
||||
* 여기서 조립만 받아 간다).
|
||||
*
|
||||
* · 폼·목록 구현 = 공용 진입점(`A00_Common/b_structures_section`) — B05 그대로.
|
||||
* · 구조물(B~G군) 추가·수정·삭제는 세션 미저장분(`writePendingStructures`)에 쌓고
|
||||
* [임시저장]·[확정]의 `flushPendingStructures`가 정본에 쓴다(캐시→저장 원칙).
|
||||
* · 계곡 통과 시설(A군, pipe_points 정본)은 **표시·선택만** — 추가·이동·삭제는
|
||||
* 배수유역 재분할 체인이 있는 B05 몫이라 안내만 한다(2026-08-29 1차 범위).
|
||||
* · 목록·폼 선택 → 해당 측점 카드 선택·스크롤. 횡단도 벽·구체 선택 → 폼에 그 시설
|
||||
* 로드(소유 측점 기준). 하위 [횡단 조정] 컨테이너는 Adjust_Dock이 맡는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createStructuresSection, type PipeFacilityItem } from "../A00_Common/b_structures_section";
|
||||
import {
|
||||
fetchStructures,
|
||||
fetchStructureTypes,
|
||||
readPendingStructures,
|
||||
structureAnchorM,
|
||||
writePendingStructures,
|
||||
type StructureInstance,
|
||||
} from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { adjustDockRoot, resetAdjustDock } from "./B06_Section_UI_Adjust_Dock";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import type { StationControls } from "./B06_Section_UI_Page_Station_Controls";
|
||||
|
||||
const PIPE_GUIDE = "계곡 통과 시설의 추가·이동·삭제는 B05(종단) 화면에서 합니다.";
|
||||
|
||||
export interface B06StructuresPanelDeps {
|
||||
projectId: string | null;
|
||||
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
|
||||
stationInterval: () => number;
|
||||
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */
|
||||
focusChainage: (chainageM: number) => void;
|
||||
}
|
||||
|
||||
export interface B06StructuresPanel {
|
||||
/** 「구조물 배치」 섹션(하위 [횡단 조정] 포함) — 좌측 패널에 붙인다. */
|
||||
root: HTMLElement;
|
||||
/** 배치된 구조물 목록 — 하단 고정 dock에 붙인다(B05와 같은 자리). */
|
||||
listRoot: HTMLElement;
|
||||
/** 타입 레지스트리·구조물 정본(미저장분 우선)·관 지점을 받아 채운다. */
|
||||
load: () => Promise<void>;
|
||||
/** 횡단도에서 고른 벽·구체의 시설을 폼에 올린다(null = 해제). */
|
||||
showPipeAt: (chainageM: number | null) => void;
|
||||
}
|
||||
|
||||
export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06StructuresPanel {
|
||||
let structures: StructureInstance[] = [];
|
||||
|
||||
/** 새 항목에 식별자를 미리 붙인다 — 저장 전에도 목록에서 고르고 지울 수 있어야
|
||||
* 하고, 서버는 빈 값일 때만 새로 발급한다(B05 다리와 같은 규칙). */
|
||||
const withLocalIds = (next: StructureInstance[]): StructureInstance[] =>
|
||||
next.map((item) =>
|
||||
item.structure_id ? item : { ...item, structure_id: crypto.randomUUID().replace(/-/g, "") },
|
||||
);
|
||||
|
||||
const section = createStructuresSection({
|
||||
onChange: (next) => {
|
||||
structures = withLocalIds(next);
|
||||
section.setStructures(structures);
|
||||
if (deps.projectId) writePendingStructures(deps.projectId, structures);
|
||||
},
|
||||
onSelect: (structure) => {
|
||||
if (structure) deps.focusChainage(structureAnchorM(structure));
|
||||
},
|
||||
getInterval: deps.stationInterval,
|
||||
onPipeAdd: () => showToast(PIPE_GUIDE, "error"),
|
||||
onPipeUpdate: () => showToast(PIPE_GUIDE, "error"),
|
||||
onPipeRemove: () => showToast(PIPE_GUIDE, "error"),
|
||||
onPipeSelect: (chainageM) => {
|
||||
if (chainageM !== null) deps.focusChainage(chainageM);
|
||||
},
|
||||
});
|
||||
|
||||
// 하위 [횡단 조정] 컨테이너 — 폼 본문 맨 아래, 기존 항목과 경계 구분(2026-08-29
|
||||
// 사용자: 병합 항목은 하위 컨테이너로 담아 혼동 방지).
|
||||
resetAdjustDock();
|
||||
section.root.querySelector(".b05-route__panel-body")?.append(adjustDockRoot());
|
||||
|
||||
async function load(): Promise<void> {
|
||||
if (!deps.projectId) return;
|
||||
const projectId = deps.projectId;
|
||||
try {
|
||||
const [types, stored, pipeResponse] = await Promise.all([
|
||||
fetchStructureTypes(),
|
||||
fetchStructures(projectId),
|
||||
fetchDetailPipePoints(projectId),
|
||||
]);
|
||||
section.setTypes(types);
|
||||
// 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(B05와 같은 규칙).
|
||||
structures = readPendingStructures(projectId) ?? stored.structures;
|
||||
section.setStructures(structures);
|
||||
const pipes: PipeFacilityItem[] = pipeResponse.pipe_points.map((pipe) => ({
|
||||
chainage_m: pipe.chainage_m,
|
||||
facility: pipe.facility ?? "pipe",
|
||||
start_m: pipe.start_m,
|
||||
end_m: pipe.end_m,
|
||||
source: pipe.source,
|
||||
options: pipe.options,
|
||||
design_flow_m3s: null,
|
||||
}));
|
||||
section.setPipeFacilities(pipes);
|
||||
} catch (error) {
|
||||
showToast(
|
||||
error instanceof Error ? error.message : "구조물 정보를 불러오지 못했습니다.",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
root: section.root,
|
||||
listRoot: section.listRoot,
|
||||
load,
|
||||
showPipeAt: (chainageM) => section.selectPipeByChainage(chainageM),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 횡단도 선택 → 좌측 폼 연동 배선. 벽(기슭막이)·세월교·BOX 선택이 일어나면 그
|
||||
* 구조물의 **소유 측점** 시설을 좌측 폼에 올린다(2026-08-29 일원화 3단계).
|
||||
* 제어 객체의 select를 감싸기만 한다 — 조작·저장 경로는 그대로다.
|
||||
*/
|
||||
export function wireStructureSelection(
|
||||
stationControls: StationControls,
|
||||
detail: () => SectionDetailResponse | null,
|
||||
panel: B06StructuresPanel,
|
||||
): void {
|
||||
const ownerChainageOf = (chainageM: number): number => {
|
||||
const sectionAt = detail()?.cross_sections.find(
|
||||
(entry) => Math.abs(entry.chainage_m - chainageM) < 0.01,
|
||||
);
|
||||
return (sectionAt && stationControls.structureSpan.ownerOf(sectionAt)?.chainage_m) ?? chainageM;
|
||||
};
|
||||
|
||||
const origRevet = stationControls.revetOffset.select;
|
||||
stationControls.revetOffset.select = (chainageM, key) => {
|
||||
origRevet(chainageM, key);
|
||||
panel.showPipeAt(key ? ownerChainageOf(chainageM) : null);
|
||||
};
|
||||
const origFord = stationControls.ford.select;
|
||||
stationControls.ford.select = (chainageM, role) => {
|
||||
origFord(chainageM, role);
|
||||
panel.showPipeAt(role ? chainageM : null);
|
||||
};
|
||||
const origBox = stationControls.box.select;
|
||||
stationControls.box.select = (chainageM, role) => {
|
||||
origBox(chainageM, role);
|
||||
panel.showPipeAt(role ? chainageM : null);
|
||||
};
|
||||
}
|
||||
@@ -112,6 +112,9 @@ export interface SectionViewController {
|
||||
) => void;
|
||||
/** 측점 하나의 카드만 새로 만들어 교체한다 (전체 재렌더 없이 설계 변경 반영). */
|
||||
refreshCard: (chainageM: number) => void;
|
||||
/** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음
|
||||
* (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). */
|
||||
focusStation: (stationId: string) => void;
|
||||
clear: () => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
@@ -701,6 +704,10 @@ export function createSectionView(
|
||||
if (renderWidth <= 0) requestAnimationFrame(() => resizeObserver.observe(root));
|
||||
},
|
||||
refreshCard,
|
||||
focusStation(stationId) {
|
||||
if (selectedStationId !== stationId) selectStation(stationId, true);
|
||||
else revealCard(stationId, "smooth");
|
||||
},
|
||||
clear() {
|
||||
currentDetail = null;
|
||||
selectedStationId = null;
|
||||
|
||||
@@ -227,3 +227,45 @@
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
/* ── 좌측 [횡단 조정] 사본(2026-08-29 B05/B06 일원화) ─────────────────────
|
||||
* 오버레이 창과 같은 DOM을 좌측 사이드바에 흐름 배치로 세운다. */
|
||||
.b06-structure-panel--dock {
|
||||
position: static;
|
||||
z-index: auto;
|
||||
max-height: none;
|
||||
margin-top: var(--spacing-8);
|
||||
border-color: var(--color-border);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 9키·십자 위치제어(이동 D-pad·집수정 9키)는 횡단도 오버레이 전용
|
||||
* (2026-08-29 사용자 지시 7) — 사본에서는 그 행을 통째로 숨긴다. */
|
||||
.b06-structure-panel--dock
|
||||
.b06-structure-panel__struct:has(> .b06-structure-panel__controls.b06-structure-panel__buttons),
|
||||
.b06-structure-panel--dock
|
||||
.b06-structure-panel__struct:has(
|
||||
> .b06-structure-panel__controls.b06-structure-panel__basin-buttons
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* [횡단 조정] 하위 컨테이너 — 구조물 배치 폼과 경계 구분(접이식). */
|
||||
.b06-adjust-dock {
|
||||
margin-top: var(--spacing-8);
|
||||
padding-top: var(--spacing-8);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b06-adjust-dock > summary {
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-adjust-dock__empty {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user