diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 1c25fc4b..0936561c 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -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; diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index 3ab454dc..c43d7700 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -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, 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 = [ diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 2b05d9bc..09e31e10 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -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; /** 배치된 구조물 목록(