fix(b05,b08): C군 벽 높이 없어도 놓임 — 높이가 밑수인 벽은 줄만 서고 미확정(판정 ①)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
# 늦게 부름 — 양식 모듈이 이 모듈을 부르므로 맨 위에서 부르면 맞물림.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user