merge: main_laptop_1 → sub_laptop_7 52-3 — 충돌 다섯 = 양쪽 다 살림

- 명세 목록 = 박스암거 · 세월교 · 물넘이 포장 · 노면공 · 기타 전부
- B05 시설 폼 = BOX 명세 폼(메인) + 물넘이 포장 명세 폼(52-3) · 옛 BOX 프리셋 · 월류 칸 뺌
- 병합 키 = BOX 명세 키 + 물넘이 포장 경사각도 · 도수로/돌붙임 머리
- 표 채우기 컨테이너 = 물넘이 포장 도수로/돌붙임 + 기타 상세 내용
- 구조물정의 = 양쪽 그룹(box_culvert · ford_pavement) · 판 12 · 직전 메인 판 11 은 이력 판_11

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
2026-10-06 00:25:11 +09:00
co-authored by Claude Opus 5.5
28 changed files with 7871 additions and 140 deletions
+35 -80
View File
@@ -18,11 +18,9 @@
* 유입·유출 길이가 정해지면 위치 칸(시작·종료 측점)을 기준 − 유입 ~ 기준 + 유출로
* 자동 채운다. 형식·치수 제원은 B06/B07 필수 승격 대상이라 보이되 비필수다.
*
* BOX암거(2026-08-17 사용자 확정): 본체는 실무 관측 규격 2.0×2.0·3.0×3.0을 프리셋
* 셀렉트로 받고(기본 2.0×2.0), 그 밖의 규격만 "사용자 지정"으로 폭·높이를 직접
* 받는다 — 저장 키는 어느 쪽이든 body_width_m·body_height_m 숫자 그대로다.
* 날개벽은 유입·유출이 같은 조각(createWingFields)을 쓰고 설치 있음·짧은쪽 높이
* 1m·길이 2m·각도 45°가 기본이며, 설치를 "없음"으로 바꾸면 제원 칸이 접힌다.
* BOX암거 = 사용자 구조물 표 02 명세(PLAN 52-2 · `design_structure_box`) — 본체 규격 ·
* 유입 · 유출 구조. 저장 키는 body_width_m·body_height_m 숫자 · wing_in·wing_out 그대로.
* 날개벽 조각(createWingFields)은 숨김 — B06 조정 행 자리만 BOX · 세월교 명세 유입 · 유출 칸으로 옮겨 붙음.
*
* 짝: 시설 속성 붙이기 `carryIndices` ↔ 서버 `common_util/common_util_drainage_pipes.py`
* `carry_indices` — 같은 규칙(허용 오차 안 · 한 원본 한 번) · 거울 시험 test_b05_facility_carry.
@@ -35,8 +33,6 @@ import type {
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import {
BOX_SIZE_CUSTOM,
BOX_SIZE_PRESETS,
createRevetSideGroup,
createWingFields,
grid,
@@ -44,15 +40,19 @@ import {
INLET_REVET_KEYS,
type InletStructureKind,
labeled,
numberInput,
optionalSelect,
OUTLET_REVET_KEYS,
putNumber,
WING_IN_KEYS,
WING_OUT_KEYS,
} from "./B05_Profile_UI_Drainage_Facility_Fields";
import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form";
import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe";
import {
BOX_SPEC,
bodyOf,
bodyOptions,
boxOptionsIn,
} from "../A00_Common/design/design_structure_box";
import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford";
import { fordHeightHint, fordHeightM } from "../A00_Common/design/design_structure_ford_calc";
import { guardHeightHint, pipeLengthM } from "../A00_Common/design/design_structure_pipe_calc";
@@ -359,23 +359,9 @@ export function createFacilityOptionsForm(
return merged;
};
// ── BOX암거 — 본체 규격(프리셋 + 사용자 지정)·날개벽(유입·유출 개별) ────
// 폭·높이 자유 입력은 "사용자 지정"을 골랐을 때만 펼친다(2026-08-17 사용자 확정).
const boxSize = optionalSelect("— 안 정함 —", [
...BOX_SIZE_PRESETS.map(([label]) => label),
BOX_SIZE_CUSTOM,
]);
const boxWidth = numberInput("0.1", "0");
const boxHeight = numberInput("0.1", "0");
const boxCustomRow = grid(
labeled("본체 폭 (m)", boxWidth, "body_width_m"),
labeled("본체 높이 (m)", boxHeight, "body_height_m"),
);
const boxWrap = document.createElement("div");
boxWrap.append(
grid(labeled("본체 규격 (m)", boxSize, ["body_width_m", "body_height_m"])),
boxCustomRow,
);
// ── BOX암거 = 사용자 구조물 표 02 명세(PLAN 52-2) — 본체 · 유입 구조 · 유출 구조 ──
const boxForm = createSpecForm(specLinks, { onChange: () => emit() });
// 날개벽 칸 — 세월교 · BOX 가 같은 조각을 쓰던 자리 · 조정 행은 명세 유입 · 유출 칸으로 옮겨 붙음
const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS);
const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS);
const wingInHome = wingInFields.slot.parentElement!;
@@ -383,18 +369,6 @@ export function createFacilityOptionsForm(
const hiddenOptions = createHiddenOptions(root);
/** 고른 프리셋을 폭·높이 칸에 실어 둔다 — 저장도, "사용자 지정"으로 바꿨을 때의
* 출발값도 이 두 칸이 정본이다. 사용자 지정 상태에서는 손대지 않는다. */
function syncBoxSize(): void {
const preset = BOX_SIZE_PRESETS.find(([label]) => label === boxSize.value);
if (preset) {
boxWidth.value = String(preset[1]);
boxHeight.value = String(preset[2]);
}
// 「안 정함」도 폭·높이 칸을 접음 — 값이 없는 상태 그대로.
boxCustomRow.hidden = boxSize.value !== BOX_SIZE_CUSTOM;
}
// ── 물넘이 포장 = 사용자 구조물 표 03 명세(PLAN 52-3) — 경사각도 · 도수로/돌붙임 ·
// 옛 월류 폭 · 높이 · 바닥 경사 · 두께 · 길이 칸은 뺌(지난 정의 값) ──
const paveForm = createSpecForm(specLinks, { onChange: () => emit() });
@@ -416,7 +390,7 @@ export function createFacilityOptionsForm(
revetTopRow,
revetInlet.root,
revetOutlet.root,
boxWrap,
boxForm.root,
wingInFields.root,
wingOutFields.root,
);
@@ -430,14 +404,14 @@ export function createFacilityOptionsForm(
const isBox = current === "box_culvert";
const isBridge = current === "ford_bridge";
pipeForm.root.hidden = !isPipe;
boxForm.root.hidden = !isBox;
fordForm.root.hidden = !isBridge;
boxWrap.hidden = !isBox;
// 날개벽 칸 = BOX암거 · 세월교는 유입 · 유출 구조 컨테이너(52-4) — B06 조정 행 자리만 그 컨테이너 끝으로
const hasWing = isBox;
wingInFields.root.hidden = !hasWing;
wingOutFields.root.hidden = !hasWing;
(isBridge ? fordForm.slot("inlet") : wingInHome).append(wingInFields.slot);
(isBridge ? fordForm.slot("outlet") : wingOutHome).append(wingOutFields.slot);
// 날개벽 칸 = 옛 자리 — BOX(52-2) · 세월교(52-4)는 명세 유입 · 유출 구조 컨테이너 · B06 조정 행 자리만 그 컨테이너 끝으로
const spec = isBox ? boxForm : isBridge ? fordForm : null;
wingInFields.root.hidden = true;
wingOutFields.root.hidden = true;
(spec ? spec.slot("inlet") : wingInHome).append(wingInFields.slot);
(spec ? spec.slot("outlet") : wingOutHome).append(wingOutFields.slot);
paveForm.root.hidden = current !== "ford_pavement";
const isRevet = current === "revetment";
revetTopRow.hidden = !isRevet;
@@ -450,11 +424,6 @@ export function createFacilityOptionsForm(
// 추가 기슭막이 칸은 **내용이 있을 때만** 선다 — 고른 다단 벽의 행이 여기 오면 뜨고,
// 선택이 풀리면 스스로 사라진다.
extraGroup.root.hidden = extraSlot.childElementCount === 0;
if (isBox) syncBoxSize();
if (hasWing) {
wingInFields.syncVisibility();
wingOutFields.syncVisibility();
}
}
function emit(): void {
@@ -465,7 +434,7 @@ export function createFacilityOptionsForm(
revetInlet.fields.onChange(emit);
revetOutlet.fields.onChange(emit);
revetSide.addEventListener("change", emit);
[boxSize, boxWidth, boxHeight, ...wingInFields.inputs, ...wingOutFields.inputs].forEach((input) =>
[...wingInFields.inputs, ...wingOutFields.inputs].forEach((input) =>
input.addEventListener("change", emit),
);
@@ -497,11 +466,13 @@ export function createFacilityOptionsForm(
bindLinks(links) {
linkSource = links;
pipeForm.paintLinks();
boxForm.paintLinks();
fordForm.paintLinks();
paveForm.paintLinks();
},
paintLinks: () => {
pipeForm.paintLinks();
boxForm.paintLinks();
fordForm.paintLinks();
paveForm.paintLinks();
},
@@ -527,30 +498,17 @@ export function createFacilityOptionsForm(
options[key] !== undefined ? String(options[key]) : "";
// 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7).
const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name]));
if (facility === "pipe")
pipeForm.write(PIPE_SPEC, options, definition?.spec_names ?? {}, groups);
const names = definition?.spec_names ?? {};
if (facility === "pipe") pipeForm.write(PIPE_SPEC, options, names, groups);
if (facility === "box_culvert") {
// 본체 규격은 저장 안 함 — 폭 · 높이로 되찾음 · 옛 설치 「있음」 = 날개벽
boxForm.write(BOX_SPEC, boxOptionsIn(options), names, groups);
for (const [id, value] of Object.entries(bodyOf(options)))
boxForm.setValue("body", id, value);
}
if (facility === "ford_bridge")
fordForm.write(FORD_SPEC, withLegacyGuards(options), definition?.spec_names ?? {}, groups);
if (facility === "ford_pavement")
paveForm.write(FORD_PAVEMENT_SPEC, options, definition?.spec_names ?? {}, groups);
// 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다.
// 값이 없으면 「안 정함」.
const savedWidth = Number.parseFloat(text("body_width_m"));
const savedHeight = Number.parseFloat(text("body_height_m"));
const hasSaved =
Number.isFinite(savedWidth) &&
savedWidth > 0 &&
Number.isFinite(savedHeight) &&
savedHeight > 0;
const preset = hasSaved
? BOX_SIZE_PRESETS.find(
([, width, height]) =>
Math.abs(width - savedWidth) < 0.05 && Math.abs(height - savedHeight) < 0.05,
)
: undefined;
boxSize.value = hasSaved ? (preset?.[0] ?? BOX_SIZE_CUSTOM) : "";
boxWidth.value = hasSaved ? String(savedWidth) : "";
boxHeight.value = hasSaved ? String(savedHeight) : "";
fordForm.write(FORD_SPEC, withLegacyGuards(options), names, groups);
if (facility === "ford_pavement") paveForm.write(FORD_PAVEMENT_SPEC, options, names, groups);
wingInFields.write(options);
wingOutFields.write(options);
revetSide.value = text("side");
@@ -570,11 +528,8 @@ export function createFacilityOptionsForm(
if (current === "pipe") {
Object.assign(options, pipeForm.read()); // 명세 컨테이너 값 = 옛 키 그대로(PLAN 52-1)
} else if (current === "box_culvert") {
// 프리셋을 골랐든 사용자 지정을 적었든 정본은 폭·높이 두 칸이다.
putNumber(options, "body_width_m", boxWidth.value);
putNumber(options, "body_height_m", boxHeight.value);
wingInFields.read(options);
wingOutFields.read(options);
// 정본은 폭 · 높이 두 칸 — 규격이면 규격 수 · 사용자 입력이면 명세 숫자 칸(PLAN 52-2)
Object.assign(options, boxForm.read(), bodyOptions(boxForm.state().body?.[0]));
} else if (current === "ford_pavement") {
Object.assign(options, paveForm.read()); // 표 03 명세 값 · 옛 월류 · 경사 · 두께 · 길이는 병합이 이어 붙임(52-3)
} else if (current === "ford_bridge") {
@@ -461,18 +461,6 @@ export function createBasinSplitFields(length: HTMLInputElement): {
};
}
/* ── BOX암거 ────────────────────────────────────────────────────────────── */
/** 본체 규격 프리셋 — 실무 관측 규격이다(2026-08-17 사용자 확정). 그 밖의 규격은
* "사용자 지정"으로 폭·높이를 직접 받는다. 저장 키는 프리셋이든 자유 입력이든
* body_width_m·body_height_m 숫자 그대로다 — 프리셋 전용 키를 만들지 않아야
* 하류(B06 횡단도·수량)가 폭·높이만 읽고 끝난다. */
export const BOX_SIZE_PRESETS: ReadonlyArray<readonly [string, number, number]> = [
["2.0×2.0", 2, 2],
["3.0×3.0", 3, 3],
];
export const BOX_SIZE_CUSTOM = "사용자 지정";
/** 날개벽 옵션 키 묶음 — 유입·유출이 같은 조각을 쓰되 키만 다르다. */
interface WingKeys {
install: string;
@@ -17,12 +17,6 @@
const revetKeys = (side: "inlet" | "outlet"): string[] =>
["form", "length_m", "height_m", "before_m", "after_m"].map((key) => `${side}_revet_${key}`);
const wingKeys = (side: "in" | "out"): string[] => [
`wing_${side}`,
`wing_${side}_height_m`,
`wing_${side}_length_m`,
`wing_${side}_angle_deg`,
];
/** 시설 종류별로 **폼이 적는 칸** — `createFacilityOptionsForm().readOptions()` 가 쓰는 키 전부. */
export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
@@ -40,7 +34,8 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
"outlet_type",
...revetKeys("outlet"),
],
box_culvert: ["body_width_m", "body_height_m", ...wingKeys("in"), ...wingKeys("out")],
// BOX = 표 02 명세 키(PLAN 52-2) — 옛 날개벽 치수는 폼에서 빠져 이어 붙임(지난 정의 값)
box_culvert: ["body_width_m", "body_height_m", "wing_in", "wing_out"],
// 물넘이 포장 = 표 03 명세 키(PLAN 52-3 · `design_structure_ford_pavement`) — 옛 월류 · 경사 · 두께 · 길이는
// 폼에서 빠져 이어 붙임 · 도수로/돌붙임 키는 `FORM_OPTION_PREFIXES`
ford_pavement: ["ford_slope_deg"],