fix(b05,b06,b08): 기슭막이 벽 높이를 관경 기준 한 값으로 · 폼은 기본값을 값으로 안 보냄

- 한 벽을 두고 값이 셋(등록부·표 H2.5 · 구조물도 H2.5 · 횡단도 관경 기준)이던 결함 — 근거 있는 관경 기준으로 맞춤
- 관 벽 높이 안 적으면 관경 + 여유 0.5 를 0.1m 로 올리고 근입 0.5 더함(Ø800 1.8 · Ø1000 2.0 · Ø1500 2.5)
- 파이썬(B06 엔진)·TS(세트 스펙·횡단 상수) 거울 시험 · 등록부 기본 2.5 걷고 empty_means
- 최소값이라 미확정·금액 밖은 그대로
- B05 벽 칸(관 유입·유출 · 독립 기슭막이)은 비워 두고 회색 제안값 · [제안값 넣기] 누른 때만 값 — 저장 한 번에 미확정이 확정으로 바뀌던 길을 막음
- 전체 시험: 받은 덤프 적재 커밋 몫 2개만 빨강(보고함)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-14 09:10:47 +09:00
co-authored by Claude Opus 5
parent a1b1182b9b
commit 8eb1189b4f
10 changed files with 192 additions and 50 deletions
+6 -4
View File
@@ -134,9 +134,10 @@
"label": "유입 기슭막이 높이",
"input": "number",
"unit": "m",
"default": 2.5,
"default": null,
"required": false,
"phase": "detail"
"phase": "detail",
"empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)"
},
{
"key": "outlet_type",
@@ -186,9 +187,10 @@
"label": "유출 기슭막이 높이",
"input": "number",
"unit": "m",
"default": 2.5,
"default": null,
"required": false,
"phase": "detail"
"phase": "detail",
"empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)"
},
{
"key": "revet_foundation",
@@ -59,9 +59,9 @@ import {
WING_IN_KEYS,
WING_OUT_KEYS,
} from "./B05_Profile_UI_Drainage_Facility_Fields";
import { pipeWallMinHeightM } from "../common_util/common_util_culvert_sets";
// 유입구 형식 목록은 700줄 한계로 `_Fields` 로 옮김(2026-09-14) — 부르던 곳은 그대로 여기서.
export type { InletStructureKind };
export type { InletStructureKind }; // 목록은 700줄 한계로 `_Fields` 로 옮김 — 부르던 곳은 그대로
/** 시설 확장 정보 한 건 — 관 지점(chainage)에 얹힌다. 배관(pipe)도 부속 옵션을
* 가지면 저장한다. */
@@ -235,9 +235,11 @@ export function createFacilityOptionsForm(
const basinRows = [
grid(labeled("형식", basinForm), labeled("길이 (m)", stepper(basinLength, 1))),
];
const height = (): string => String(pipeWallMinHeightM(Number(pipeDiameter.value) / 1000));
const inletRevet = createRevetmentFields(INLET_REVET_KEYS, {
...REVET_COMMON_DEFAULTS,
form: "돌쌓기(찰)",
height,
});
// 첫 행 = [구조][형태] — 구조에 따라 집수정 형태와 기슭막이 형태가 갈아 끼워진다
// (2026-08-17 사용자 지시 3).
@@ -260,6 +262,7 @@ export function createFacilityOptionsForm(
const outletRevet = createRevetmentFields(OUTLET_REVET_KEYS, {
...REVET_COMMON_DEFAULTS,
form: "돌쌓기(메)",
height,
});
outletGroup.body.append(
grid(labeled("구조", outletType), outletRevet.formField),
@@ -201,16 +201,20 @@ export interface RevetmentKeys {
after?: string;
}
/** 부위별 기본 구성 — 형태는 유입 돌쌓기(찰)·유출 돌쌓기(메), 길이 10m·높이 2.5m. */
/** 부위별 **제안값** — 형태는 유입 돌쌓기(찰)·유출 돌쌓기(메), 길이 10m. 관 벽 높이는 관경 기준
* (함수로 받아 볼 때마다 셈).
* ⚠ 제안값은 **칸에 채우지 않고 회색 글씨로만** 보임 — 사용자가 적거나 [제안값 넣기]를 누른 때만
* 값이 됨(2026-09-14 브레인 판정 「기본값을 몰래 확정으로 바꾸지 않는다」). 칸에 채워 두면 저장이
* 그 값을 적어 **미확정이 저장 한 번에 확정으로 바뀜** — 사용자가 한 일이 없는데 금액이 섬. */
export interface RevetmentDefaults {
form: string;
length: string;
height: string;
height: string | (() => string);
before: string;
after: string;
}
/** 부위와 무관한 공통 기본 치수 — 길이 10m(전 5·후 5)·높이 2.5m. */
/** 부위와 무관한 공통 제안 치수 — 길이 10m(전 5·후 5)·높이 2.5m(독립 기슭막이 · 관 벽은 관경 기준). */
export const REVET_COMMON_DEFAULTS = {
length: "10",
height: "2.5",
@@ -284,26 +288,30 @@ export function createRevetmentFields(
keys: RevetmentKeys,
defaults: RevetmentDefaults,
): RevetmentFields {
// ⚠ 제안값은 **회색 글씨(placeholder)로만** — 칸은 비워 둠(`RevetmentDefaults` 주석).
const suggestHeight = (): string =>
typeof defaults.height === "function" ? defaults.height() : defaults.height;
const form = document.createElement("select");
form.replaceChildren(...REVET_FORMS.map((value) => new Option(value, value)));
form.value = defaults.form;
const length = keys.length ? numberInput("0.1") : null;
if (length) length.value = defaults.length;
const height = numberInput("0.1");
height.value = defaults.height;
form.replaceChildren(
new Option(`제안 ${defaults.form}`, ""),
...REVET_FORMS.map((value) => new Option(value, value)),
);
const length = keys.length ? numberInput("0.1", "0", `제안 ${defaults.length}`) : null;
const height = numberInput("0.1", "0", `제안 ${suggestHeight()}`);
// 관 벽 높이 제안은 관경을 따라감 — 칸을 볼 때마다 다시 셈.
height.addEventListener("focus", () => (height.placeholder = `제안 ${suggestHeight()}`));
// 기준측점 전/후(2026-08-23 사용자) — 구조물 패널(D군)과 같은 연동:
// 전 + 후 = 길이, 나중에 고친 쪽이 살아남고 반대쪽이 재배분된다.
const before = keys.before && length ? numberInput("0.1") : null;
const after = keys.after && length ? numberInput("0.1") : null;
if (before) before.value = defaults.before;
if (after) after.value = defaults.after;
const before = keys.before && length ? numberInput("0.1", "0", `제안 ${defaults.before}`) : null;
const after = keys.after && length ? numberInput("0.1", "0", `제안 ${defaults.after}`) : null;
let lastSplitEdit: "before" | "after" = "before";
const readNumber = (input: HTMLInputElement | null): number => {
const value = input ? Number.parseFloat(input.value) : Number.NaN;
return Number.isFinite(value) && value >= 0 ? value : 0;
};
const syncSplit = (source: "length" | "before" | "after"): void => {
if (!length || !before || !after) return;
// 길이가 비었으면 전/후를 0 으로 채우지 않음 — 빈 칸이 「0 으로 적음」이 되면 안 됨.
if (!length || !before || !after || length.value === "") return;
if (source !== "length") lastSplitEdit = source;
const total = readNumber(length);
const keep = lastSplitEdit === "after" ? after : before;
@@ -319,10 +327,25 @@ export function createRevetmentFields(
const beforeField = before ? labeled("기준측점 전 (m)", stepper(before, 0.5)) : null;
const afterField = after ? labeled("기준측점 후 (m)", stepper(after, 0.5)) : null;
// [제안값 넣기] — **누른 때만** 빈 칸에 제안값을 넣음(적은 칸은 안 건드림) · 층따기 단추와 같은 모양.
const fill = document.createElement("button");
fill.type = "button";
fill.className = "b05-structure__suggest";
fill.textContent = "제안값 넣기";
fill.addEventListener("click", () => {
if (!form.value) form.value = defaults.form;
if (length && !length.value) length.value = defaults.length;
if (!height.value) height.value = suggestHeight();
if (before && !before.value) before.value = defaults.before;
if (after && !after.value) after.value = defaults.after;
form.dispatchEvent(new Event("change"));
});
// 형태는 그룹 첫 행(구조 옆)에 따로 놓이므로 여기 행에서는 뺀다.
const rows = [
grid(...[lengthField, heightField].filter((item): item is HTMLLabelElement => item !== null)),
...(beforeField && afterField ? [grid(beforeField, afterField)] : []),
grid(fill),
];
return {
@@ -330,13 +353,15 @@ export function createRevetmentFields(
rows,
lengthInput: length,
write(options) {
// 저장된 값만 칸에 — 없으면 비워 두고 제안값은 회색 글씨로(몰래 채우지 않음).
const text = (key: string): string =>
options[key] !== undefined ? String(options[key]) : "";
form.value = text(keys.form) || defaults.form;
if (length && keys.length) length.value = text(keys.length) || defaults.length;
height.value = text(keys.height) || defaults.height;
if (before && keys.before) before.value = text(keys.before) || defaults.before;
if (after && keys.after) after.value = text(keys.after) || defaults.after;
form.value = text(keys.form);
if (length && keys.length) length.value = text(keys.length);
height.value = text(keys.height);
height.placeholder = `제안 ${suggestHeight()}`;
if (before && keys.before) before.value = text(keys.before);
if (after && keys.after) after.value = text(keys.after);
syncSplit("length");
},
read(target) {
+29 -5
View File
@@ -74,6 +74,26 @@ APRON_THICKNESS_M = 0.45
# 1:0.3~0.5의 급한 쪽. 지식DB 근거가 있는 값이라 교차 참조 표시가 붙지 않는다.
REVET_FACE_SLOPE = 0.3
# ── 관 벽 최소 높이 — ⚠ TS 짝: `B06_Section_UI_Cross_Culvert_Const.ts`
# (`REVET_FREEBOARD_M`·`REVET_HEIGHT_STEP_M`·`REVET_EMBED_DEPTH_M`·`pipeWallMinPureHeight`).
# 횡단도가 관 벽을 그리는 그 높이 — 구조물 표·구조물도 그림도 이 값을 씀(2026-09-14 브레인 판정
# 「그림 둘이 같은 벽을 다르게 그리면 결함 · 근거 있는 관경 기준으로 맞춤」). 거울 시험
# `test_b06_pipe_wall_height_mirror`.
REVET_FREEBOARD_M = 0.5 # 관 상단 ~ 벽 상단 여유고(잠정 · 토피 0.5 와 같은 값)
REVET_HEIGHT_STEP_M = 0.1 # 높이 눈금
REVET_EMBED_DEPTH_M = 0.5 # 근입 깊이
def pipe_wall_min_height_m(diameter_m: float) -> float:
"""관 벽 **순수 높이**(근입 포함) 최소값 — 관경 + 여유고를 0.1m 로 올린 뒤 근입을 더함.
⚠ 「최소」다 — 실제 설계값은 더 클 수 있어 이 값으로 선 수량은 미확정으로 둠(금액 밖).
"""
raw = float(diameter_m) + REVET_FREEBOARD_M
steps = math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9)
return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3)
# (2026-08-20 사용자 확정) 배관은 **수평도 가능** — 그림에서 경사를 강제로 만들지
# 않는다. config `DRAINAGE_PIPE_SLOPE_DEG`(10도)는 유효직경 수리계산 전용으로 남는다.
@@ -139,7 +159,9 @@ def _number(value: Any, fallback: float | None) -> float | None:
return fallback
def _side_spec(options: dict[str, Any], defaults: dict[str, Any], side: str) -> dict[str, Any]:
def _side_spec(
options: dict[str, Any], defaults: dict[str, Any], side: str, diameter_m: float = 1.0
) -> dict[str, Any]:
"""유입("inlet")·유출("outlet") 한쪽의 부속 제원.
구조가 집수정이면 기슭막이·보호공을 만들지 않는다 — 집수정 단면은 후속 작업이라
@@ -164,9 +186,10 @@ def _side_spec(options: dict[str, Any], defaults: dict[str, Any], side: str) ->
)
return spec
# 높이를 안 적었으면 **횡단도가 그리는 관경 기준 최소 높이**(2026-09-14 · 옛 기본 2.5 걷음).
height = _number(
options.get(f"{side}_revet_height_m"),
_number(defaults.get(f"{side}_revet_height_m"), None),
_number(defaults.get(f"{side}_revet_height_m"), pipe_wall_min_height_m(diameter_m)),
)
length = _number(
options.get(f"{side}_revet_length_m"),
@@ -208,10 +231,11 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]:
values.get("pipe_diameter_mm"), _number(defaults.get("pipe_diameter_mm"), 1000.0)
)
kind = values.get("pipe_kind") or defaults.get("pipe_kind")
diameter_m = round((diameter_mm or 1000.0) / 1000.0, 3)
return {
"type": "pipe",
"pipe_kind": str(kind) if kind else None,
"diameter_m": round((diameter_mm or 1000.0) / 1000.0, 3),
"diameter_m": diameter_m,
"min_cover_m": MIN_PIPE_COVER_M,
# 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다(2026-09-09). 한쪽만 기초유로
# 하는 일이 드물어 폼을 둘로 늘리지 않았다. 실무가 갈라 쓰면 그때 나눌 것.
@@ -219,8 +243,8 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]:
str(values.get("revet_foundation") or defaults.get("revet_foundation") or "").strip()
or None
),
"inlet": _side_spec(values, defaults, "inlet"),
"outlet": _side_spec(values, defaults, "outlet"),
"inlet": _side_spec(values, defaults, "inlet", diameter_m),
"outlet": _side_spec(values, defaults, "outlet", diameter_m),
}
+31 -8
View File
@@ -208,12 +208,15 @@ REVET_ROLES = (
("inlet", "유입부 기슭막이", "유입 칸 벽"),
("outlet", "유출부 기슭막이", "유출 칸 벽"),
)
#: ⚠ 「횡단도와 같은 값」이 아님 — B06 은 관 벽 높이를 **관경 기준 최소 높이**로 따로 그림
#: (`revetWallSpec`), 독립 기슭막이도 저장 높이가 없으면 근입 깊이로 그림. 형태·길이만 같은 기본값.
#: 관 벽 높이를 안 적었으면 **횡단도가 그리는 관경 기준 최소 높이**(B06 `pipe_wall_min_height_m`,
#: TS 짝 `pipeWallMinPureHeight`)로 섬 — 등록부·구조물도·횡단도가 한 값(2026-09-14 브레인 판정).
#: ⚠ 「최소」라 실제 설계값이 더 클 수 있어 **미확정·금액 밖은 그대로**.
NOTE_DEFAULT_WALL = (
"⚠ 시설 지점에 {filled} 을 안 적어 등록부 기본값으로 섰음 — 미확정이라 금액에 안 들어감"
" · 횡단도 벽 높이는 관경 기준으로 따로 그려져 다를 수 있음"
"⚠ 시설 지점에 {filled} 을 안 적어 기본값으로 섰음 — 미확정이라 금액에 안 들어감"
)
#: 꼬리 — 관 벽은 횡단도와 같은 높이 · 독립 기슭막이는 아직 갈림(관경이 없음).
NOTE_WALL_HEIGHT_PIPE = " · 높이는 횡단도와 같은 관경 기준 최소 높이(실제 설계값은 더 클 수 있음)"
NOTE_WALL_HEIGHT_OWN = " · ⚠ 횡단도는 저장 높이가 없으면 근입 0.5m 만 그림 — 표 높이와 다름"
#: 금액에서 뺄 까닭(2026-09-14 브레인 판정) — 줄·표·그림은 서되 내역 합에는 안 듦.
UNCONFIRMED_WALL = "벽 치수({filled})가 시설 지점에 없어 기본값으로 섰음 — 적으면 금액이 섬"
NOTE_SIDE_UNKNOWN = (
@@ -234,9 +237,13 @@ def _num_or_zero(value: Any) -> float:
def _revet_values(
options: dict[str, Any], role: str, defaults: dict[str, Any], legacy: bool
options: dict[str, Any],
role: str,
defaults: dict[str, Any],
legacy: bool,
min_height_m: float | None = None,
) -> tuple[dict[str, Any], list[str]]:
"""벽 한쪽 제원과 등록부 기본값으로 채운 칸 — B06 세트 스펙(`_side_spec`)과 같은 채움."""
"""벽 한쪽 제원과 기본값으로 채운 칸 — 등록부 기본값 · 관 벽 높이는 관경 기준 최소 높이."""
values: dict[str, Any] = {}
filled: list[str] = []
for name, label in REVET_FIELDS:
@@ -245,12 +252,23 @@ def _revet_values(
value = options.get(name)
if _blank(value):
value = defaults.get(name if legacy else f"{role}_revet_{name}")
if value is None and name == "height_m" and min_height_m is not None:
value = min_height_m
label = f"{label}(관경 기준)"
if label and value is not None:
filled.append(f"{label} {value}")
values[name] = value
return values, filled
def _pipe_diameter_m(options: dict[str, Any], defaults: dict[str, Any]) -> float:
"""관경(m) — 저장값, 없으면 등록부 기본(B06 세트 스펙과 같은 채움)."""
raw = options.get("pipe_diameter_mm")
if _blank(raw):
raw = defaults.get("pipe_diameter_mm")
return _num_or_zero(raw) / 1000.0 or 1.0
def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""계곡 통과 시설(`pipe_points.json`) → 원단위 전개가 읽는 구조물 줄 (A1, 2026-09-14).
@@ -260,6 +278,7 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]:
⚠ 집수정·기슭막이 터파기·되메우기는 전개 성분(`destination: earthwork`)이라 토공집계로만 감.
"""
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B06_Section.B06_Section_Engine_Culvert import pipe_wall_min_height_m
from B08_Quantity.B08_Quantity_Engine_StructureSummary import pipe_row_id
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import attachments_of, child_row
@@ -304,11 +323,15 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]:
)
rows.extend(children)
walls = []
min_height = (
None if legacy else pipe_wall_min_height_m(_pipe_diameter_m(options, wall_defaults))
)
for role, pipe_label, own_label in roles:
values, filled = _revet_values(options, role, wall_defaults, legacy)
values, filled = _revet_values(options, role, wall_defaults, legacy, min_height)
notes = list(inlet_notes) if role == "inlet" else []
if filled:
notes.append(NOTE_DEFAULT_WALL.format(filled=" · ".join(filled)))
tail = NOTE_WALL_HEIGHT_OWN if legacy else NOTE_WALL_HEIGHT_PIPE
notes.append(NOTE_DEFAULT_WALL.format(filled=" · ".join(filled)) + tail)
walls.append([role, own_label if legacy else pipe_label, values, notes, False, filled])
side = str(options.get("side") or "") if legacy else ""
if side in ("", ""):
+18 -4
View File
@@ -46,6 +46,18 @@ export const FORD_WALL_THICKNESS_M = 0.2;
export const FORD_DEFAULT_WIDTH_M = 10.0;
export const FORD_PAVEMENT_DEFAULT_WIDTH_M = 5.0;
export const BOX_COVER_M = 0.5;
/** 관 벽 최소 높이 — 횡단도 `B06_Section_UI_Cross_Culvert_Const.ts` 와 같은 값(거울 시험이 셋을 대조).
* 높이를 안 적은 관 벽은 이 값으로 섬(2026-09-14 브레인 판정 · 옛 등록부 기본 2.5 걷음). */
export const REVET_FREEBOARD_M = 0.5;
export const REVET_HEIGHT_STEP_M = 0.1;
export const REVET_EMBED_DEPTH_M = 0.5;
/** 관 벽 순수 높이(근입 포함) 최소값 — 파이썬 `pipe_wall_min_height_m`. */
export function pipeWallMinHeightM(diameterM: number): number {
const raw = diameterM + REVET_FREEBOARD_M;
const steps = Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9);
return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3);
}
/** 시설 종류 — 파이썬 `common_util_drainage_pipes` 의 상수와 같은 문자열. */
const FACILITY_PIPE = "pipe";
@@ -82,6 +94,7 @@ function sideSpec(
options: Record<string, unknown>,
defaults: Record<string, unknown>,
side: string,
diameterM: number,
): CulvertSetSpec {
const structure = text(options[`${side}_type`]) ?? text(defaults[`${side}_type`]) ?? "기슭막이";
const spec: CulvertSetSpec = { role: side, structure };
@@ -100,7 +113,7 @@ function sideSpec(
const height = num(
options[`${side}_revet_height_m`],
num(defaults[`${side}_revet_height_m`], null),
num(defaults[`${side}_revet_height_m`], pipeWallMinHeightM(diameterM)),
);
const length = num(
options[`${side}_revet_length_m`],
@@ -133,17 +146,18 @@ function culvertSet(
): CulvertSetSpec {
const defaults = registry.pipe ?? {};
const diameterMm = num(options.pipe_diameter_mm, num(defaults.pipe_diameter_mm, 1000.0));
const diameterM = pipeDiameterM(diameterMm);
return {
type: "pipe",
pipe_kind: text(options.pipe_kind) ?? text(defaults.pipe_kind),
diameter_m: pipeDiameterM(diameterMm),
diameter_m: diameterM,
min_cover_m: MIN_PIPE_COVER_M,
// 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다. 한 공사에서 한쪽만 기초유로
// 하는 일은 드물어 폼을 둘로 늘리지 않았다(2026-09-09). 실무가 갈라 쓰는 것이
// 확인되면 그때 나눌 것.
foundation: text(options.revet_foundation) ?? text(defaults.revet_foundation),
inlet: sideSpec(options, defaults, "inlet"),
outlet: sideSpec(options, defaults, "outlet"),
inlet: sideSpec(options, defaults, "inlet", diameterM),
outlet: sideSpec(options, defaults, "outlet", diameterM),
};
}
@@ -451,10 +451,12 @@ def test_pipe_accessory_options_defined():
"outlet_pitching_area_m2",
):
assert gone not in options, f"pipe.{gone}는 보호공으로 합쳐졌다"
# 기본 치수 — 길이 10m·높이 2.5m.
# 기본 치수 — 길이 10m. 높이는 **기본값 없음**(2026-09-14 브레인 판정): 비우면 관경 기준 최소
# 높이로 횡단도·구조물도·표가 같은 값을 씀(옛 2.5 는 횡단도가 안 쓰던 근거 없는 값이라 걷음).
for side in ("inlet", "outlet"):
assert options[f"{side}_revet_length_m"].default == 10
assert options[f"{side}_revet_height_m"].default == 2.5
assert options[f"{side}_revet_height_m"].default is None
assert "관경 기준" in options[f"{side}_revet_height_m"].empty_means
# 유무·길이·관경·유입 구조 = b05, 형태·치수 제원 = detail.
for key in ("inlet_type", "inlet_revet_length_m", "outlet_revet_length_m"):
assert options[key].phase == "b05", f"pipe.{key}는 b05 phase"
+9 -5
View File
@@ -34,7 +34,11 @@ def test_cross_reference_constants_match_plan():
def test_empty_options_fall_back_to_registry_defaults():
"""옵션이 빈 저장분은 레지스트리 기본값(Ø1000·기슭막이 H2.5/L10)으로 채운다."""
"""옵션이 빈 저장분은 레지스트리 기본값(Ø1000·L10)으로 채운다.
높이는 2026-09-14 브레인 판정으로 **관경 기준 최소 높이**(Ø1000 → 1.5 + 근입 0.5 = 2.0) —
횡단도가 그리는 그 높이. 옛 기본 2.5 는 횡단도가 안 쓰던 값이라 걷음.
"""
spec = _culvert_set(None)
assert spec["type"] == "pipe"
assert spec["diameter_m"] == 1.0
@@ -42,11 +46,11 @@ def test_empty_options_fall_back_to_registry_defaults():
for side in ("inlet", "outlet"):
part = spec[side]
assert part["structure"] == "기슭막이"
assert part["revet_height_m"] == 2.5
assert part["revet_height_m"] == 2.0
assert part["revet_length_m"] == 10
assert part["face_slope"] == REVET_FACE_SLOPE
# 보호공 = 낙차고(2.5) × 2 = 5.0m, 두께 0.45m(사용자 확정).
assert part["apron_length_m"] == 5.0
# 보호공 = 낙차고(2.0) × 2 = 4.0m, 두께 0.45m(사용자 확정).
assert part["apron_length_m"] == 4.0
assert part["apron_thickness_m"] == 0.45
# 레지스트리 기본 형태: 유입 찰 / 유출 메.
assert spec["inlet"]["revet_form"] == "돌쌓기(찰)"
@@ -61,7 +65,7 @@ def test_basin_inlet_skips_revet_and_apron():
assert "apron_length_m" not in spec["inlet"]
# 유출측은 그대로 세트를 갖춘다.
assert spec["outlet"]["structure"] == "기슭막이"
assert spec["outlet"]["apron_length_m"] == 5.0
assert spec["outlet"]["apron_length_m"] == 4.0 # 관경 기준 높이 2.0 × 2
def test_string_diameter_and_custom_height():
@@ -0,0 +1,43 @@
"""관 벽 최소 높이 거울 — 파이썬(`B06_Section_Engine_Culvert`) ↔ TS(`_Cross_Culvert_Const.ts`).
⚠ 횡단도(TS)가 그리는 높이와 구조물 표·구조물도 그림(파이썬)이 같은 벽을 다르게 그리면 결함
(2026-09-14 브레인 판정). 상수 셋이 같고 식이 같으면 같은 값.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B06_Section import B06_Section_Engine_Culvert as culvert # noqa: E402
TS = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Const.ts").read_text(encoding="utf-8")
#: 세트 스펙 TS 짝(`common_util_culvert_sets.ts`)도 같은 상수를 들고 있음 — 셋을 함께 대조.
SETS = (ROOT / "common_util" / "common_util_culvert_sets.ts").read_text(encoding="utf-8")
def _ts_const(source: str, name: str) -> float:
return float(re.search(rf"export const {name} = ([\d.]+);", source).group(1))
def test_상수_셋이_TS_와_같다() -> None:
for name in ("REVET_FREEBOARD_M", "REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M"):
assert getattr(culvert, name) == _ts_const(TS, name) == _ts_const(SETS, name), name
def test_TS_식_모양이_그대로다() -> None:
"""식을 바꾸면 여기서 잡힘 — 파이썬 식도 함께 고칠 것."""
assert "const raw = diameterM + REVET_FREEBOARD_M;" in TS
assert "Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) * REVET_HEIGHT_STEP_M" in TS
assert "return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;" in TS
def test_관경별_최소_높이() -> None:
assert culvert.pipe_wall_min_height_m(1.0) == 2.0
assert culvert.pipe_wall_min_height_m(0.8) == 1.8
assert culvert.pipe_wall_min_height_m(1.2) == 2.2
@@ -34,7 +34,9 @@ def test_관_하나에_기슭막이_둘이_기본값_사유와_함께_서고_관
inlet, outlet = table["structures"]
assert inlet["options"]["form"] == "돌쌓기(찰)" and outlet["options"]["form"] == "돌쌓기(메)"
assert all(s["components"] and s["length_m"] == 10 for s in table["structures"])
assert "등록부 기본값으로 섰음" in inlet["notes"][0]
assert "기본값으로 섰음" in inlet["notes"][0]
# 높이는 횡단도와 같은 관경 기준 최소 높이(Ø1000 → 2.0) — 그림 둘·표가 한 값(브레인 판정)
assert inlet["height_m"] == outlet["height_m"] == 2.0 and "관경 기준" in inlet["notes"][0]
# 관 줄은 관 연장만 — 기슭막이가 거기 섞이지 않음(두 번 안 셈).
pipe = build_rows([point], [{"chainage_m": 100.0, "design": {"pipe_length_m": 8}}])
assert [row["quantity"] for row in pipe["rows"]] == [8.0]