From 0eebb50c03d6558deea50a0fc59fab5b23e9b2cc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 08:40:49 +0900 Subject: [PATCH] =?UTF-8?q?feat(b05,b08):=20=EB=93=B1=EB=A1=9D=EB=B6=80=20?= =?UTF-8?q?=EC=B9=B8=20=EC=B1=84=EC=9B=80=20A2=C2=B7A3=20=E2=80=94=20?= =?UTF-8?q?=EA=B0=9C=EA=B1=B0=20=EC=97=B0=EC=9E=A5=20=C2=B7=20=EB=AC=BC?= =?UTF-8?q?=EB=84=98=EC=9D=B4=ED=8F=AC=EC=9E=A5=20=EB=91=90=EA=BB=98=C2=B7?= =?UTF-8?q?=EB=85=B8=ED=8F=AD=20=EB=B0=A9=ED=96=A5=20=EA=B8=B8=EC=9D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 표가 읽는데 등록부에 없어 영영 안 서던 칸 — 옛 저장본은 안 깨지고 비면 사유(어디서 적는지) - 개거(점 배치) length_m · 물넘이포장 thickness_cm·length_m(detail · 구조물 집계표에서도 고침) - B05 시설 칸에 물넘이 두께·길이 입력 — 시설 저장이 칸을 통째로 갈아 끼워 집계표로 적은 값이 지워지던 길을 막음 - 700줄 한계로 유입구 형식 목록을 _Fields 로 옮김(부르는 쪽 그대로) - 사유 문구: 「open_ditch 연장」 키 노출 · 「연장·면적이 0」 → 무엇을 적으면 서는지 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B05_Profile/B05_Profile_Structure_Types.json | 29 +++++++++ .../B05_Profile_UI_Drainage_Facility.ts | 42 ++++++------- ...B05_Profile_UI_Drainage_Facility_Fields.ts | 26 ++++++++ .../B08_Quantity_Engine_ObservedUnit.py | 7 ++- ..._Quantity_Engine_UnitQuantity_Revetment.py | 4 +- B08_Quantity/B08_Quantity_Wording.py | 3 + .../tester/test_b05_registry_fill_a2_a3.py | 60 +++++++++++++++++++ 7 files changed, 145 insertions(+), 26 deletions(-) create mode 100644 resources/tester/test_b05_registry_fill_a2_a3.py diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 820ef552..6362f9c3 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -332,6 +332,26 @@ "unit": "%", "default": null, "required": false + }, + { + "key": "thickness_cm", + "label": "포장 두께", + "input": "number", + "unit": "㎝", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 표가 안 서고 「두께를 적어야 섬」 사유가 뜸 — 두께로 관측 원단위(울진 20㎝)를 고름(2026-09-14 A3)" + }, + { + "key": "length_m", + "label": "포장 길이(노폭 방향)", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 면적(월류 폭 × 길이)이 0 이라 표가 안 서고 사유가 뜸 — 노견까지 전폭을 적음(2026-09-14 A3)" } ] }, @@ -479,6 +499,15 @@ "required": false, "phase": "detail", "empty_means": "비우면 실무 붙박이(콘크리트 개거)로 돎" + }, + { + "key": "length_m", + "label": "연장", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "empty_means": "비우면 표가 안 서고 「연장을 적어야 섬」 사유가 뜸 — 개거는 m당 원단위라 연장이 곧 밑수(2026-09-14 A2)" } ] }, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 1caa2b97..409ed2f1 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -45,7 +45,10 @@ import { fordSection, grid, group, + INLET_KINDS, INLET_REVET_KEYS, + inletKindOf, + type InletStructureKind, labeled, numberInput, optionalSelect, @@ -57,29 +60,8 @@ import { WING_OUT_KEYS, } from "./B05_Profile_UI_Drainage_Facility_Fields"; -/** B06 유입측 구조물 형식 — 조정창 드롭다운과 같은 값 집합(2026-08-29 병합). - * 문자열 리터럴로 두어 B05가 B06 모듈에 기대지 않게 한다. */ -export type InletStructureKind = "auto" | "revet" | "I" | "L" | "U"; - -/** 유입구 "구조" 목록 — 조정창의 구조물 형식(자동/기슭막이/집수정 I·ㄴ·ㄷ)을 이 한 - * 드롭다운으로 합쳤다(2026-08-29 사용자 지시 5: 일단 리스트를 합치고 중복은 뒤에 뺀다). - * 고른 값 하나가 정본 둘을 정한다 — 관 옵션 `inlet_type`(기슭막이/집수정)과 B06 - * 유입측 형식(`inlet_structure`). */ -const INLET_KINDS: ReadonlyArray<{ - label: string; - type: "기슭막이" | "집수정"; - structure: InletStructureKind; -}> = [ - { label: "기슭막이", type: "기슭막이", structure: "revet" }, - { label: "집수정", type: "집수정", structure: "auto" }, - { label: "자동(규칙)", type: "기슭막이", structure: "auto" }, - { label: "집수정 I형", type: "집수정", structure: "I" }, - { label: "집수정 ㄴ형", type: "집수정", structure: "L" }, - { label: "집수정 ㄷ형", type: "집수정", structure: "U" }, -]; - -const inletKindOf = (label: string): (typeof INLET_KINDS)[number] => - INLET_KINDS.find((kind) => kind.label === label) ?? INLET_KINDS[0]; +// 유입구 형식 목록은 700줄 한계로 `_Fields` 로 옮김(2026-09-14) — 부르던 곳은 그대로 여기서. +export type { InletStructureKind }; /** 시설 확장 정보 한 건 — 관 지점(chainage)에 얹힌다. 배관(pipe)도 부속 옵션을 * 가지면 저장한다. */ @@ -382,7 +364,15 @@ export function createFacilityOptionsForm( // 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다. const fordSlope = numberInput("0.1"); fordSlope.placeholder = "노면 기울기"; - const fordSlopeRow = grid(labeled("바닥 경사 유입→유출 (%)", stepper(fordSlope, 0.1))); + // 포장 두께·노폭 방향 길이 — 수량(㎡ = 월류 폭 × 길이 · 두께로 원단위 고름)이 읽는 칸(2026-09-14 A3). + // 비우면 표가 안 서고 그 까닭이 뜸 — 지어내지 않는다. + const fordThickness = numberInput("1"); + const fordLength = numberInput("0.1"); + const fordSlopeRow = grid( + labeled("바닥 경사 유입→유출 (%)", stepper(fordSlope, 0.1)), + labeled("포장 두께 (㎝)", stepper(fordThickness, 1, 0)), + labeled("포장 길이 노폭 방향 (m)", stepper(fordLength, 0.1)), + ); const fordSummary = document.createElement("p"); fordSummary.className = "b05-drainage__facility-note"; /** 담당 유역 설계유량(㎥/s) — 개략 단면의 입력. 유역이 없으면 null. */ @@ -634,6 +624,8 @@ export function createFacilityOptionsForm( // 새 시설을 올리는 참이다 — 저장된 높이는 사용자 값으로 보고 자동 추적을 끊는다. fordAutoDepthM = null; fordSlope.value = text("ford_slope_pct"); + fordThickness.value = text("thickness_cm"); + fordLength.value = text("length_m"); revetSide.value = text("side") || "양쪽"; const spread = legacyRevetOptions(options); revetInlet.fields.write(spread); @@ -679,6 +671,8 @@ export function createFacilityOptionsForm( putNumber(options, "ford_width_m", fordWidth.value); putFordHeight(options); putNumber(options, "ford_slope_pct", fordSlope.value); + putNumber(options, "thickness_cm", fordThickness.value); + putNumber(options, "length_m", fordLength.value); } else if (current === "ford_bridge") { options.pipe_kind = pipeMaterial.value; options.pipe_diameter_mm = Number(pipeDiameter.value); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index 0bead6cd..1b3eb9ce 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -10,6 +10,32 @@ import { FORD_MANNING_N, FORD_SLOPE } from "@config/config_frontend"; +/* ── 유입구 구조 ──────────────────────────────────────────────────────────── */ + +/** B06 유입측 구조물 형식 — 조정창 드롭다운과 같은 값 집합(2026-08-29 병합). + * 문자열 리터럴로 두어 B05가 B06 모듈에 기대지 않게 한다. */ +export type InletStructureKind = "auto" | "revet" | "I" | "L" | "U"; + +/** 유입구 "구조" 목록 — 조정창의 구조물 형식(자동/기슭막이/집수정 I·ㄴ·ㄷ)을 이 한 + * 드롭다운으로 합쳤다(2026-08-29 사용자 지시 5: 일단 리스트를 합치고 중복은 뒤에 뺀다). + * 고른 값 하나가 정본 둘을 정한다 — 관 옵션 `inlet_type`(기슭막이/집수정)과 B06 + * 유입측 형식(`inlet_structure`). */ +export const INLET_KINDS: ReadonlyArray<{ + label: string; + type: "기슭막이" | "집수정"; + structure: InletStructureKind; +}> = [ + { label: "기슭막이", type: "기슭막이", structure: "revet" }, + { label: "집수정", type: "집수정", structure: "auto" }, + { label: "자동(규칙)", type: "기슭막이", structure: "auto" }, + { label: "집수정 I형", type: "집수정", structure: "I" }, + { label: "집수정 ㄴ형", type: "집수정", structure: "L" }, + { label: "집수정 ㄷ형", type: "집수정", structure: "U" }, +]; + +export const inletKindOf = (label: string): (typeof INLET_KINDS)[number] => + INLET_KINDS.find((kind) => kind.label === label) ?? INLET_KINDS[0]; + /* ── 물넘이·세월교 개략 단면 ────────────────────────────────────────────── */ /** 월류 단면 한 벌 — 설계유량을 흘리는 데 **필요한** 수심·단면적과 그때의 유속. */ diff --git a/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py b/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py index 878de5d4..b8ba88ed 100644 --- a/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py +++ b/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py @@ -273,7 +273,12 @@ def expand_observed( if scale <= 0: from B08_Quantity.B08_Quantity_Wording import type_label - return [], [f"{type_label(type_id)}의 연장·면적이 0 이라 물량을 내지 않았습니다"] + where = ( + "면적(월류 폭 × 포장 길이)이 0 — 두 칸을 적으면 섬" + if found.get("unit") == "㎡" + else "연장이 0 — 연장을 적으면 섬" + ) + return [], [f"{type_label(type_id)}의 {where}"] source_key = str(found.get("source") or "") options = structure.get("options") or {} diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py index 939c4e05..f138f672 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py @@ -519,7 +519,9 @@ def open_ditch(length_m: float, options: dict[str, Any]) -> tuple[list[Component from B08_Quantity.B08_Quantity_Engine_UnitQuantity import DESTINATION if length_m <= 0: - return [], ["연장이 없어 전개하지 않음"] + from B08_Quantity.B08_Quantity_Wording import option_missing + + return [], [option_missing("length_m", "open_ditch")] spec = str(options.get("ditch_spec") or "콘크리트 개거 150×200").strip() table = OPEN_DITCH_FORMS.get(spec) if table is None: diff --git a/B08_Quantity/B08_Quantity_Wording.py b/B08_Quantity/B08_Quantity_Wording.py index 86f22eca..673ebde7 100644 --- a/B08_Quantity/B08_Quantity_Wording.py +++ b/B08_Quantity/B08_Quantity_Wording.py @@ -28,6 +28,7 @@ TYPE_LABELS = { "pipe": "배수관", "pipe_inlet_basin": "배수관 유입부 집수정", "ford_pavement": "물넘이포장", + "open_ditch": "개거(겉도랑)", "ford_bridge": "세월교", "box_culvert": "BOX암거", # 사방 시설 — 레지스트리 이름 그대로 옮긴 것(2026-09-08 확인). 지어낸 이름 아님. @@ -44,6 +45,8 @@ OPTION_LABELS = { "form": ("옹벽 형식", "구조물 상세 입력"), "height_m": ("높이(m)", "구조물 배치"), "length_m": ("연장(m)", "구조물 배치"), + # 물넘이포장(관 지점 시설) — B05 시설 칸 · 구조물 집계표에서 적음(2026-09-14 A3). + "thickness_cm": ("포장 두께(㎝)", "구조물 배치 물넘이포장 칸 · 구조물 집계표"), # 2026-09-09 칸이 생겼음(`ebdf2988`) — 「아직 칸이 없음」이 거짓이 되어 고침. # ⚠ 비워 두면 품셈 표준경사표로 자동 판정된다(확정 ⑨). "face_slope_ratio": ("전면 기울기", "구조물 상세 입력 — 비우면 품셈 표준경사로 자동"), diff --git a/resources/tester/test_b05_registry_fill_a2_a3.py b/resources/tester/test_b05_registry_fill_a2_a3.py new file mode 100644 index 00000000..4bbd8da1 --- /dev/null +++ b/resources/tester/test_b05_registry_fill_a2_a3.py @@ -0,0 +1,60 @@ +"""등록부 칸 채우기 A2·A3 (2026-09-14) — 표가 읽는 칸이 등록부에 없어 영영 안 서던 자리. + +A2 개거(점 배치) — 연장 칸 · A3 물넘이포장(관 지점 시설) — 두께·노폭 방향 길이 칸. +⚠ 옛 저장본(칸 없음)은 안 깨지고 **사유가 보임** — 「값이 없으면 표가 안 선다」가 아니라 「사유를 보인다」. +""" + +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 B05_Profile.B05_Profile_Structures_Repository import _validate_types # noqa: E402 +from B05_Profile.B05_Profile_Structures_Schema import StructureInstance, structure_type_map # noqa: E402 +from B08_Quantity.B08_Quantity_Engine_Pipe import facility_structures # noqa: E402 +from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402 + + +def _ditch(**options) -> dict: + return build_table( + [{"structure_id": "d", "type_id": "open_ditch", "chainage_m": 10.0, "options": options}] + )["structures"][0] + + +def test_개거_연장_칸이_등록부에_서고_구조물_목록에_저장된다() -> None: + keys = {option.key for option in structure_type_map()["open_ditch"].options} + assert "length_m" in keys + item = StructureInstance( + type_id="open_ditch", placement="point", chainage_m=10.0, options={"length_m": 12} + ) + _validate_types([item]) # 등록부에 없는 칸이면 여기서 거절됨 + + +def test_개거_연장이_있으면_표가_서고_없으면_어디서_적는지_사유() -> None: + assert _ditch(length_m=12)["components"] + old = _ditch() # 옛 저장본 — 칸이 없음 + assert not old["components"] + assert old["notes"][0].startswith("개거(겉도랑) 연장(m)이(가) 아직 입력되지 않았습니다") + + +def _ford(**options) -> dict: + point = {"facility": "ford_pavement", "chainage_m": 30.0, "options": options} + return build_table(facility_structures([point]))["structures"][0] + + +def test_물넘이포장_두께_길이_칸이_있으면_면적으로_선다() -> None: + keys = {option.key: option for option in structure_type_map()["ford_pavement"].options} + assert keys["thickness_cm"].phase == "detail" and keys["length_m"].phase == "detail" + row = _ford(ford_width_m=5, thickness_cm=20, length_m=8) + assert row["components"] and (row["billing_unit"], row["billing_quantity"]) == ("㎡", 40.0) + + +def test_물넘이포장_칸이_비면_무엇을_적을지_사유() -> None: + old = _ford(ford_width_m=5) + assert not old["components"] and "포장 두께(㎝)" in old["notes"][0] + no_length = _ford(ford_width_m=5, thickness_cm=20) + assert not no_length["components"] and "포장 길이" in no_length["notes"][0]