feat(b06): BOX·세월교·물넘이 기본값으로 그린 칸을 횡단 카드 머리에 「기본값(미확정)」 · 월류 높이 없어 파임 안 그리는 까닭(판정 ②③)

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 11:50:31 +09:00
co-authored by Claude Opus 5
parent 290eccbb92
commit ecf3b65b42
7 changed files with 271 additions and 15 deletions
+4
View File
@@ -265,6 +265,8 @@ export interface FordSet {
min_cover_m: number;
wing_in: FordWingSpec;
wing_out: FordWingSpec;
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */
defaulted?: string[];
}
/** BOX암거 측점의 세트 제원 — 상판·내공(유로)·저판 + 날개벽 투영 연장. */
@@ -283,6 +285,8 @@ export interface BoxSet {
span_m: number;
wing_in: FordWingSpec;
wing_out: FordWingSpec;
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */
defaulted?: string[];
}
export interface CrossSection extends SectionStation {
+67 -7
View File
@@ -332,6 +332,36 @@ def _wing_spec(values: dict[str, Any], defaults: dict[str, Any], side: str) -> d
}
def _defaulted(values: dict[str, Any], fields: list[tuple[str, str, Any]]) -> list[str]:
"""정본에 안 적혀 **기본값으로 그린 칸** 「이름 값」 — 횡단도가 「기본값(미확정)」으로 적음.
2026-09-14 브레인 판정 ② — 저장만 안 하는 것으로는 모자람 · 그림이 설계값처럼 보이면 같은 병.
TS 짝 `defaulted`(`common_util_culvert_sets.ts`).
"""
found = []
for key, label, used in fields:
if used is None or values.get(key) not in (None, ""):
continue
found.append(f"{label} {used:g}" if isinstance(used, float) else f"{label} {used}")
return found
def _wing_defaulted(values: dict[str, Any], wing: dict[str, Any], side: str) -> list[str]:
"""날개벽 한쪽의 기본값 칸 — 안 세운 날개벽은 치수를 안 봄."""
name = "유입" if side == "in" else "유출"
prefix = f"wing_{side}"
fields: list[tuple[str, str, Any]] = [
(prefix, f"{name} 날개벽", "있음" if wing["installed"] else "없음")
]
if wing["installed"]:
fields += [
(f"{prefix}_height_m", f"{name} 날개벽 높이", wing["height_m"]),
(f"{prefix}_length_m", f"{name} 날개벽 길이", wing["length_m"]),
(f"{prefix}_angle_deg", f"{name} 날개벽 각도", wing["angle_deg"]),
]
return _defaulted(values, fields)
def _ford_set(options: dict[str, Any] | None) -> dict[str, Any]:
"""세월교 1개소의 세트 제원(관 + 양측 측벽 + 바닥판 + 날개벽 연장).
@@ -347,12 +377,26 @@ def _ford_set(options: dict[str, Any] | None) -> dict[str, Any]:
width = _number(values.get("ford_width_m"), _number(defaults.get("ford_width_m"), None))
count = _number(values.get("pipe_count"), _number(defaults.get("pipe_count"), None))
depth = _number(values.get("ford_height_m"), None)
span = width if width and width > 0 else FORD_DEFAULT_WIDTH_M
pipe_count = max(int(count), 1) if count else 1
wing_in = _wing_spec(values, defaults, "in")
wing_out = _wing_spec(values, defaults, "out")
defaulted = _defaulted(
values,
[
("pipe_kind", "관종", str(kind) if kind else None),
("pipe_diameter_mm", "관경", diameter_mm),
("ford_width_m", "월류 폭", span),
("pipe_count", "배관 수량", pipe_count),
("ford_height_m", "월류 높이", 0.0),
],
)
return {
"type": "ford",
"pipe_kind": str(kind) if kind else None,
"diameter_m": round((diameter_mm or 1000.0) / 1000.0, 3),
"pipe_count": max(int(count), 1) if count else 1,
"span_m": width if width and width > 0 else FORD_DEFAULT_WIDTH_M,
"pipe_count": pipe_count,
"span_m": span,
# 월류 높이 — 구체 위 노면은 이만큼 낮게 앉는다(단면은 월류부 가장 아래를 자른
# 자리다). 계획고를 통째로 내려 측벽·바닥판·절성토 면적이 함께 따라간다
# (2026-08-30 사용자 확정). 값이 없으면 0 = 내리지 않는다.
@@ -360,8 +404,11 @@ def _ford_set(options: dict[str, Any] | None) -> dict[str, Any]:
"slab_thickness_m": FORD_SLAB_THICKNESS_M,
"wall_thickness_m": FORD_WALL_THICKNESS_M,
"min_cover_m": MIN_PIPE_COVER_M,
"wing_in": _wing_spec(values, defaults, "in"),
"wing_out": _wing_spec(values, defaults, "out"),
"wing_in": wing_in,
"wing_out": wing_out,
"defaulted": defaulted
+ _wing_defaulted(values, wing_in, "in")
+ _wing_defaulted(values, wing_out, "out"),
}
@@ -377,12 +424,14 @@ def _ford_pavement_set(options: dict[str, Any] | None) -> dict[str, Any]:
width = _number(values.get("ford_width_m"), _number(defaults.get("ford_width_m"), None))
depth = _number(values.get("ford_height_m"), None)
slope = _number(values.get("ford_slope_pct"), None)
span = width if width and width > 0 else FORD_PAVEMENT_DEFAULT_WIDTH_M
return {
"type": "ford_pavement",
"span_m": width if width and width > 0 else FORD_PAVEMENT_DEFAULT_WIDTH_M,
"span_m": span,
# 노선 중심에서 잰 깊이. 없으면 화면이 파임을 그리지 않는다(수치를 지어내지 않는다).
"depth_m": depth if depth and depth > 0 else None,
"slope_pct": slope,
"defaulted": _defaulted(values, [("ford_width_m", "월류 폭", span)]),
}
@@ -399,6 +448,8 @@ def _box_set(options: dict[str, Any] | None) -> dict[str, Any]:
inner_height = _number(values.get("body_height_m"), _number(defaults.get("body_height_m"), 2.0))
wall = FORD_WALL_THICKNESS_M
slab = FORD_SLAB_THICKNESS_M
wing_in = _wing_spec(values, defaults, "in")
wing_out = _wing_spec(values, defaults, "out")
return {
"type": "box",
"inner_width_m": inner_width or 2.0,
@@ -409,8 +460,17 @@ def _box_set(options: dict[str, Any] | None) -> dict[str, Any]:
"cover_m": BOX_COVER_M,
# 도로 진행 방향 길이 = 내공 폭 + 측벽 두 장. 이 폭만큼 측점에 걸친다.
"span_m": (inner_width or 2.0) + 2 * wall,
"wing_in": _wing_spec(values, defaults, "in"),
"wing_out": _wing_spec(values, defaults, "out"),
"wing_in": wing_in,
"wing_out": wing_out,
"defaulted": _defaulted(
values,
[
("body_width_m", "본체 폭", inner_width or 2.0),
("body_height_m", "본체 높이", inner_height or 2.0),
],
)
+ _wing_defaulted(values, wing_in, "in")
+ _wing_defaulted(values, wing_out, "out"),
}
@@ -49,6 +49,33 @@ function fillSlopeInfo(section: CrossSection): HTMLElement | null {
return info;
}
/**
* 시설을 **기본값으로 그렸거나 못 그린 까닭** `[보이는 글, 툴팁]` (2026-09-14 브레인 판정 ②③).
* 저장만 안 하는 것으로는 모자람 — 그림이 설계값처럼 보이면 기본값이 몰래 확정으로 굳던 병의 잔재.
*/
function facilityNotes(section: CrossSection): Array<[string, string]> {
const notes: Array<[string, string]> = [];
const sets: Array<[string, string[] | undefined]> = [
["BOX암거", section.box?.defaulted],
["세월교", section.ford?.defaulted],
["물넘이포장", section.ford_pavement?.defaulted],
];
for (const [name, items] of sets) {
if (!items?.length) continue;
notes.push([
`${name} 기본값(미확정)`,
`B05 시설 칸에 안 적어 기본값으로 그림 — ${items.join(" · ")} · 적으면 그 값으로 그림`,
]);
}
if (section.ford_pavement && !section.ford_pavement.depth_m) {
notes.push([
"⚠ 월류 높이 없음 — 파임 안 그림",
"물넘이포장 월류 높이를 안 적어 파인 노면을 안 그림(수치를 지어내지 않음) — B05 시설 칸에 월류 높이를 적으면 그림",
]);
}
return notes;
}
/**
* 카드 제목행을 만들어 카드에 붙인다(설계 조작이 있으면 조작 바까지).
*
@@ -86,6 +113,13 @@ export function appendCardHeader(
openSlope.title = L("B06_Cross_SlopeUnclosed_Tip");
meta.append(openSlope);
}
for (const [text, tip] of facilityNotes(section)) {
const note = document.createElement("span");
note.className = "b06-cross-card__warning";
note.textContent = text;
note.title = tip;
meta.append(note);
}
const structureName = section.structure;
if (structureName) {
const structure = document.createElement("span");
@@ -27,6 +27,8 @@ export interface FordPavementSpec {
depth_m: number | null;
/** 유입 → 유출 바닥 경사(%). 비우면 노면 횡단경사를 쓴다. */
slope_pct: number | null;
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */
defaulted?: string[];
}
interface Edge {
@@ -36,6 +36,38 @@ function wingOptions(role: "inlet" | "outlet", patch: WingPatch): Record<string,
return options;
}
/** 옵션 키 → 「기본값(미확정)」 목록의 이름(파이썬 `_defaulted` 이름표와 같은 글). */
const DEFAULTED_LABELS: Record<string, string> = {
pipe_kind: "관종",
pipe_diameter_mm: "관경",
ford_width_m: "월류 폭",
pipe_count: "배관 수량",
body_width_m: "본체 폭",
body_height_m: "본체 높이",
};
for (const [side, name] of [
["in", "유입"],
["out", "유출"],
]) {
DEFAULTED_LABELS[`wing_${side}`] = `${name} 날개벽`;
DEFAULTED_LABELS[`wing_${side}_height_m`] = `${name} 날개벽 높이`;
DEFAULTED_LABELS[`wing_${side}_length_m`] = `${name} 날개벽 길이`;
DEFAULTED_LABELS[`wing_${side}_angle_deg`] = `${name} 날개벽 각도`;
}
/** 조작으로 값이 온 칸은 「기본값(미확정)」 목록에서 뺌 — 캐시를 바로 고치는 길이라 카드 머리가
* 옛 기본값을 계속 말하지 않게(2026-09-14 브레인 판정 ②). 항목 모양은 「이름 값」(값에 빈칸 없음). */
function dropDefaulted(spec: { defaulted?: string[] } | undefined, options: object): void {
if (!spec?.defaulted) return;
const labels = Object.keys(options).flatMap((key) => DEFAULTED_LABELS[key] ?? []);
spec.defaulted = spec.defaulted.filter(
(item) =>
!labels.some(
(label) => item.startsWith(`${label} `) && !item.slice(label.length + 1).includes(" "),
),
);
}
/** 좌측 폼이 낸 옵션에서 그 측 날개벽 조작값을 읽는다(없는 항목은 빼고 돌려준다). */
function wingPatchFrom(patch: Record<string, number | string>, prefix: string): WingPatch {
const num = (key: string): number | undefined => {
@@ -157,6 +189,7 @@ export function createFordControls(deps: FordControlDeps): FordControls {
// 월류 폭 = 구체의 도로 진행 방향 길이(`span_m`) — 백엔드 `_ford_set`과 같은 자리.
if (patch.ford_width_m) spec.span_m = patch.ford_width_m;
}
dropDefaulted(spec, patch);
deps.queuePipeOptions(chainageM, patch);
deps.refreshCard(chainageM);
},
@@ -171,6 +204,7 @@ export function createFordControls(deps: FordControlDeps): FordControls {
if (patch.angle_deg !== undefined) wing.angle_deg = patch.angle_deg;
wing.slab_extend_m = wingSlabExtendM(wing.installed, wing.length_m, wing.angle_deg);
}
dropDefaulted(spec, wingOptions(role, patch));
deps.queuePipeOptions(chainageM, wingOptions(role, patch));
deps.refreshCard(chainageM);
},
@@ -349,6 +383,7 @@ export function createBoxControls(deps: FordControlDeps): {
}
if (patch.body_height_m) spec.inner_height_m = patch.body_height_m;
}
dropDefaulted(spec, patch);
deps.queuePipeOptions(chainageM, patch as Record<string, number | string>);
deps.refreshCard(chainageM);
},
@@ -364,6 +399,7 @@ export function createBoxControls(deps: FordControlDeps): {
? Math.max((wing.length_m ?? 0) * Math.cos(((wing.angle_deg ?? 45) * Math.PI) / 180), 0)
: 0;
}
dropDefaulted(spec, wingOptions(role, patch));
deps.queuePipeOptions(chainageM, wingOptions(role, patch));
deps.refreshCard(chainageM);
},
+67 -8
View File
@@ -247,6 +247,37 @@ function wingSpec(
};
}
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 파이썬 `_defaulted`(2026-09-14 브레인 판정 ②). */
function defaulted(
values: Record<string, unknown>,
fields: ReadonlyArray<[string, string, unknown]>,
): string[] {
return fields
.filter(([key, , used]) => used !== null && used !== undefined && text(values[key]) === null)
.map(([, label, used]) => `${label} ${String(used)}`);
}
/** 날개벽 한쪽의 기본값 칸 — 파이썬 `_wing_defaulted`. */
function wingDefaulted(
values: Record<string, unknown>,
wing: CulvertSetSpec,
side: string,
): string[] {
const name = side === "in" ? "유입" : "유출";
const prefix = `wing_${side}`;
const fields: Array<[string, string, unknown]> = [
[prefix, `${name} 날개벽`, wing.installed ? "있음" : "없음"],
];
if (wing.installed) {
fields.push(
[`${prefix}_height_m`, `${name} 날개벽 높이`, wing.height_m],
[`${prefix}_length_m`, `${name} 날개벽 길이`, wing.length_m],
[`${prefix}_angle_deg`, `${name} 날개벽 각도`, wing.angle_deg],
);
}
return defaulted(values, fields);
}
/** 세월교 1개소의 세트 제원(관 + 양측 측벽 + 바닥판 + 날개벽 연장). */
function fordSet(
values: Record<string, unknown>,
@@ -257,19 +288,35 @@ function fordSet(
const width = num(values.ford_width_m, num(defaults.ford_width_m, null));
const count = num(values.pipe_count, num(defaults.pipe_count, null));
const depth = num(values.ford_height_m, null);
const kind = text(values.pipe_kind) ?? text(defaults.pipe_kind);
const span = width && width > 0 ? width : FORD_DEFAULT_WIDTH_M;
const pipeCount = count ? Math.max(Math.trunc(count), 1) : 1;
const wingIn = wingSpec(values, defaults, "in");
const wingOut = wingSpec(values, defaults, "out");
return {
type: "ford",
pipe_kind: text(values.pipe_kind) ?? text(defaults.pipe_kind),
pipe_kind: kind,
diameter_m: pipeDiameterM(diameterMm),
pipe_count: count ? Math.max(Math.trunc(count), 1) : 1,
span_m: width && width > 0 ? width : FORD_DEFAULT_WIDTH_M,
pipe_count: pipeCount,
span_m: span,
// 월류 높이 — 구체 위 노면은 이만큼 낮게 앉는다. 없으면 0 = 내리지 않는다.
overflow_depth_m: depth && depth > 0 ? depth : 0.0,
slab_thickness_m: FORD_SLAB_THICKNESS_M,
wall_thickness_m: FORD_WALL_THICKNESS_M,
min_cover_m: MIN_PIPE_COVER_M,
wing_in: wingSpec(values, defaults, "in"),
wing_out: wingSpec(values, defaults, "out"),
wing_in: wingIn,
wing_out: wingOut,
defaulted: [
...defaulted(values, [
["pipe_kind", "관종", kind],
["pipe_diameter_mm", "관경", diameterMm],
["ford_width_m", "월류 폭", span],
["pipe_count", "배관 수량", pipeCount],
["ford_height_m", "월류 높이", 0],
]),
...wingDefaulted(values, wingIn, "in"),
...wingDefaulted(values, wingOut, "out"),
],
};
}
@@ -281,12 +328,14 @@ function fordPavementSet(
const defaults = registry.ford_pavement ?? {};
const width = num(values.ford_width_m, num(defaults.ford_width_m, null));
const depth = num(values.ford_height_m, null);
const span = width && width > 0 ? width : FORD_PAVEMENT_DEFAULT_WIDTH_M;
return {
type: "ford_pavement",
span_m: width && width > 0 ? width : FORD_PAVEMENT_DEFAULT_WIDTH_M,
span_m: span,
// 노선 중심에서 잰 깊이. 없으면 화면이 파임을 그리지 않는다(수치를 지어내지 않는다).
depth_m: depth && depth > 0 ? depth : null,
slope_pct: num(values.ford_slope_pct, null),
defaulted: defaulted(values, [["ford_width_m", "월류 폭", span]]),
};
}
@@ -300,6 +349,8 @@ function boxSet(
const innerHeight = num(values.body_height_m, num(defaults.body_height_m, 2.0));
const wall = FORD_WALL_THICKNESS_M;
const slab = FORD_SLAB_THICKNESS_M;
const wingIn = wingSpec(values, defaults, "in");
const wingOut = wingSpec(values, defaults, "out");
return {
type: "box",
inner_width_m: innerWidth || 2.0,
@@ -309,8 +360,16 @@ function boxSet(
top_thickness_m: slab,
cover_m: BOX_COVER_M,
span_m: boxSpanM(innerWidth || 2.0),
wing_in: wingSpec(values, defaults, "in"),
wing_out: wingSpec(values, defaults, "out"),
wing_in: wingIn,
wing_out: wingOut,
defaulted: [
...defaulted(values, [
["body_width_m", "본체 폭", innerWidth || 2.0],
["body_height_m", "본체 높이", innerHeight || 2.0],
]),
...wingDefaulted(values, wingIn, "in"),
...wingDefaulted(values, wingOut, "out"),
],
};
}
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
"""BOX암거·세월교·물넘이포장 — **기본값으로 그린 칸을 그림에 적음** (2026-09-14 브레인 판정 ②③).
폼이 기본값을 안 보내게 고친 뒤, 비운 칸은 횡단도가 등록부 기본값으로 그리고 있었음. 저장만 안 하면
된다고 볼 일이 아님 — 그림이 설계값처럼 보이는 것이 같은 병. 세트 제원이 `defaulted` 「이름 값」을
싣고 카드 머리가 「⚠ … 기본값(미확정)」으로 보임. 물넘이 월류 높이가 비면 파임을 안 그리는 까닭도 보임.
TS 짝은 거울 시험(`test_b06_culvert_sets_mirror`)이 딕셔너리째 대조함.
"""
from __future__ import annotations
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.B06_Section_Engine_Culvert import ( # noqa: E402
_box_set,
_ford_pavement_set,
_ford_set,
)
CHROME = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Card_Chrome.ts").read_text(encoding="utf-8")
CONTROLS = (ROOT / "B06_Section" / "B06_Section_UI_Page_Ford_Controls.ts").read_text(
encoding="utf-8"
)
def test_BOX_빈_칸은_기본값_목록에_선다() -> None:
items = _box_set({})["defaulted"]
assert "본체 폭 2" in items and "본체 높이 2" in items
assert "유입 날개벽 있음" in items and "유출 날개벽 각도 45" in items
def test_적은_칸은_목록에서_빠진다() -> None:
items = _box_set({"body_width_m": 3.0, "wing_in": "없음"})["defaulted"]
assert not any(item.startswith("본체 폭 ") for item in items)
# 안 세운 날개벽은 치수를 안 봄.
assert not any(item.startswith("유입 날개벽") for item in items)
assert "본체 높이 2" in items
def test_세월교_월류_폭_관경도_적힌다() -> None:
items = _ford_set({"ford_width_m": 12.0})["defaulted"]
assert "관경 1000" in items and "월류 높이 0" in items
assert not any(item.startswith("월류 폭 ") for item in items)
def test_물넘이_월류_높이가_비면_파임_없음() -> None:
spec = _ford_pavement_set({})
assert spec["depth_m"] is None
assert spec["defaulted"] == ["월류 폭 5"]
def test_카드_머리가_목록과_까닭을_보인다() -> None:
assert "기본값(미확정)" in CHROME
assert "월류 높이 없음 — 파임 안 그림" in CHROME
# 조작으로 값이 오면 목록에서 뺌 — 캐시를 바로 고치는 길.
assert CONTROLS.count("dropDefaulted(spec,") == 4