feat(b05,b08): 등록부 칸 채움 A2·A3 — 개거 연장 · 물넘이포장 두께·노폭 방향 길이

- 표가 읽는데 등록부에 없어 영영 안 서던 칸 — 옛 저장본은 안 깨지고 비면 사유(어디서 적는지)
- 개거(점 배치) length_m · 물넘이포장 thickness_cm·length_m(detail · 구조물 집계표에서도 고침)
- B05 시설 칸에 물넘이 두께·길이 입력 — 시설 저장이 칸을 통째로 갈아 끼워 집계표로 적은 값이 지워지던 길을 막음
- 700줄 한계로 유입구 형식 목록을 _Fields 로 옮김(부르는 쪽 그대로)
- 사유 문구: 「open_ditch 연장」 키 노출 · 「연장·면적이 0」 → 무엇을 적으면 서는지

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 08:40:49 +09:00
co-authored by Claude Opus 5
parent e3dbef9700
commit 0eebb50c03
7 changed files with 145 additions and 26 deletions
@@ -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)"
}
]
},
+18 -24
View File
@@ -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);
@@ -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];
/* ── 물넘이·세월교 개략 단면 ────────────────────────────────────────────── */
/** 월류 단면 한 벌 — 설계유량을 흘리는 데 **필요한** 수심·단면적과 그때의 유속. */
@@ -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 {}
@@ -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:
+3
View File
@@ -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": ("전면 기울기", "구조물 상세 입력 — 비우면 품셈 표준경사로 자동"),
@@ -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]