From 755e37ba551cb7b243eb0fe8ece2e6986ca6e2cc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 04:31:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=ED=99=94=EB=A9=B4=20=EC=82=AC?= =?UTF-8?q?=EC=9C=A0=20=EB=AC=B8=EA=B5=AC=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EB=A7=90=EB=A1=9C=20=E2=80=94=20=ED=82=A4=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=95=88=20=EC=83=88=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ㉑. `back_len_cm` 같은 개발자 키가 그대로 화면에 뜨던 자리를 모두 바꿈 (`B08_Quantity_Wording.py`). 반영률 라벨이 서버 키로 뜨던 그 자리와 같은 병. - 「없다」만 말하지 않고 **어디서 채우면 값이 서는지**를 함께 적음. 「돌 뒷길이(㎝)가 아직 입력되지 않았습니다 — 구조물 상세 입력에서 입력하면 값이 섭니다」 · 「옹벽의 이 규격은 자료에 없습니다 — 자료에 있는 규격: 옹벽 형식 반중력식 · 높이(m) 2.0」 - 규격도 사람 말로 — `form`·`height_m` 이 아니라 「옹벽 형식」·「높이(m)」. - ⚠ 모르는 키는 지어내지 않고 그대로 보임. 잘못된 안내가 없는 안내보다 나쁨. - 키가 새는지 검사를 둠(`test_b08_wording.py`) — 새 문구를 넣다 흘리면 깨짐. 검증 — 문구 9건 통과, 전체 633 passed. 실물 프로젝트(`5601e828`)로 문구 확인. Co-Authored-By: Claude Opus 5 (1M context) --- B08_Quantity/B08_Quantity_Engine_Handoff.py | 31 +++++-- .../B08_Quantity_Engine_ObservedUnit.py | 10 ++- .../B08_Quantity_Engine_UnitQuantity.py | 14 +++- B08_Quantity/B08_Quantity_Wording.py | 80 +++++++++++++++++++ 4 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 B08_Quantity/B08_Quantity_Wording.py diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index c7c00a0e..11273cb0 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -81,9 +81,14 @@ def masonry_class( """ found = table if table is not None else load_masonry_table() spec = (found or {}).get("back_length") or {} - raw = options.get(str(spec.get("option_key") or "back_len_cm")) + from B08_Quantity.B08_Quantity_Wording import option_missing + + option_key = str(spec.get("option_key") or "back_len_cm") + raw = options.get(option_key) if raw is None: - return None, "뒷길이가 저장돼 있지 않아 갈래를 못 고름" + # ⚠ 「없다」만 말하지 않는다 — **어디서 채우면 단가가 붙는지**까지. + # 이름은 부르는 쪽(`unmatched`)이 이미 앞에 붙이므로 여기서는 칸 이름만 말한다. + return None, option_missing(option_key) + " (단가 갈래를 못 고름)" try: value = float(raw) except (TypeError, ValueError): @@ -94,6 +99,9 @@ def masonry_class( return None, f"뒷길이 {value:g}㎝ 를 덮는 갈래가 표에 없음" +from B08_Quantity.B08_Quantity_Wording import type_label as wording_type_label + + def normalize_kind_key(label: str) -> str: """갈래 키 — **내부 공백만** 지운다 (2026-09-07 두 창 확정). @@ -348,7 +356,13 @@ def rebar_complexity( return None, str(row.get("why") or "원문 예시에 없음") if fallback and fallback.get("class"): return str(fallback["class"]), f"품셈 12-3 [주]① 「{fallback.get('matched')}」" - return None, f"품셈 12-3 [주]① 예시에 없는 구조({type_id} {form}) — 임의로 고르지 않음" + from B08_Quantity.B08_Quantity_Wording import type_label + + detail = f"({form})" if form else "(형식이 아직 입력되지 않음)" + return None, ( + f"{type_label(type_id)} {detail} 는 품셈 12-3 [주]① 예시에 없어 " + "철근 갈래를 정하지 못했습니다 — 임의로 고르지 않습니다" + ) def euroform_type(type_id: str, table: dict[str, Any] | None = None) -> tuple[str | None, str]: @@ -366,7 +380,12 @@ def euroform_type(type_id: str, table: dict[str, Any] | None = None) -> tuple[st for row in (found or {}).get("type_map") or []: if row.get("type_id") == type_id and row.get("class"): return str(row["class"]), f"품셈 12-38-3 [주]④ 「{row.get('matched')}」" - return None, f"품셈 12-38-3 [주]④ 예시에 없는 시설({type_id}) — 임의로 고르지 않음" + from B08_Quantity.B08_Quantity_Wording import type_label + + return None, ( + f"{type_label(type_id)} 는 품셈 12-38-3 [주]④ 예시에 없어 유로폼 유형을 " + "정하지 못했습니다 — 임의로 고르지 않습니다" + ) def _spec_detail(structure: dict[str, Any]) -> str: @@ -519,9 +538,9 @@ def _structure_rows( if composite: parts, parts_missing = composite_quantities(structure, composite, mapping) if code is None and composite is None: - unmatched.append(f"구조물({type_id})") + unmatched.append(f"{wording_type_label(type_id)} — 품셈 공종을 아직 못 이었습니다") elif entry.get("class_from") == "back_length" and class_key is None: - unmatched.append(f"구조물({type_id}) — {class_basis}") + unmatched.append(f"{wording_type_label(type_id)} — {class_basis}") length = float(structure.get("length_m") or 0.0) rows.append( { diff --git a/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py b/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py index ad2d25f6..b2caa79d 100644 --- a/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py +++ b/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py @@ -138,15 +138,19 @@ def expand_observed( table: ObservedUnitTable | None = None, ) -> tuple[list[dict[str, Any]], list[str]]: """(성분 목록, 알림). 규격이 표에 없으면 **빈 목록 + 미확보 알림**을 낸다.""" + from B08_Quantity.B08_Quantity_Wording import spec_missing + found = (table or load_observed_table()).find(type_id, spec) if found is None: known = (table or load_observed_table()).specs_for(type_id) - detail = f" — 표에 있는 규격: {known}" if known else "" - return [], [f"{NOTE_UNIT_MISSING} ({type_id} {spec}){detail}"] + # ⚠ 키 이름을 화면에 내보내지 않는다 — 사용자는 `retaining_wall` 을 모른다. + return [], [spec_missing(type_id, known)] scale, scale_note = scale_for(found, structure) if scale <= 0: - return [], [f"{NOTE_UNIT_MISSING} — 곱할 연장·면적이 0 ({type_id})"] + from B08_Quantity.B08_Quantity_Wording import type_label + + return [], [f"{type_label(type_id)}의 연장·면적이 0 이라 물량을 내지 않았습니다"] source_key = str(found.get("source") or "") components: list[dict[str, Any]] = [] diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 2e5f5972..13117703 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -214,8 +214,8 @@ def boulder_masonry( diameter = _boulder_diameter(options) if diameter is None: return [], [ - "직경 갈래(`stone_cm`)가 저장돼 있지 않거나 표에 없는 값 — " - f"품셈 13-6 갈래는 {list(BOULDER_DIAMETERS)}" + "큰돌쌓기 돌 직경이 아직 입력되지 않았습니다 — 구조물 상세 입력에서 " + f"{' · '.join(BOULDER_DIAMETERS)}㎝ 중 하나를 고르면 값이 섭니다" ] slope_ratio = _num(options.get("face_slope_ratio"), 0.3) # 레지스트리에 칸 없음 — 기본 0.3 @@ -475,7 +475,9 @@ def _observed_components( options = structure.get("options") or {} spec = {key: options[key] for key in keys if options.get(key) is not None} if not spec: - return [], [f"{type_id} 규격이 비어 있음 — 관측 원단위를 고를 수 없음"] + from B08_Quantity.B08_Quantity_Wording import option_missing + + return [], [option_missing(keys[0], type_id)] return expand_observed(type_id, spec, structure, observed) @@ -519,7 +521,11 @@ def expand( result.components = [Component(**item) for item in components] result.notes.extend(notes) return result - result.notes.append(f"'{type_id}' 전개식이 아직 없음 — 물량을 내지 않음") + from B08_Quantity.B08_Quantity_Wording import type_label + + result.notes.append( + f"{type_label(type_id, names)}의 수량 산출식이 아직 없습니다 — 물량이 서지 않습니다" + ) return result result.components, notes = expander(height, length, options) result.notes.extend(notes) diff --git a/B08_Quantity/B08_Quantity_Wording.py b/B08_Quantity/B08_Quantity_Wording.py new file mode 100644 index 00000000..b535dd4e --- /dev/null +++ b/B08_Quantity/B08_Quantity_Wording.py @@ -0,0 +1,80 @@ +"""사용자에게 뜨는 문구 — **키 이름을 화면에 내보내지 않는다** (B08 ㉑). + +왜 있나 + 「`back_len_cm` 가 저장돼 있지 않아 갈래를 못 고름」처럼 **개발자 키 이름이 그대로** + 화면에 뜨던 자리가 있었다. 사용자는 그 이름을 모르고, 무엇을 해야 하는지도 알 수 없다. + (반영률 라벨이 서버 키로 뜨던 그 자리와 같은 병이다.) + +⚠ 「없다」만 말하지 않는다 — **어디서 채우면 풀리는지**를 함께 적는다 + 「원단위 미확보」로 끝나면 사용자는 손쓸 데를 모른다. **무엇이 채워지면 값이 서는지**가 + 같이 있어야 그 말이 쓸모 있다. 오늘 「표에 있는 규격을 함께 알린」 그 방식이다. + +⚠ 여기서 값을 바꾸지 않는다 + 문구만 다듬는 자리다. 판정·계산은 각 엔진이 하고, 이 모듈은 **그 결과를 사람 말로** 옮긴다. +""" + +from __future__ import annotations + +from typing import Any + +#: 구조물 종류의 사람 이름. 레지스트리 이름이 정본이고 여기는 **화면 문구가 필요할 때만** 쓴다. +#: ⚠ 레지스트리에 없는 이름을 지어내지 않는다 — 못 찾으면 원래 값을 그대로 보인다. +TYPE_LABELS = { + "masonry_wet": "돌쌓기(찰)", + "masonry_dry": "돌쌓기(메)", + "boulder_masonry": "큰돌쌓기", + "retaining_wall": "옹벽", + "soil_guard": "흙막이", + "pipe": "배수관", + "pipe_inlet_basin": "배수관 유입부 집수정", + "ford_pavement": "물넘이포장", + "ford_bridge": "세월교", + "box_culvert": "BOX암거", +} + +#: 저장 제원 칸의 사람 이름 + **어디서 채우는지**. 키 이름을 화면에 내보내지 않기 위한 표. +OPTION_LABELS = { + "back_len_cm": ("돌 뒷길이(㎝)", "구조물 상세 입력"), + "stone_cm": ("돌 직경(㎝)", "구조물 상세 입력"), + "form": ("옹벽 형식", "구조물 상세 입력"), + "height_m": ("높이(m)", "구조물 배치"), + "length_m": ("연장(m)", "구조물 배치"), + "face_slope_ratio": ("전면 기울기", "아직 입력 칸이 없음"), +} + + +def type_label(type_id: str, names: dict[str, str] | None = None) -> str: + """구조물 이름 — 레지스트리 이름이 있으면 그것을 먼저 쓴다.""" + stored = (names or {}).get(type_id) + return stored or TYPE_LABELS.get(type_id, type_id) + + +def option_missing(option_key: str, type_id: str = "", names: dict[str, str] | None = None) -> str: + """「무엇이 없고 어디서 채우면 되는지」 한 줄. + + ⚠ 모르는 키면 **지어내지 않고** 키를 그대로 보인다 — 잘못된 안내가 없는 안내보다 나쁘다. + """ + label, where = OPTION_LABELS.get(option_key, (option_key, "")) + subject = f"{type_label(type_id, names)} " if type_id else "" + tail = f" — {where}에서 입력하면 값이 섭니다" if where else "" + return f"{subject}{label}이(가) 아직 입력되지 않았습니다{tail}" + + +def spec_missing( + type_id: str, known: list[dict[str, Any]], names: dict[str, str] | None = None +) -> str: + """규격이 표에 없을 때 — **표에 있는 규격을 함께** 보인다.""" + name = type_label(type_id, names) + if not known: + return f"{name}의 표준 물량 자료가 아직 없습니다 — 설계 표준도 확보가 필요합니다" + readable = " / ".join(_spec_text(spec) for spec in known) + return f"{name}의 이 규격은 자료에 없습니다 — 자료에 있는 규격: {readable}" + + +def _spec_text(spec: dict[str, Any]) -> str: + """규격 한 벌을 사람 말로. 키 이름 대신 라벨을 쓴다.""" + parts = [] + for key, value in spec.items(): + label = OPTION_LABELS.get(key, (key, ""))[0] + parts.append(f"{label} {value}") + return " · ".join(parts)