Files
Aislo/B08_Quantity/B08_Quantity_Engine_Preparation.py
T
eomsangdonandClaude Opus 5 5755b1b413 fix(B08): 실사용자 통과에서 드러난 화면 결함 셋 수정
㉕ 통과(엔진 실행)에서 잡힌 것.

- 준비공 사방공 줄에 개발자 키(`soil_guard`)가 그대로 뜨던 자리 수정.
  레지스트리 이름을 받아 쓰고, 키는 `type_id` 칸으로만 오감.
  문구표에 사방 시설 이름 넷 추가(레지스트리 값 그대로 옮긴 것).
- 좌측 「토사」 비율 칸 제거 — 넣어도 버려지던 죽은 칸.
  토사 물량은 토적표 값이 그대로 서고, 비율은 암 총량을 갈래로 나눌 때만 쓰임.
- 안분 안내 문구를 「암 갈래 입력 합 …」으로 명시.
  칸에 100 을 채운 사용자에게 「입력 합 60 %」로 뜨던 자리.
- 항목 라벨을 「암 갈래 구성비(%) — 암 총량 기준」으로 바꿈.

시험: 655 passed · 24 skipped (B05 코리도 1건 기존 깨짐, 이 변경과 무관).
사방공 이름 시험 2건 추가, 키를 화면 이름으로 못 박던 옛 시험 1건 정정.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 05:13:35 +09:00

