feat(B05,B06): 배관 조정 항목을 유입구·유출구 폼으로 모은다

2026-08-29 사용자 지시(좌측 패널 항목 정리 — 배관 자리):
- 길이·높이·기준측점 전/후 입력칸에 [-][+] 묶음 추가(stepper). 눈금은 조정창과
  같다(길이 1m·전후 0.5m·높이 0.1m). 래퍼 여백 0이라 2열 격자는 그대로다.
- 형태·구조물 형식 행은 [횡단 조정] 사본에서 숨김 — 폼의 형태·구조와 중복.
- 단 수·옵션 행은 고른 벽 쪽(유입구/유출구) 그룹으로 DOM 이식. 배선을 옮기지 않아
  값·동기는 그대로고, 사본이 갈릴 때마다 다시 이식한다.
- 유입구 '구조' 드롭다운에 조정창 형식(자동·집수정 I/ㄴ/ㄷ)을 병합. 고른 값이
  정본 둘(inlet_type·inlet_structure)을 함께 정하고, B06에서는 유입측 형식 제어와
  양방향으로 맞춘다. 형태·형식 연결은 미연결 상태 그대로 둔다.

검증(공용 브라우저): 유입구 스텝 5·유출구 4개, 구조 목록 6종 병합, 벽 선택 시
단 수·옵션이 유출구 그룹으로 이동(dock에는 높이·구간값만 남음). B05도 동일 폼이
정상 표시되고 [횡단 조정]은 뜨지 않음. typecheck 통과.
This commit is contained in:
2026-08-29 15:30:55 +09:00
parent 608bdc9f90
commit 8e8849e4ac
9 changed files with 285 additions and 17 deletions
@@ -51,10 +51,35 @@ import {
OUTLET_REVET_KEYS,
putNumber,
REVET_COMMON_DEFAULTS,
stepper,
WING_IN_KEYS,
WING_OUT_KEYS,
} from "./B05_Profile_UI_Drainage_Facility_Fields";
/** B06 유입측 구조물 형식 — 조정창 드롭다운과 같은 값 집합(2026-08-29 병합).
* 문자열 리터럴로 두어 B05가 B06 모듈에 기대지 않게 한다. */
export type InletStructureKind = "auto" | "revet" | "I" | "L" | "U";
/** 유입구 "구조" 목록 — 조정창의 구조물 형식(자동/기슭막이/집수정 I·ㄴ·ㄷ)을 이 한
* 드롭다운으로 합쳤다(2026-08-29 사용자 지시 5: 일단 리스트를 합치고 중복은 뒤에 뺀다).
* 고른 값 하나가 정본 둘을 정한다 — 관 옵션 `inlet_type`(기슭막이/집수정)과 B06
* 유입측 형식(`inlet_structure`). */
const INLET_KINDS: ReadonlyArray<{
label: string;
type: "기슭막이" | "집수정";
structure: InletStructureKind;
}> = [
{ label: "기슭막이", type: "기슭막이", structure: "revet" },
{ label: "집수정", type: "집수정", structure: "auto" },
{ label: "자동(규칙)", type: "기슭막이", structure: "auto" },
{ label: "집수정 I형", type: "집수정", structure: "I" },
{ label: "집수정 ㄴ형", type: "집수정", structure: "L" },
{ label: "집수정 ㄷ형", type: "집수정", structure: "U" },
];
const inletKindOf = (label: string): (typeof INLET_KINDS)[number] =>
INLET_KINDS.find((kind) => kind.label === label) ?? INLET_KINDS[0];
/** 시설 확장 정보 한 건 — 관 지점(chainage)에 얹힌다. 배관(pipe)도 부속 옵션을
* 가지면 저장한다. */
export interface FacilityAttributes {
@@ -153,6 +178,14 @@ export interface FacilityOptionsForm {
/** 설계유량만 갱신한다 — 필드 값은 건드리지 않는다. 임시 배치(2026-08-18)의
* 유역 재계산 결과를 입력 중인 폼에 살짝 반영하는 용도. */
setDesignFlow: (designFlow: number | null) => void;
/** 조정창에서 옮겨 오는 행(단 수·옵션)이 붙을 자리 — B06만 채운다(2026-08-29 지시 4). */
inletSlot: HTMLElement;
outletSlot: HTMLElement;
/** 유입구 "구조"에 합쳐진 B06 형식 값(2026-08-29 지시 5). B06이 조정창 값과 맞춘다. */
inletStructure: () => InletStructureKind;
setInletStructure: (value: InletStructureKind) => void;
/** 구조가 바뀔 때 B06 유입측 형식 제어에 알린다(B05는 등록하지 않는다). */
onInletStructureChange: (handler: (value: InletStructureKind) => void) => void;
}
/** 구조물 배치 폼의 옵션 자리에 끼워지는 계곡 통과 시설 부속 옵션 서브폼. */
@@ -182,8 +215,8 @@ export function createFacilityOptionsForm(
// 빈값(미지정)을 두면 하위 칸이 전부 숨어 기본값이 안 보인다 — 기슭막이를 기본으로
// 두고 저유량 지점에서 집수정으로 바꾼다(2026-08-17 사용자 지시).
const inletType = document.createElement("select");
inletType.replaceChildren(...["기슭막이", "집수정"].map((value) => new Option(value, value)));
inletType.value = "기슭막이";
inletType.replaceChildren(...INLET_KINDS.map((kind) => new Option(kind.label, kind.label)));
inletType.value = INLET_KINDS[0].label;
const basinForm = optionalSelect("선택", [
"동물이동형",
"□형(기본형)",
@@ -201,17 +234,25 @@ export function createFacilityOptionsForm(
// 집수정 종방향 길이 — 3D 예상형상용, 전후 동일 배분·기본 2m(2026-08-23 사용자).
const basinLength = numberInput("0.1");
basinLength.value = "2";
const basinRows = [grid(labeled("형식", basinForm), labeled("길이 (m)", basinLength))];
const basinRows = [
grid(labeled("형식", basinForm), labeled("길이 (m)", stepper(basinLength, 1))),
];
const inletRevet = createRevetmentFields(INLET_REVET_KEYS, {
...REVET_COMMON_DEFAULTS,
form: "돌쌓기(찰)",
});
// 첫 행 = [구조][형태] — 구조에 따라 집수정 형태와 기슭막이 형태가 갈아 끼워진다
// (2026-08-17 사용자 지시 3).
// 조정창에서 옮겨 오는 행(단 수·옵션)이 붙을 자리(2026-08-29 사용자 지시 4).
// 비어 있으면 아무 자리도 차지하지 않는다.
const inletSlot = document.createElement("div");
inletSlot.className = "b05-structure__adjust-slot";
const outletSlot = inletSlot.cloneNode() as HTMLDivElement;
inletGroup.body.append(
grid(labeled("구조", inletType), basinMaterialField, inletRevet.formField),
...basinRows,
...inletRevet.rows,
inletSlot,
);
// ── 유출구 — 구조 선택지는 기슭막이뿐이지만 양식을 맞춘다(사용자 지시 2·4) ──
@@ -225,6 +266,7 @@ export function createFacilityOptionsForm(
outletGroup.body.append(
grid(labeled("구조", outletType), outletRevet.formField),
...outletRevet.rows,
outletSlot,
);
// ── 독립 기슭막이(2026-08-28 이관: 배관처럼 pipe_points 관리) ─────────────
@@ -405,8 +447,10 @@ export function createFacilityOptionsForm(
}
if (isPipe) {
// 유입구 구조에 따라 집수정 칸과 기슭막이 칸을 갈아 끼운다(사용자 지시 6~8).
const basin = inletType.value === "집수정";
const revet = inletType.value === "기슭막이";
// 병합 목록이라 판정은 라벨이 아니라 매핑된 정본 종류로 한다(2026-08-29).
const kind = inletKindOf(inletType.value);
const basin = kind.type === "집수정";
const revet = kind.type === "기슭막이";
basinRows.forEach((row) => (row.hidden = !basin));
basinMaterialField.hidden = !basin;
inletRevet.formField.hidden = !revet;
@@ -444,8 +488,29 @@ export function createFacilityOptionsForm(
fordWidth,
].forEach((input) => input.addEventListener("change", emit));
/** 유입구 구조 변경 수신자(B06 유입측 형식 제어). B05는 비워 둔다. */
let inletStructureHandler: ((value: InletStructureKind) => void) | null = null;
inletType.addEventListener("change", () =>
inletStructureHandler?.(inletKindOf(inletType.value).structure),
);
return {
root,
inletSlot,
outletSlot,
inletStructure: () => inletKindOf(inletType.value).structure,
setInletStructure(value) {
// 지금 고른 정본 종류(기슭막이/집수정)는 지키고 형식만 맞춘다.
const type = inletKindOf(inletType.value).type;
const hit =
INLET_KINDS.find((kind) => kind.type === type && kind.structure === value) ??
INLET_KINDS.find((kind) => kind.structure === value);
if (hit) inletType.value = hit.label;
syncVisibility();
},
onInletStructureChange(handler) {
inletStructureHandler = handler;
},
setFacility(facility, options = {}, designFlow = null) {
current = facility;
designFlowM3s = designFlow ?? null;
@@ -458,7 +523,15 @@ export function createFacilityOptionsForm(
const isFord = facility === "ford_bridge";
pipeMaterial.value = text("pipe_kind") || "파형강관";
pipeDiameter.value = text("pipe_diameter_mm") || "1000";
inletType.value = text("inlet_type") || "기슭막이";
// 병합 목록: 저장된 정본 둘(inlet_type·inlet_structure)로 라벨을 되찾는다.
const savedType = text("inlet_type") || "기슭막이";
const savedStructure = text("inlet_structure");
inletType.value = (
INLET_KINDS.find(
(kind) =>
kind.type === savedType && (!savedStructure || kind.structure === savedStructure),
) ?? inletKindOf(savedType)
).label;
outletType.value = text("outlet_type") || "기슭막이";
basinForm.value = text("inlet_basin_form");
basinMaterial.value = text("inlet_basin_material");
@@ -510,12 +583,15 @@ export function createFacilityOptionsForm(
if (current === "pipe") {
options.pipe_diameter_mm = Number(pipeDiameter.value);
options.pipe_kind = pipeMaterial.value;
options.inlet_type = inletType.value;
if (inletType.value === "집수정") {
const inletKind = inletKindOf(inletType.value);
options.inlet_type = inletKind.type;
// B06 유입측 형식(조정창과 같은 값) — 병합 드롭다운이 함께 정한다(2026-08-29).
options.inlet_structure = inletKind.structure;
if (inletKind.type === "집수정") {
if (basinForm.value) options.inlet_basin_form = basinForm.value;
if (basinMaterial.value) options.inlet_basin_material = basinMaterial.value;
putNumber(options, "inlet_basin_length_m", basinLength.value);
} else if (inletType.value === "기슭막이") {
} else {
inletRevet.read(options);
}
options.outlet_type = outletType.value;
@@ -72,6 +72,39 @@ export function numberInput(step: string, min = "0", placeholder = ""): HTMLInpu
return input;
}
/**
* 숫자 칸 + [][] 한 묶음(2026-08-29 사용자 지시 2) — 조정창처럼 눈금으로도
* 고칠 수 있게 한다. 래퍼 여백은 0이라 기존 격자 레이아웃을 흔들지 않는다.
* 버튼은 값을 고친 뒤 input·change를 모두 울려 기존 리스너(연동·저장)가 그대로 탄다.
*/
export function stepper(input: HTMLInputElement, stepM: number): HTMLElement {
const wrap = document.createElement("div");
wrap.className = "b05-structure__stepper";
const nudge = (delta: number) => (): void => {
const current = Number.parseFloat(input.value);
const min = Number.parseFloat(input.min);
const floor = Number.isFinite(min) ? min : 0;
const next = Math.max(
floor,
Math.round(((Number.isFinite(current) ? current : 0) + delta) * 10) / 10,
);
input.value = next.toFixed(1);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
};
const button = (label: string, delta: number): HTMLButtonElement => {
const element = document.createElement("button");
element.type = "button";
element.className = "b05-structure__step";
element.textContent = label;
element.title = `${stepM}m ${delta > 0 ? "늘리기" : "줄이기"}`;
element.addEventListener("click", nudge(delta));
return element;
};
wrap.append(input, button("", -stepM), button("", stepM));
return wrap;
}
/** 양수만 옵션에 싣는다 — 빈칸·0·음수는 값이 없는 것으로 본다. */
export function putNumber(target: Record<string, string | number>, key: string, raw: string): void {
const value = Number.parseFloat(raw);
@@ -207,11 +240,12 @@ export function createRevetmentFields(
(keep === before ? after : before).value = Math.max(total - kept, 0).toFixed(1);
};
// 눈금은 조정창과 같은 값이다 — 길이 1m, 전/후 0.5m, 높이 0.1m(2026-08-29 일원화).
const formField = labeled("형태", form);
const lengthField = length ? labeled("길이 (m)", length) : null;
const heightField = labeled("높이 (m)", height);
const beforeField = before ? labeled("기준측점 전 (m)", before) : null;
const afterField = after ? labeled("기준측점 후 (m)", after) : null;
const lengthField = length ? labeled("길이 (m)", stepper(length, 1)) : null;
const heightField = labeled("높이 (m)", stepper(height, 0.1));
const beforeField = before ? labeled("기준측점 전 (m)", stepper(before, 0.5)) : null;
const afterField = after ? labeled("기준측점 후 (m)", stepper(after, 0.5)) : null;
// 형태는 그룹 첫 행(구조 옆)에 따로 놓이므로 여기 행에서는 뺀다.
const rows = [
@@ -25,6 +25,7 @@ import type { PipeFacility, PipeSource } from "../B04_PreProcess/B04_PreProcess_
import {
createFacilityOptionsForm,
type FacilityAttributes,
type FacilityOptionsForm,
} from "./B05_Profile_UI_Drainage_Facility";
import { field, numberInput, select, stationFields } from "./B05_Profile_UI_Structures_Fields";
import { renderStructureList } from "./B05_Profile_UI_Structures_List";
@@ -63,6 +64,9 @@ export interface PipeFacilityItem {
export interface StructuresSection {
root: HTMLElement;
/** 계곡 통과 시설 부속 옵션 서브폼 — B06이 조정창 행을 이 폼의 유입구·유출구
* 그룹으로 옮겨 붙이고 유입측 형식을 맞춘다(2026-08-29 일원화). */
facility: FacilityOptionsForm;
/** 배치된 구조물 목록(<ul>) — 섹션 본문이 아니라 사이드 하단 고정 영역에 붙인다
* (2026-08-18 사용자 지시: 폼 길이에 밀려 목록이 화면 밖으로 나가던 문제). */
listRoot: HTMLElement;
@@ -891,6 +895,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
return {
root,
facility: facilityOptions,
listRoot: list,
setTypes(next) {
types = next;
@@ -243,3 +243,36 @@
flex: 1;
min-width: 0;
}
/* 숫자 칸 + [][] 묶음(2026-08-29 사용자 지시 2) — 유입구·유출구 폼에서도
조정창처럼 눈금으로 고친다. 여백 0이라 기존 2열 격자 폭·간격은 그대로다. */
.b05-structure__stepper {
display: flex;
min-width: 0;
align-items: stretch;
margin: 0;
padding: 0;
gap: 2px;
}
.b05-structure__stepper > input {
min-width: 0;
flex: 1 1 auto;
}
.b05-structure__step {
flex: none;
width: 20px;
padding: 0;
border: 1px solid var(--color-border, #3a3f4a);
border-radius: var(--radius-inputs, 4px);
background: var(--color-surface);
color: var(--color-text-body);
font-size: 11px;
line-height: 1;
cursor: pointer;
}
.b05-structure__step:hover {
border-color: var(--color-royal-amethyst, rgb(109 40 217));
}
+42
View File
@@ -13,6 +13,45 @@
let dockRoot: HTMLDetailsElement | null = null;
let slot: HTMLElement | null = null;
let placeholder: HTMLElement | null = null;
/** 유입구·유출구 그룹의 이식 자리(2026-08-29 사용자 지시 4). B06 패널이 등록한다. */
let sideSlots: { inlet: HTMLElement; outlet: HTMLElement } | null = null;
/** 단 수·옵션 행을 옮겨 붙일 자리를 등록한다(계곡 통과 시설 폼의 유입구·유출구). */
export function setAdjustSideSlots(next: { inlet: HTMLElement; outlet: HTMLElement }): void {
sideSlots = next;
}
/** 이식 자리가 지금 화면에 서 있는지 — 숨겨진 그룹(다른 시설)이면 옮기지 않는다. */
function slotUsable(target: HTMLElement | undefined): target is HTMLElement {
if (!target) return false;
const group = target.closest("fieldset");
return !!group && !group.hidden;
}
/** 옮겨 놨던 행을 비운다 — 사본이 갈릴 때마다 옛 행을 남기지 않는다. */
function clearSideSlots(): void {
sideSlots?.inlet.replaceChildren();
sideSlots?.outlet.replaceChildren();
}
/**
* 조정창 사본의 단 수·옵션 행을 고른 벽 쪽(유입/유출) 그룹으로 옮긴다. 행 DOM을
* 그대로 옮기므로 배선·값 동기는 손대지 않는다. 자리가 없으면 사본에 그대로 둔다.
*/
function relocateSideRows(panelRoot: HTMLElement): void {
clearSideSlots();
const side = panelRoot.dataset.side;
if (!sideSlots || (side !== "inlet" && side !== "outlet")) return;
const target = sideSlots[side];
if (!slotUsable(target)) return;
const rows = panelRoot.querySelectorAll<HTMLElement>(
".b06-structure-panel__struct--tier, .b06-structure-panel__struct--options",
);
rows.forEach((row) => {
if (row.classList.contains("is-hidden")) return;
target.append(row);
});
}
function ensureDock(): void {
if (dockRoot) return;
@@ -39,6 +78,7 @@ export function adjustDockRoot(): HTMLElement {
export function resetAdjustDock(): void {
ensureDock();
slot!.replaceChildren();
clearSideSlots();
placeholder!.hidden = false;
}
@@ -50,11 +90,13 @@ export function adoptAdjustPanel(panelRoot: HTMLElement): void {
slot!.replaceChildren(panelRoot);
}
placeholder!.hidden = true;
relocateSideRows(panelRoot);
}
/** 이 사본이 슬롯에 서 있으면 내린다 — 다른 사본이 이미 대신 서 있으면 손대지 않는다. */
export function releaseAdjustPanel(panelRoot: HTMLElement): void {
if (!slot || !slot.contains(panelRoot)) return;
slot.replaceChildren();
clearSideSlots();
placeholder!.hidden = false;
}
@@ -215,6 +215,8 @@ export function buildStructurePanel(
// B05에서 고른 형태가 3종으로 뭉개졌다.
const materialParts = makeRow(L("B06_Cross_Mat_Label"));
const materialRow = materialParts.row;
// 좌측 유입구·유출구 폼의 "형태"와 중복 — 사본에서는 숨긴다(2026-08-29 지시 3).
materialRow.classList.add("b06-structure-panel__struct--form");
const materialSelect = document.createElement("select");
materialSelect.className = "b06-structure-panel__select";
for (const form of REVET_FORMS) {
@@ -249,6 +251,8 @@ export function buildStructurePanel(
// 유입측 구조물 형식 드롭다운(2026-08-22 사용자) — 자동/기슭막이/집수정 I·ㄴ·ㄷ.
const structureParts = makeRow(L("B06_Cross_Struct_Label"));
const structureRow = structureParts.row;
// 유입구 "구조" 드롭다운으로 병합됐다 — 사본에서는 숨긴다(2026-08-29 지시 5).
structureRow.classList.add("b06-structure-panel__struct--inlet-kind");
const select = document.createElement("select");
select.className = "b06-structure-panel__select";
const OPTIONS: Array<[InletStructureChoice, string]> = [
@@ -346,6 +350,8 @@ export function buildStructurePanel(
// 유입 벽·집수정 계류측은 `basinExtras`(bextra 키).
const extraParts = makeRow(L("B06_Cross_Extra_Count"));
const extraRow = extraParts.row;
// 유입구·유출구 그룹으로 옮겨 붙는 행(2026-08-29 지시 4).
extraRow.classList.add("b06-structure-panel__struct--tier");
const countValue = document.createElement("span");
countValue.className = "b06-structure-panel__hval";
let currentCount = 0;
@@ -391,6 +397,8 @@ export function buildStructurePanel(
// 연동 기슭막이 옵션 2가지(2026-08-24 사용자) — **버튼 두 개를 한 행**에 둔다.
// 연동은 측점별, 경사(종단경사 반영)는 기슭막이 한 벌 전체 공통이다.
const optionParts = makeRow("옵션");
// 단 수와 함께 유입구·유출구 그룹으로 옮겨 붙는다(2026-08-29 지시 4).
optionParts.row.classList.add("b06-structure-panel__struct--options");
optionParts.controls.classList.add("b06-structure-panel__toggles");
const makeToggle = (label: string, title: string, onToggle: (on: boolean) => void) => {
const button = document.createElement("button");
@@ -556,6 +564,14 @@ export function buildStructurePanel(
// 상태 따라 나타났다 사라지면 레이아웃이 널뛴다). 집수정 계류측은 둘 자리가
// 있을 때만 뜬다(배관 유입 기슭막이는 다단 채널 자체가 없다).
tierChannel = key === "outlet" ? "outlet" : key === "inlet" ? "basin" : null;
// 좌측 [횡단 조정] 사본의 단 수·옵션 행이 어느 그룹(유입구·유출구)으로 옮겨
// 붙을지 알린다(2026-08-29 지시 4). 다단 벽은 자기 채널 쪽으로 간다.
root.dataset.side =
tierChannel === "outlet" || key.startsWith("extra")
? "outlet"
: tierChannel === "basin" || key.startsWith("bextra")
? "inlet"
: "";
const tiers =
tierChannel === "outlet"
? deps.extraState()
+6 -1
View File
@@ -465,7 +465,11 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
stationControls.box,
);
// 횡단도 벽·구체 선택 → 좌측 「구조물 배치」 폼에 그 시설 로드(2026-08-29 일원화).
wireStructureSelection(stationControls, () => sectionDetail, structuresPanel);
const structureSelection = wireStructureSelection(
stationControls,
() => sectionDetail,
structuresPanel,
);
// 횡단도(카드) 자체를 골라도 그 측점 구조물 정보를 폼에 올린다(2026-08-29 사용자).
// 연동으로 옆에서 넘어온 카드는 **소유 측점** 시설을 보여 준다.
sectionView.setStationSelectListener((stationId) => {
@@ -478,6 +482,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
}
const owner = stationControls.structureSpan.ownerOf(target);
structuresPanel.showAtChainage(owner?.chainage_m ?? target.chainage_m);
structureSelection.syncInletStructure();
});
// 메인 영역: 종·횡단 도면(sectionView) 또는 안내 메시지를 표시한다.
@@ -24,7 +24,7 @@ import {
} 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 { adjustDockRoot, resetAdjustDock, setAdjustSideSlots } from "./B06_Section_UI_Adjust_Dock";
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
import type { StationControls } from "./B06_Section_UI_Page_Station_Controls";
@@ -50,6 +50,13 @@ export interface B06StructuresPanel {
/** 고른 횡단도(측점)에 있는 구조물을 폼에 올린다 — 계곡 통과 시설이 먼저고,
* 없으면 그 측점을 덮는 구조물 정본 항목. 둘 다 없으면 해제(2026-08-29). */
showAtChainage: (chainageM: number | null) => void;
/** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */
currentChainage: () => number | null;
/** 유입구 "구조"에 합쳐진 B06 유입측 형식 — 조정창 값과 맞춘다(2026-08-29 지시 5). */
facility: {
setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void;
onInletStructureChange: (handler: (value: "auto" | "revet" | "I" | "L" | "U") => void) => void;
};
}
/** 관 매칭 허용 오차(m) — 관은 계획선에 스냅돼 측점과 살짝 어긋난다(B05와 같은 기준). */
@@ -66,6 +73,8 @@ function coversChainage(structure: StructureInstance, chainageM: number): boolea
export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06StructuresPanel {
let structures: StructureInstance[] = [];
let pipeFacilities: PipeFacilityItem[] = [];
/** 지금 폼에 올라온 계곡 통과 시설의 누가거리 — 유입측 형식 반영 대상. */
let currentChainageM: number | null = null;
/** 새 항목에 식별자를 미리 붙인다 — 저장 전에도 목록에서 고르고 지울 수 있어야
* 하고, 서버는 빈 값일 때만 새로 발급한다(B05 다리와 같은 규칙). */
@@ -96,6 +105,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
// 사용자: 병합 항목은 하위 컨테이너로 담아 혼동 방지).
resetAdjustDock();
section.root.querySelector(".b05-route__panel-body")?.append(adjustDockRoot());
// 단 수·옵션 행은 유입구·유출구 그룹으로 옮겨 붙는다(2026-08-29 지시 4).
setAdjustSideSlots({ inlet: section.facility.inletSlot, outlet: section.facility.outletSlot });
async function load(): Promise<void> {
if (!deps.projectId) return;
@@ -132,8 +143,17 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
root: section.root,
listRoot: section.listRoot,
load,
showPipeAt: (chainageM) => section.selectPipeByChainage(chainageM),
currentChainage: () => currentChainageM,
facility: {
setInletStructure: (value) => section.facility.setInletStructure(value),
onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler),
},
showPipeAt: (chainageM) => {
currentChainageM = chainageM;
section.selectPipeByChainage(chainageM);
},
showAtChainage: (chainageM) => {
currentChainageM = null;
if (chainageM === null) {
section.selectById(null);
return;
@@ -144,6 +164,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
(entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M,
);
if (pipe) {
currentChainageM = pipe.chainage_m;
section.selectPipeByChainage(pipe.chainage_m);
return;
}
@@ -162,7 +183,7 @@ export function wireStructureSelection(
stationControls: StationControls,
detail: () => SectionDetailResponse | null,
panel: B06StructuresPanel,
): void {
): { syncInletStructure: () => void } {
const ownerChainageOf = (chainageM: number): number => {
const sectionAt = detail()?.cross_sections.find(
(entry) => Math.abs(entry.chainage_m - chainageM) < 0.01,
@@ -170,10 +191,27 @@ export function wireStructureSelection(
return (sectionAt && stationControls.structureSpan.ownerOf(sectionAt)?.chainage_m) ?? chainageM;
};
// 유입구 "구조" ↔ B06 유입측 형식 — 폼에서 고르면 조정값에 반영하고, 시설을 폼에
// 올릴 때는 조정값을 폼으로 되읽는다(2026-08-29 지시 5 병합).
const sectionAt = (chainageM: number) =>
detail()?.cross_sections.find((entry) => Math.abs(entry.chainage_m - chainageM) < 0.01);
panel.facility.onInletStructureChange((value) => {
const chainageM = panel.currentChainage();
if (chainageM === null) return;
stationControls.inletStructure.set(chainageM, value);
});
/** 폼에 올라온 시설의 유입측 형식을 폼 드롭다운에 되싣는다. */
const syncInletStructure = (): void => {
const chainageM = panel.currentChainage();
const target = chainageM === null ? undefined : sectionAt(chainageM);
if (target) panel.facility.setInletStructure(stationControls.inletStructure.valueFor(target));
};
const origRevet = stationControls.revetOffset.select;
stationControls.revetOffset.select = (chainageM, key) => {
origRevet(chainageM, key);
panel.showPipeAt(key ? ownerChainageOf(chainageM) : null);
syncInletStructure();
};
const origFord = stationControls.ford.select;
stationControls.ford.select = (chainageM, role) => {
@@ -185,4 +223,5 @@ export function wireStructureSelection(
origBox(chainageM, role);
panel.showPipeAt(role ? chainageM : null);
};
return { syncInletStructure };
}
@@ -285,3 +285,21 @@
.b06-structure-panel--dock .b06-structure-panel__basin-reset {
display: none;
}
/* 좌측 폼과 중복된 행은 사본에서 숨긴다(2026-08-29 사용자 지시 3·5):
* 형태 = 유입구·유출구 그룹의 "형태", 구조물 형식 = 유입구 "구조"로 합쳤다. */
.b06-structure-panel--dock .b06-structure-panel__struct--form,
.b06-structure-panel--dock .b06-structure-panel__struct--inlet-kind {
display: none;
}
/* 유입구·유출구 그룹으로 옮겨 붙은 행(단 수·옵션) — 그룹 안에서는 폼 여백을 따른다. */
.b05-structure__adjust-slot:empty {
display: none;
}
.b05-structure__adjust-slot {
display: flex;
flex-direction: column;
gap: 6px;
}