From 290eccbb9213f09bc37f1525eda8e494d17b14dc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 11:44:01 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(b05,b08):=20C=EA=B5=B0=20=EB=B2=BD=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4=20=EC=97=86=EC=96=B4=EB=8F=84=20=EB=86=93?= =?UTF-8?q?=EC=9E=84=20=E2=80=94=20=EB=86=92=EC=9D=B4=EA=B0=80=20=EB=B0=91?= =?UTF-8?q?=EC=88=98=EC=9D=B8=20=EB=B2=BD=EC=9D=80=20=EC=A4=84=EB=A7=8C=20?= =?UTF-8?q?=EC=84=9C=EA=B3=A0=20=EB=AF=B8=ED=99=95=EC=A0=95(=ED=8C=90?= =?UTF-8?q?=EC=A0=95=20=E2=91=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B05_Profile/B05_Profile_Structure_Types.json | 12 ++-- .../B05_Profile_UI_Structures_Fields.ts | 4 ++ .../B05_Profile_UI_Structures_Panel_Commit.ts | 9 +-- .../B08_Quantity_Engine_UnitQuantity.py | 3 +- .../B08_Quantity_Engine_UnitQuantity_Base.py | 17 +++++ .../test_b08_wall_height_unconfirmed.py | 64 +++++++++++++++++++ 6 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 resources/tester/test_b08_wall_height_unconfirmed.py diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 9d7772ef..38146213 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -728,7 +728,8 @@ "unit": "m", "default": 2.5, "required": false, - "phase": "b05" + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" }, { "key": "length_m", @@ -814,7 +815,8 @@ "unit": "m", "default": 2.5, "required": false, - "phase": "b05" + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" }, { "key": "length_m", @@ -987,7 +989,8 @@ "unit": "m", "default": 2.5, "required": false, - "phase": "b05" + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" }, { "key": "length_m", @@ -1373,7 +1376,8 @@ "unit": "m", "default": 2.5, "required": false, - "phase": "b05" + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" }, { "key": "length_m", diff --git a/B05_Profile/B05_Profile_UI_Structures_Fields.ts b/B05_Profile/B05_Profile_UI_Structures_Fields.ts index 5e695ecf..cdfcd657 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Fields.ts @@ -38,6 +38,8 @@ interface OptionShape { choices: string[]; default: string | number | null; required?: boolean; + /** 비워 두면 어떻게 되나 — 칸에 마우스를 올리면 보임. */ + empty_means?: string | null; } /** @@ -60,6 +62,7 @@ export function optionControl( : "— 안 정함 —"; const element = select([["", blank], ...option.choices.map((c) => [c, c] as [string, string])]); element.value = value; + if (option.empty_means) element.title = option.empty_means; return element; } const element = @@ -67,6 +70,7 @@ export function optionControl( if (option.input !== "number") element.type = "text"; element.value = value; element.placeholder = suggested ? `제안 ${suggested}` : option.required ? "필수 입력" : ""; + if (option.empty_means) element.title = option.empty_means; return element; } diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts index 7ef4bc87..516ac307 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts @@ -231,13 +231,8 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis ctx.optionInputs.find((entry) => entry.key === "length_m")?.input.focus(); return; } - // 높이도 비면 안 놓음 — 칸이 비면 횡단도 벽이 조용히 안 서고 수량도 안 섬. 적거나 - // [제안값 넣기]를 누를 것(2026-09-14 브레인 판정 「기본값을 몰래 확정으로 안 바꿈」). - const height = ctx.optionInputs.find((entry) => entry.key === "height_m"); - if (height?.isEmpty()) { - height.input.focus(); - return; - } + // 높이는 비워도 놓음 — 자리부터 잡고 치수를 뒤에 넣는 길을 막지 않음. 높이가 물량 밑수인 + // 벽은 B08 에서 줄만 서고 미확정(금액 밖)으로 뜸(2026-09-14 브레인 판정 ①, `missing_height_reason`). const before = ctx.readBeforeM(); if (anchor - before < -0.005) { const beforeInput = ctx.optionInputs.find((entry) => entry.key === "before_m")?.input; diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index a7feeb03..69c2fc15 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -52,6 +52,7 @@ from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import ( # noqa: F401 StructureQuantity, _num, is_unconfirmed, + missing_height_reason, ) from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Masonry import ( # noqa: F401 — 다시 내보냄 boulder_masonry, @@ -529,7 +530,7 @@ def build_table( if rubble is not None: quantity.components.append(rubble) _append_section_trench(quantity, item, observed, rubble_base_thickness_m) - quantity.unconfirmed = str(item.get("unconfirmed") or "") + quantity.unconfirmed = str(item.get("unconfirmed") or "") or missing_height_reason(item) quantities.append(quantity) if use_templates: # 늦게 부름 — 양식 모듈이 이 모듈을 부르므로 맨 위에서 부르면 맞물림. diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py index ae11a11b..8b8aabc5 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py @@ -145,5 +145,22 @@ def is_unconfirmed(structure: dict[str, Any]) -> bool: return bool(structure.get("unconfirmed")) +#: 높이가 물량 밑수인 벽 — 높이를 안 적고 놓아도 줄은 서되 금액 밖(2026-09-14 브레인 판정 ① +#: 「설계자가 자리부터 잡고 치수를 뒤에 넣는 길을 막지 말 것」). 흙막이(떼 개소당)는 높이를 안 씀. +HEIGHT_DRIVEN_TYPES = frozenset({"retaining_wall", "masonry_wet", "masonry_dry", "boulder_masonry"}) +UNCONFIRMED_NO_HEIGHT = "높이를 안 적고 놓았음 — 높이를 적으면 금액이 섬" + + +def missing_height_reason(structure: dict[str, Any]) -> str: + """높이 없이 놓인 벽이면 미확정 까닭 한 줄, 아니면 빈 글.""" + if structure.get("type_id") not in HEIGHT_DRIVEN_TYPES: + return "" + try: + height = float((structure.get("options") or {}).get("height_m") or 0.0) + except (TypeError, ValueError): + height = 0.0 + return "" if height > 0 else UNCONFIRMED_NO_HEIGHT + + def _num(value: Any, fallback: float = 0.0) -> float: return float(value) if isinstance(value, (int, float)) else fallback diff --git a/resources/tester/test_b08_wall_height_unconfirmed.py b/resources/tester/test_b08_wall_height_unconfirmed.py new file mode 100644 index 00000000..0b375b30 --- /dev/null +++ b/resources/tester/test_b08_wall_height_unconfirmed.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +"""높이 없이 놓인 벽 — **놓기는 되고 줄은 서되 미확정(금액 밖)** (2026-09-14 브레인 판정 ①). + +앞서 B05 폼이 C군 높이가 비면 놓기를 막았음. 우리 규칙 「줄은 서고 금액은 안 섬」과 어긋나 풀고, +높이가 물량 밑수인 벽(옹벽·돌쌓기 찰/메·큰돌쌓기)은 B08 표에서 미확정으로 뜨게 함. +흙막이(떼 개소당)는 높이를 안 써 그대로. +""" + +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 B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402 +from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import ( # noqa: E402 + UNCONFIRMED_NO_HEIGHT, + missing_height_reason, +) + +COMMIT = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel_Commit.ts").read_text( + encoding="utf-8" +) + + +def _wall(type_id: str = "masonry_wet", **options) -> dict: + return { + "structure_id": "w", + "type_id": type_id, + "start_m": 0.0, + "end_m": 10.0, + "options": { + "length_m": 10.0, + "back_len_cm": 45, + "stone_kind": "깬돌", + "foundation": "기초유", + **options, + }, + } + + +def test_높이_없는_돌쌓기는_미확정으로_선다() -> None: + table = build_table([_wall()], {"masonry_wet": "돌쌓기(찰)"}, {}) + row = table["structures"][0] + assert row["unconfirmed"] == UNCONFIRMED_NO_HEIGHT + + +def test_높이를_적으면_미확정이_아니다() -> None: + table = build_table([_wall(height_m=2.0)], {"masonry_wet": "돌쌓기(찰)"}, {}) + assert table["structures"][0]["unconfirmed"] == "" + + +def test_높이를_안_쓰는_종류는_그대로() -> None: + assert missing_height_reason(_wall("soil_guard")) == "" + for type_id in ("retaining_wall", "masonry_dry", "boulder_masonry"): + assert missing_height_reason(_wall(type_id)) == UNCONFIRMED_NO_HEIGHT, type_id + assert missing_height_reason(_wall(type_id, height_m="1.5")) == "", type_id + + +def test_폼이_높이_빈칸으로_놓기를_안_막는다() -> None: + assert "height?.isEmpty()" not in COMMIT From ecf3b65b42f8556419ae3403a44955450c9b9d5e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 11:50:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(b06):=20BOX=C2=B7=EC=84=B8=EC=9B=94?= =?UTF-8?q?=EA=B5=90=C2=B7=EB=AC=BC=EB=84=98=EC=9D=B4=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=EC=9C=BC=EB=A1=9C=20=EA=B7=B8=EB=A6=B0=20=EC=B9=B8?= =?UTF-8?q?=EC=9D=84=20=ED=9A=A1=EB=8B=A8=20=EC=B9=B4=EB=93=9C=20=EB=A8=B8?= =?UTF-8?q?=EB=A6=AC=EC=97=90=20=E3=80=8C=EA=B8=B0=EB=B3=B8=EA=B0=92(?= =?UTF-8?q?=EB=AF=B8=ED=99=95=EC=A0=95)=E3=80=8D=20=C2=B7=20=EC=9B=94?= =?UTF-8?q?=EB=A5=98=20=EB=86=92=EC=9D=B4=20=EC=97=86=EC=96=B4=20=ED=8C=8C?= =?UTF-8?q?=EC=9E=84=20=EC=95=88=20=EA=B7=B8=EB=A6=AC=EB=8A=94=20=EA=B9=8C?= =?UTF-8?q?=EB=8B=AD(=ED=8C=90=EC=A0=95=20=E2=91=A1=E2=91=A2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B06_Section/B06_Section_Api_Types.ts | 4 + B06_Section/B06_Section_Engine_Culvert.py | 74 ++++++++++++++++-- .../B06_Section_UI_Cross_Card_Chrome.ts | 34 +++++++++ .../B06_Section_UI_Cross_Ford_Pavement.ts | 2 + .../B06_Section_UI_Page_Ford_Controls.ts | 36 +++++++++ common_util/common_util_culvert_sets.ts | 75 +++++++++++++++++-- .../tester/test_b06_facility_default_notes.py | 61 +++++++++++++++ 7 files changed, 271 insertions(+), 15 deletions(-) create mode 100644 resources/tester/test_b06_facility_default_notes.py diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index c5d1a4a0..0abb514b 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -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 { diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index f1a8bcd4..db99b34d 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -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"), } diff --git a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts index b6e1b8d5..5695a5c9 100644 --- a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts +++ b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts @@ -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"); diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts index 734c63d0..74ce333b 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts @@ -27,6 +27,8 @@ export interface FordPavementSpec { depth_m: number | null; /** 유입 → 유출 바닥 경사(%). 비우면 노면 횡단경사를 쓴다. */ slope_pct: number | null; + /** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */ + defaulted?: string[]; } interface Edge { diff --git a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts index 9b2e21cd..e399baa2 100644 --- a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts @@ -36,6 +36,38 @@ function wingOptions(role: "inlet" | "outlet", patch: WingPatch): Record = { + 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, 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); 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); }, diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index 9144fa4d..a86dbb7f 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -247,6 +247,37 @@ function wingSpec( }; } +/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 파이썬 `_defaulted`(2026-09-14 브레인 판정 ②). */ +function defaulted( + values: Record, + 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, + 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, @@ -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"), + ], }; } diff --git a/resources/tester/test_b06_facility_default_notes.py b/resources/tester/test_b06_facility_default_notes.py new file mode 100644 index 00000000..3b704566 --- /dev/null +++ b/resources/tester/test_b06_facility_default_notes.py @@ -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