267 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""준비공·사방공 — **자리를 만들되 없는 값을 지어내지 않는다** (B08 일감 ⑪ · PLAN 8-3).
8-3 대응표에서 ❌ 로 남아 있던 둘이다. 여기서 하는 일은 **줄을 세우고, 설 수 있는 줄은
값을 채우고, 못 서는 줄은 왜 못 서는지 적는 것**이다. 빈 표를 내면 「빠뜨린 것」과
「원래 없는 것」이 구별되지 않는다.
⚠⚠ 지장목제거와 겹치지 않는다 (이중계상)
벌목·지장목제거는 **이미 토공집계의 사면 계열로 서 있다**(`tree_removal_*` × 반영률).
여기서 또 세우면 같은 나무를 두 번 벤다. 그래서 준비공의 벌목 줄은 **값을 내지 않고
「토공집계 지장목제거로 이미 섬」이라고 가리키기만** 한다.
⚠ 값이 없는 줄의 사유를 적는다
· 표토제거(9-15) — 면적은 사면적에서 나오나 **두께·대상 구간이 설계로 안 정해져 있다**.
· 제근·뿌리다듬기(9-20~21) — 단위가 **「개」(그루 수)**인데 입목 본수를 우리가 안 든다.
· 규준틀(11-2) — **개소** 산정 기준(구조물·절성토 구간별 몇 개소)이 안 정해져 있다.
⚠ 사방공은 이 노선에 실물이 없으면 「해당 없음」이다
있는 것처럼 0 을 적지 않는다. 구조물 목록에 사방 시설이 서면 그때 값이 선다.
"""
from __future__ import annotations
from typing import Any, Iterable
from B08_Quantity.B08_Quantity_Wording import type_label
#: 사방 시설로 보는 구조물 종류 — **레지스트리의 실제 `type_id` 를 쓴다**(D 그룹 + 흙막이).
#: 목록에 없으면 그 노선에 사방공이 **없는** 것이다. 이름을 지어내면 영영 안 걸린다.
EROSION_CONTROL_TYPES = frozenset(
{
"erosion_check", # 골막이
"bed_sill", # 바닥막이
"check_dam_small", # 소형사방댐(복합형)
"revetment", # 기슭막이
"soil_guard", # 흙막이
}
)
#: 규준틀 — **품셈 원문이 개소 기준을 정해 둔다**(2026-09-07 ㉒ 에서 찾음).
#: 11-2 [주]① 「비탈길이 **10m 이상** **20m마다** 설치한다」
#: 11-3 [주]① 「중심점에서 **성토 높이 5m 이상**에 설치한다」
#: ⚠ 재료량은 [주]④ 「설계수량에 따른다」 — **개소만 내고 재료는 미확보**로 둔다.
BATTER_MIN_SLOPE_LENGTH_M = 10.0
BATTER_INTERVAL_M = 20.0
LEVEL_MIN_FILL_HEIGHT_M = 5.0
STATUS_COUNTED_ELSEWHERE = "다른 표에서 이미 섬"
STATUS_PENDING = "값을 낼 근거가 없음"
STATUS_NOT_APPLICABLE = "해당 없음"
STATUS_READY = "값 있음"
def batter_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[str]]:
"""비탈 규준틀 개소 — **비탈길이 10m 이상인 구간에서 20m마다**(품셈 11-2 [주]①).
⚠ 「10m 이상」은 **비탈길이**(사면길이) 조건이고 「20m마다」는 **노선 거리** 간격이다.
둘을 섞지 않는다 — 사면이 긴 구간의 **연장**을 20m 로 나눈다.
"""
length_m = 0.0
notes: list[str] = []
for row in slope_rows:
lengths = row.get("lengths") or {}
# 그 측점의 사면길이는 계열마다 있으나 **가장 긴 것**으로 본다(같은 사면이다).
longest = max((float(v) for v in lengths.values()), default=0.0)
if longest >= BATTER_MIN_SLOPE_LENGTH_M:
length_m += float(row.get("distance_m") or 0.0)
if length_m <= 0:
return 0, ["비탈길이 10m 이상인 구간이 없어 비탈 규준틀이 서지 않음"]
count = int(length_m // BATTER_INTERVAL_M) + 1
notes.append(
f"비탈길이 {BATTER_MIN_SLOPE_LENGTH_M:g}m 이상 구간 {length_m:g}m ÷ "
f"{BATTER_INTERVAL_M:g}m + 1 (품셈 11-2 [주]①)"
)
return count, notes
def level_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int | None, list[str]]:
"""수평 규준틀 — **성토고 5m 이상 측점마다** 한 개소(품셈 11-3 [주]①).
⚠ 「비탈길이 10m 이상 20m마다」인 비탈규준틀과 **기준이 다르다** — 이쪽은 **지점 조건**이라
간격이 없다. 두 기준을 같은 식으로 쓰면 조용히 틀린다.
⚠ 성토고 칸이 아예 없으면 **0 으로 때우지 않고** 미확보로 둔다 — 「없음」과 다르다.
"""
rows = list(slope_rows)
if not rows:
return None, ["사면표가 없어 성토고를 못 봄"]
if all("fill_height_m" not in row for row in rows):
return None, ["성토고가 사면표에 없어 개소를 못 셈 (품셈 11-3 [주]① 「성토고 5m 이상」)"]
tall = [
row for row in rows if float(row.get("fill_height_m") or 0.0) >= LEVEL_MIN_FILL_HEIGHT_M
]
return len(tall), [
f"성토고 {LEVEL_MIN_FILL_HEIGHT_M:g}m 이상 측점 {len(tall)}곳 (품셈 11-3 [주]①)"
]
def preparation_rows(
slope_totals: dict[str, float] | None = None,
slope_rows: Iterable[dict[str, Any]] = (),
topsoil_thickness_m: float | None = None,
) -> list[dict[str, Any]]:
"""준비공 줄 — 값이 서는 것과 안 서는 것을 **한 목록에** 낸다."""
slope = slope_totals or {}
tree_area = float(slope.get("tree_removal_fill", 0.0)) + float(
slope.get("tree_removal_cut", 0.0)
)
return [
{
"group": "준비공",
"item": "벌목·지장목제거",
"unit": "㎡",
"amount": None,
"status": STATUS_COUNTED_ELSEWHERE,
# ⚠ 값을 여기서 또 내면 같은 나무를 두 번 벤다. 참고로 면적만 보인다.
"reference_amount": tree_area,
"reason": (
"토공집계의 「지장목제거」로 이미 섬 — 여기서 또 세우면 이중계상. "
"⚠ 다만 **공종 미확정** — 품셈 4장이 벌목을 목적별로 갈라(수확베기·단목베기·"
"위험목 베기) 임도 지장목이 어디에 붙는지 원본이 말하지 않음. 지금은 공종코드 없이 감."
),
"work_item_code": None,
},
_topsoil_row(slope, topsoil_thickness_m),
{
"group": "준비공",
"item": "제근·뿌리다듬기",
"unit": "개",
"amount": None,
"status": STATUS_PENDING,
"reason": "단위가 「개」(그루 수)인데 입목 본수를 들고 있지 않음 (품셈 9-20~21)",
"work_item_code": "FP-09-21",
},
_batter_frame_row(list(slope_rows)),
_level_frame_row(list(slope_rows)),
]
def _topsoil_row(slope: dict[str, float], thickness_m: float | None) -> dict[str, Any]:
"""표토제거 — **두께는 품셈이 아니라 설계가 정한다**(9-15 [주]② 「T : 표토두께(m)」).
⚠ 두께를 안 넣으면 **0 으로 때우지 않고** 물량을 안 낸다. 대상 면적은 절·성토 사면적을
쓴다(사면 계열의 면고르기 면적과 같은 자리).
"""
area = float(slope.get("face_dressing_fill", 0.0)) + float(slope.get("face_dressing_cut", 0.0))
if thickness_m is None or float(thickness_m) <= 0:
return {
"group": "준비공",
"item": "표토제거",
"unit": "㎥",
"amount": None,
"status": STATUS_PENDING,
"reason": (
"표토 두께가 아직 입력되지 않았습니다 — 품셈 9-15 [주]② 가 두께를 "
"「공식의 입력 변수(T)」로 두어 **품셈이 정하는 값이 아닙니다**. "
f"산출 조건에서 두께를 넣으면 값이 섭니다 (대상 면적 {area:,.1f}㎡)"
),
"reference_amount": area,
"work_item_code": "FP-09-15",
}
thickness = float(thickness_m)
return {
"group": "준비공",
"item": "표토제거",
"unit": "㎥",
"amount": area * thickness,
"status": STATUS_READY,
"reason": f"사면적 {area:,.1f}× 두께 {thickness:g}m (품셈 9-15)",
"work_item_code": "FP-09-15",
}
def _batter_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
"""비탈 규준틀 한 줄. **개소는 원문 기준으로 서고 재료는 미확보**다."""
count, notes = batter_frame_count(slope_rows)
return {
"group": "준비공",
"item": "비탈 규준틀",
"unit": "개소",
"amount": float(count) if count else None,
"status": STATUS_READY if count else STATUS_PENDING,
"reason": ("; ".join(notes) + " · 재료량은 품셈 11-2 [주]④ 「설계수량에 따른다」라 미확보"),
"work_item_code": "FP-11-02",
}
def _level_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
"""수평 규준틀 한 줄. 성토고를 못 보면 미확보, 보면 개소가 선다."""
count, notes = level_frame_count(slope_rows)
return {
"group": "준비공",
"item": "수평 규준틀",
"unit": "개소",
"amount": float(count) if count is not None else None,
"status": STATUS_READY if count is not None else STATUS_PENDING,
"reason": "; ".join(notes)
+ (
" · 재료량은 품셈 11-3 [주]④ 「설계수량에 따른다」라 미확보"
if count is not None
else ""
),
"work_item_code": "FP-11-03",
}
def erosion_rows(
structures: Iterable[dict[str, Any]] = (),
names: dict[str, str] | None = None,
) -> list[dict[str, Any]]:
"""사방공 줄 — 이 노선에 사방 시설이 **있을 때만** 값이 선다.
⚠ 줄 이름은 **레지스트리 이름**을 쓴다 — `type_id` 를 그대로 적으면 화면에
`soil_guard` 같은 개발자 키가 뜬다(B08 ㉑ 과 같은 병). `names` 가 없으면
문구표가 받아 주고, 그것도 없으면 키를 보이되 **지어내지는 않는다**.
"""
found = sorted(
{
str(item.get("type_id"))
for item in structures
if str(item.get("type_id")) in EROSION_CONTROL_TYPES
}
)
if not found:
return [
{
"group": "사방공",
"item": "사방 시설",
"unit": "",
"amount": None,
"status": STATUS_NOT_APPLICABLE,
"reason": "이 노선에 사방 시설이 배치돼 있지 않음 — 있는 것처럼 0 을 적지 않음",
"work_item_code": None,
}
]
return [
{
"group": "사방공",
"item": type_label(type_id, names),
"type_id": type_id,
"unit": "개소",
"amount": None,
"status": STATUS_PENDING,
"reason": "구조물 원단위가 아직 없음 — 전개식·관측값 모두 미확보",
"work_item_code": None,
}
for type_id in found
]
def build_table(
slope_totals: dict[str, float] | None = None,
structures: Iterable[dict[str, Any]] = (),
slope_rows: Iterable[dict[str, Any]] = (),
topsoil_thickness_m: float | None = None,
names: dict[str, str] | None = None,
) -> dict[str, Any]:
"""화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**"""
rows = preparation_rows(slope_totals, slope_rows, topsoil_thickness_m) + erosion_rows(
structures, names
)
return {
"columns": ["구분", "공종", "단위", "수량", "상태", "사유"],
"rows": rows,
"ready_count": sum(1 for row in rows if row["status"] == STATUS_READY),
"pending_count": sum(1 for row in rows if row["status"] == STATUS_PENDING),
"row_count": len(rows),
}