feat(b05): 원단위 표가 아직 안 읽는 칸에 「표에 안 쓰임」 — 골막이·바닥막이·큰돌쌓기·흙막이 23칸 · 값을 바꿔 돌려 보는 시험이 표시를 지킴(①)

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 14:01:20 +09:00
co-authored by Claude Opus 5
parent 42d5566dc9
commit d352774ce2
6 changed files with 169 additions and 27 deletions
@@ -38,6 +38,8 @@ export interface StructureOptionField {
/** 이 값을 넘으면 칸이 경고색 + 툴팁(막지 않음) — 기준값과 까닭 한 줄. */
warn_above?: number | null;
warn_message?: string | null;
/** 원단위 표가 아직 안 읽는 칸 — 칸 이름 옆 「표에 안 쓰임」 + 툴팁 사유. */
not_in_table?: string | null;
}
/** 구조물 배치 폼을 어느 화면이 쓰는가 — B05 는 유무·종류·위치만, **B06/B07 은 상세
+46 -23
View File
@@ -1279,7 +1279,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김"
"empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "thickness_bottom_m",
@@ -1289,7 +1290,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김"
"empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "back_len_cm",
@@ -1299,7 +1301,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 돌쌓기 기본값으로 서고 「기본값으로 섰음」이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검"
"empty_means": "비우면 돌쌓기 기본값으로 서고 「기본값으로 섰음」이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "stone_kind",
@@ -1309,7 +1312,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 야면석 계수 열로 서고 그 사실이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검"
"empty_means": "비우면 야면석 계수 열로 서고 그 사실이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "stone_supply",
@@ -1319,7 +1323,8 @@
"default": "채집",
"required": false,
"phase": "detail",
"default_basis": "「채집」 — 별표2 「야면석 등은 가급적 현장에서 채취·사용」 권고. 구조물마다 바꿀 수 있음"
"default_basis": "「채집」 — 별표2 「야면석 등은 가급적 현장에서 채취·사용」 권고. 구조물마다 바꿀 수 있음",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "stone_coeff_basis",
@@ -1329,7 +1334,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀"
"empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "fill_concrete_mpa",
@@ -1339,7 +1345,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 계산 쪽 기준 강도로 돎"
"empty_means": "비우면 계산 쪽 기준 강도로 돎",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "face_slope_ratio",
@@ -1348,7 +1355,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김"
"empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "foundation",
@@ -1367,7 +1375,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
"empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "weep_hole_area_m2",
@@ -1377,7 +1386,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」",
"not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
}
]
},
@@ -1454,7 +1464,8 @@
"choices": ["채집", "구입"],
"default": "채집",
"required": false,
"phase": "detail"
"phase": "detail",
"not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기는 채집석 공제를 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "stone_coeff_basis",
@@ -1464,7 +1475,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀"
"empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀",
"not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기(13-6)는 계수 열을 아직 안 가름 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "fill_concrete_mpa",
@@ -1474,7 +1486,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 계산 쪽 기준 강도로 돎"
"empty_means": "비우면 계산 쪽 기준 강도로 돎",
"not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기(13-6)는 채움 콘크리트를 품에 포함 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "face_slope_ratio",
@@ -1510,7 +1523,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
"empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」",
"not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기는 물빼기 관을 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "weep_hole_area_m2",
@@ -1520,7 +1534,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」",
"not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기는 물빼기 관을 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "blinding_concrete",
@@ -1648,7 +1663,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김"
"empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김",
"not_in_table": "아직 표에 안 쓰임 — 골막이 두께는 정본 식(ℓ3+0.1H · ℓ3+0.4H)으로만 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "thickness_bottom_m",
@@ -1658,7 +1674,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김"
"empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김",
"not_in_table": "아직 표에 안 쓰임 — 골막이 두께는 정본 식(ℓ3+0.1H · ℓ3+0.4H)으로만 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "back_len_cm",
@@ -1676,7 +1693,8 @@
"choices": ["채집", "구입"],
"default": "채집",
"required": false,
"phase": "detail"
"phase": "detail",
"not_in_table": "아직 표에 안 쓰임 — 골막이는 채집석 공제를 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "stone_coeff_basis",
@@ -1686,7 +1704,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀"
"empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀",
"not_in_table": "아직 표에 안 쓰임 — 골막이는 계수 열을 아직 안 가름 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "fill_concrete_mpa",
@@ -1797,7 +1816,8 @@
"choices": ["채집", "구입"],
"default": "채집",
"required": false,
"phase": "detail"
"phase": "detail",
"not_in_table": "아직 표에 안 쓰임 — 바닥막이는 채집석 공제를 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "stone_coeff_basis",
@@ -1807,7 +1827,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀"
"empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀",
"not_in_table": "아직 표에 안 쓰임 — 바닥막이 원단위는 돌붙임 정본 표 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "fill_concrete_mpa",
@@ -1817,7 +1838,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 계산 쪽 기준 강도로 돎"
"empty_means": "비우면 계산 쪽 기준 강도로 돎",
"not_in_table": "아직 표에 안 쓰임 — 바닥막이 원단위는 돌붙임 정본 표 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "face_slope_ratio",
@@ -1826,7 +1848,8 @@
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김"
"empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김",
"not_in_table": "아직 표에 안 쓰임 — 바닥막이는 평면적(돌붙임 ㎡)이라 기울기 몫이 없음 · 넣어도 수량·금액이 안 바뀜(2026-09-14)"
},
{
"key": "foundation",
@@ -64,6 +64,9 @@ class StructureOptionField(BaseModel):
# 넘는지 보는 기준값과 그 까닭 한 줄(원문 쪽·줄 번호).
warn_above: float | None = None
warn_message: str | None = None
# 칸은 있는데 **원단위 표가 아직 안 읽는** 칸 — 넣어도 수량·금액이 안 바뀜을 칸 옆에 보임(2026-09-14 ①).
# 참말인지는 `test_b05_not_in_table_marks` 가 값을 바꿔 돌려 보고 지킴.
not_in_table: str | None = None
class StructureType(BaseModel):
@@ -43,6 +43,8 @@ interface OptionShape {
/** 이 값을 넘으면 칸이 경고색 + 툴팁(막지 않음). */
warn_above?: number | null;
warn_message?: string | null;
/** 원단위 표가 아직 안 읽는 칸 — 툴팁이 이 사유를 먼저 보임. */
not_in_table?: string | null;
}
/**
@@ -57,6 +59,8 @@ export function optionControl(
): HTMLInputElement | HTMLSelectElement {
const value = stored === undefined || stored === null ? "" : String(stored);
const suggested = option.default === null || option.default === "" ? "" : String(option.default);
// 툴팁 — 표가 아직 안 읽는 칸이면 그 사유가 먼저(넣어도 안 바뀜을 알아야 함), 아니면 비울 때의 뜻.
const tip = option.not_in_table ?? option.empty_means ?? "";
if (option.input === "select") {
const blank = suggested
? `— 안 정함 (제안 ${suggested}) —`
@@ -65,7 +69,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;
if (tip) element.title = tip;
return element;
}
const element =
@@ -73,14 +77,14 @@ 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;
if (tip) element.title = tip;
const limit = option.warn_above;
if (typeof limit === "number") {
// 기준을 넘으면 알리기만 — 막지 않음(2026-09-14 브레인 판정 ①·㉰).
const warn = (): void => {
const over = element.value.trim() !== "" && Number(element.value) > limit;
element.classList.toggle("is-outside-standard", over);
element.title = over ? (option.warn_message ?? "") : (option.empty_means ?? "");
element.title = over ? (option.warn_message ?? "") : tip;
};
element.addEventListener("input", warn);
warn();
@@ -322,7 +322,7 @@ export function createStructuresSection(
// 첫 보기를 미리 안 고름(2026-09-14 브레인 판정 ①②, `optionControl` 주석).
const input = optionControl(option, values[option.key]);
suggestions.push({ input, suggested: String(option.default ?? "") });
const label = option.unit ? `${option.label} (${option.unit})` : option.label;
const label = `${option.unit ? `${option.label} (${option.unit})` : option.label}${option.not_in_table ? " · 표에 안 쓰임" : ""}`;
// `enabled:false` 는 칸을 남기고 잠그기만 한다 — 값은 기본값이 그대로 저장된다
// (고를 수 없는 칸이라 제안이 아니라 프로그램 값).
const locked = option.enabled === false;
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
"""「칸은 있는데 표가 안 읽음」 — 등록부 칸 옆에 「아직 표에 안 쓰임」 (2026-09-14 브레인 차례 ①).
사용자가 값을 넣어도 수량·금액이 아무것도 안 바뀌는 칸. 사유를 표에만 두지 말고 칸 옆에도 보임
(브레인 판정 ③ 「B」). 표시가 참말인지 **값을 바꿔 돌려 보고** 지킴 — 표가 그 칸을 읽기 시작하면
여기서 깨지고, 그때 표시를 걷음(표시가 거짓말로 남지 않게).
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402
#: 종류 → (표가 서는 기준 제원, 안 읽는 칸). 2026-09-14 값을 바꿔 전수 대조한 결과.
#: ⚠ 기초(foundation)·높이는 인계 구조물터파기 심도 구간이 읽어 뺌 · 단 수·올림·이동은 횡단도 칸이라 뺌.
MARKED: dict[str, tuple[dict, list[str]]] = {
"erosion_check": (
{"height_m": 2.0, "top_length_m": 6.0, "bottom_length_m": 4.0, "form": ""},
["thickness_top_m", "thickness_bottom_m", "stone_supply", "stone_coeff_basis"],
),
"bed_sill": (
{"area_m2": 10.0, "form": "돌붙임(찰)"},
["stone_supply", "stone_coeff_basis", "fill_concrete_mpa", "face_slope_ratio"],
),
"boulder_masonry": (
{"height_m": 2.5, "length_m": 10.0, "stone_cm": "60~80", "bond": "찰쌓기"},
[
"stone_supply",
"stone_coeff_basis",
"fill_concrete_mpa",
"weep_hole_diameter_mm",
"weep_hole_area_m2",
],
),
"soil_guard": (
{"height_m": 1.0, "length_m": 10.0, "form": ""},
[
"thickness_top_m",
"thickness_bottom_m",
"back_len_cm",
"stone_kind",
"stone_supply",
"stone_coeff_basis",
"fill_concrete_mpa",
"face_slope_ratio",
"weep_hole_diameter_mm",
"weep_hole_area_m2",
],
),
}
FIELDS = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Fields.ts").read_text(encoding="utf-8")
PANEL = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel.ts").read_text(encoding="utf-8")
def _snapshot(type_id: str, options: dict) -> str:
structure = {
"structure_id": "p",
"type_id": type_id,
"start_m": 0.0,
"end_m": 10.0,
"chainage_m": 5.0,
"options": options,
}
table = build_table([structure], {}, {5.0: "both_fill"}, None, 0.2)
# 저장 제원을 그대로 되돌려 싣는 칸(`options`)은 뺌 — 값이 **표에 닿았는지**만 봄.
for row in table["structures"]:
row.pop("options", None)
return json.dumps(table, ensure_ascii=False, sort_keys=True, default=str)
def _probe_values(option) -> list:
if option.choices:
return list(option.choices)
return [0.73, 1.37] if option.input == "number" else ["x"]
@pytest.mark.parametrize("type_id", sorted(MARKED))
def test_등록부에_표시가_있다(type_id: str) -> None:
options = {option.key: option for option in structure_type_map()[type_id].options}
for key in MARKED[type_id][1]:
note = options[key].not_in_table
assert note and "아직 표에 안 쓰임" in note, f"{type_id}.{key}"
@pytest.mark.parametrize("type_id", sorted(MARKED))
def test_표시한_칸은_정말_안_읽힌다(type_id: str) -> None:
base, keys = MARKED[type_id]
options = {option.key: option for option in structure_type_map()[type_id].options}
reference = _snapshot(type_id, dict(base))
assert '"components": []' not in reference, "기준 제원으로 표가 안 섬 — 시험이 무의미"
for key in keys:
for value in _probe_values(options[key]):
assert _snapshot(type_id, {**base, key: value}) == reference, (
f"{type_id}.{key}={value!r} 로 표가 바뀜 — 이제 읽힘, 표시를 걷을 것"
)
def test_폼_칸_이름에_붙는다() -> None:
assert "not_in_table" in FIELDS and "not_in_table" in PANEL