- 뭉친 줄 해석기: 「굴 삭 기 (무한궤도)」를 굴착기(무한궤도)로 맞춰 큰돌쌓기 메쌓기(13-6-1)· 13-7-2 장비 몫이 붙음(형식을 적은 이름만 · 형식 없는 굴삭기는 판정 Ⓒ대로 규격 미정) - 비고·합계 줄을 자원으로 안 읽음(단목베기 4-2-2 가짜 막힘 제거) · 「-」 칸은 그 갈래에 품 없음 (산림복원용 흙막이 13-13-2 가 특별인부·굴삭기 빠진 채 조용히 서던 것을 사유와 함께 막음) - 지장목제거(판정 Ⓑ): 잡관목제거 → FP-04-02-02 #5m미만 · 뿌리뽑기 → FP-09-21 + 임목축적 등급 갈래 · 준비공 「제근·뿌리다듬기」는 같은 면적이라 참조로만(이중계상 막이) · 미결 목록에서 뺌 - 기계 이름 별칭(백호·굴삭기→굴착기)은 별칭표 대상이 아닌 카탈로그 이름 정규화로 주석 정리 - 일위대가 전후 대조(HEAD↔수정): 새로 선 공종 13-6-1·13-7-2 · 새로 막힌 공종 13-13-2(사유 있음) · 내역 금액: 단목베기·제근은 밑수 미확보로 계속 막혀 변화 없음 · 전체 시험 1513 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
233 lines
11 KiB
Python
233 lines
11 KiB
Python
"""준비공·사방공 검사 — PLAN 8-3 의 ❌ 둘 (2026-09-07 ⑪).
|
||
|
||
여기서 하는 일은 **줄을 세우고 못 서는 줄의 사유를 적는 것**이다. 빈 표를 내면
|
||
「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다.
|
||
|
||
⚠⚠ 벌목은 이미 토공집계의 「지장목제거」로 선다 — 여기서 또 세우면 같은 나무를 두 번 벤다.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from B08_Quantity.B08_Quantity_Engine_Preparation import ( # noqa: E402
|
||
EROSION_CONTROL_TYPES,
|
||
STATUS_COUNTED_ELSEWHERE,
|
||
STATUS_NOT_APPLICABLE,
|
||
STATUS_PENDING,
|
||
build_table,
|
||
erosion_rows,
|
||
)
|
||
|
||
|
||
def 줄(table: dict, item: str) -> dict:
|
||
return next(row for row in table["rows"] if row["item"] == item)
|
||
|
||
|
||
# ── 이중계상 방어 ───────────────────────────────────────────────────
|
||
|
||
|
||
def test_벌목은_값을_내지_않고_가리키기만_함() -> None:
|
||
"""⚠ 이 파일의 핵심 — 토공집계의 「지장목제거」와 겹치면 같은 나무를 두 번 벤다."""
|
||
table = build_table({"tree_removal_fill": 1000.0, "tree_removal_cut": 500.0})
|
||
row = 줄(table, "벌목·지장목제거")
|
||
assert row["amount"] is None # 값은 안 낸다
|
||
assert row["status"] == STATUS_COUNTED_ELSEWHERE
|
||
assert "이중계상" in row["reason"]
|
||
# 참고 면적은 보인다 — 어느 값과 겹치는지 사람이 알아야 한다.
|
||
assert row["reference_amount"] == pytest.approx(1500.0)
|
||
|
||
|
||
# ── 못 서는 줄은 사유를 적는다 ─────────────────────────────────────
|
||
|
||
|
||
def test_못_서는_줄이_목록에_남을것() -> None:
|
||
"""빈 표를 내면 「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다."""
|
||
table = build_table()
|
||
items = [row["item"] for row in table["rows"]]
|
||
assert "표토제거" in items and "제근·뿌리다듬기" in items
|
||
# 규준틀은 둘로 갈렸다 — 비탈(11-2)과 수평(11-3)의 설치 기준이 다르다.
|
||
assert "비탈 규준틀" in items and "수평 규준틀" in items
|
||
|
||
|
||
def test_사유가_비어_있지_않을것() -> None:
|
||
for row in build_table()["rows"]:
|
||
assert row["reason"], f"{row['item']} 에 사유가 없다"
|
||
|
||
|
||
def test_값이_없어도_공종코드는_알려줄것() -> None:
|
||
"""무엇으로 서게 될 줄인지 알아야 나중에 잇는다."""
|
||
assert 줄(build_table(), "표토제거")["work_item_code"] == "FP-09-15"
|
||
|
||
|
||
# ── 사방공 ──────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_사방_시설이_없으면_해당_없음() -> None:
|
||
"""있는 것처럼 0 을 적지 않는다."""
|
||
row = 줄(build_table(structures=[{"type_id": "masonry_wet"}]), "사방 시설")
|
||
assert row["status"] == STATUS_NOT_APPLICABLE
|
||
assert row["amount"] is None
|
||
|
||
|
||
def test_사방_시설이_있으면_줄이_섬() -> None:
|
||
table = build_table(structures=[{"type_id": "erosion_check"}, {"type_id": "revetment"}])
|
||
사방 = [row for row in table["rows"] if row["group"] == "사방공"]
|
||
# 키는 `type_id` 칸으로 오가고, 화면에 뜨는 `item` 은 사람 이름이다.
|
||
assert [row["type_id"] for row in 사방] == ["erosion_check", "revetment"]
|
||
assert [row["item"] for row in 사방] == ["골막이", "기슭막이"]
|
||
assert all(row["status"] == STATUS_PENDING for row in table["rows"] if row["group"] == "사방공")
|
||
|
||
|
||
def test_사방_종류_이름이_레지스트리와_맞을것() -> None:
|
||
"""⚠ 이름을 지어내면 영영 안 걸린다 — 레지스트리의 실제 `type_id` 여야 한다."""
|
||
import json
|
||
|
||
registry = json.loads(
|
||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||
)
|
||
types = registry if isinstance(registry, list) else (registry.get("types") or [])
|
||
known = {t["type_id"] for t in types}
|
||
assert EROSION_CONTROL_TYPES <= known, EROSION_CONTROL_TYPES - known
|
||
|
||
|
||
def test_셈이_맞을것() -> None:
|
||
table = build_table()
|
||
assert table["row_count"] == len(table["rows"])
|
||
assert table["pending_count"] == sum(1 for r in table["rows"] if r["status"] == STATUS_PENDING)
|
||
|
||
|
||
# ── 화면에 안 보이던 확정 항목을 드러내기 (2026-09-07) ─────────────
|
||
#
|
||
# ⚠ 목록에만 있고 화면에 없으면 **사용자는 그것이 잠정인 줄도 모른다.**
|
||
# 「확인 필요」만 있어도 안 된다 — **지금 무슨 값으로 돌고 있는지**를 함께 적어야
|
||
# 사용자가 무엇을 정할지 안다(원단위 미확보에서 「표에 있는 규격을 함께 알린」 그 방식).
|
||
|
||
|
||
def test_지장목제거_줄에_공종_자리가_적힐것() -> None:
|
||
"""⭐ 2026-09-13 판정 — 잡관목제거는 단목베기(4-2-2), 뿌리뽑기는 제근(9-21)."""
|
||
row = 줄(build_table(), "벌목·지장목제거")
|
||
assert "FP-04-02-02" in row["reason"] and "FP-09-21" in row["reason"]
|
||
|
||
|
||
# ── 규준틀 개소 — 원문이 기준을 정해 둠 (2026-09-07 ㉒) ────────────
|
||
#
|
||
# 11-2 [주]① 「비탈길이 10m 이상 20m마다 설치한다」
|
||
# 11-3 [주]① 「중심점에서 성토 높이 5m 이상에 설치한다」
|
||
# ⚠ 재료량은 [주]④ 「설계수량에 따른다」 — **개소만 내고 재료는 미확보**.
|
||
|
||
|
||
def 사면줄(*pairs: tuple[float, float]) -> list[dict]:
|
||
"""(그 측점의 사면길이, 앞 측점까지 거리) 목록."""
|
||
return [
|
||
{"lengths": {"face_dressing_fill": length}, "distance_m": dist} for length, dist in pairs
|
||
]
|
||
|
||
|
||
def test_비탈길이_10m_이상_구간만_셀것() -> None:
|
||
"""⚠ 「10m 이상」은 **비탈길이** 조건이고 「20m마다」는 **노선 거리** 간격이다.
|
||
둘을 섞으면 짧은 사면 구간까지 세어 개소가 부푼다."""
|
||
from B08_Quantity.B08_Quantity_Engine_Preparation import batter_frame_count
|
||
|
||
# 사면길이 12m 인 구간 40m + 사면길이 3m 인 구간 100m → 40m 만 센다.
|
||
count, notes = batter_frame_count(사면줄((12.0, 20.0), (12.0, 20.0), (3.0, 100.0)))
|
||
assert count == 3 # 40 ÷ 20 + 1
|
||
assert "11-2" in notes[0]
|
||
|
||
|
||
def test_긴_사면이_없으면_안_설것() -> None:
|
||
from B08_Quantity.B08_Quantity_Engine_Preparation import batter_frame_count
|
||
|
||
count, notes = batter_frame_count(사면줄((4.0, 100.0)))
|
||
assert count == 0
|
||
assert notes
|
||
|
||
|
||
def test_비탈_규준틀이_표에_값으로_설것() -> None:
|
||
# ⚠ 실제 사면표는 **첫 측점의 distance_m 이 0** 이다(앞 측점이 없다). 그 모양으로 넣는다.
|
||
table = build_table(slope_rows=사면줄((12.0, 0.0), (12.0, 20.0)))
|
||
row = 줄(table, "비탈 규준틀")
|
||
assert row["amount"] == 2.0 # 20 ÷ 20 + 1
|
||
assert row["status"] == "값 있음"
|
||
# 재료는 여전히 미확보임을 같은 줄에 적는다.
|
||
assert "설계수량" in row["reason"]
|
||
|
||
|
||
def test_수평_규준틀은_성토고_칸이_없으면_못_셈() -> None:
|
||
"""⚠ 0 으로 때우지 않는다 — 「없음」과 「못 셈」은 다르다."""
|
||
row = 줄(build_table(slope_rows=[{"lengths": {}, "distance_m": 20.0}]), "수평 규준틀")
|
||
assert row["amount"] is None
|
||
assert "성토고" in row["reason"] and "11-3" in row["reason"]
|
||
|
||
|
||
def test_수평_규준틀은_성토고_5m_이상_측점마다() -> None:
|
||
"""⚠ 비탈규준틀(「10m 이상 20m마다」)과 **기준이 다르다** — 이쪽은 **지점 조건**이라
|
||
간격이 없다. 두 기준을 같은 식으로 쓰면 조용히 틀린다."""
|
||
rows = [
|
||
{"fill_height_m": 6.0, "distance_m": 20.0},
|
||
{"fill_height_m": 2.0, "distance_m": 20.0},
|
||
{"fill_height_m": 5.0, "distance_m": 20.0},
|
||
]
|
||
row = 줄(build_table(slope_rows=rows), "수평 규준틀")
|
||
assert row["amount"] == 2.0 # 6.0 · 5.0 두 곳(5.0 은 「이상」이라 든다)
|
||
assert row["status"] == "값 있음"
|
||
|
||
|
||
def test_성토고가_다_낮으면_0개소() -> None:
|
||
"""0 이 **값**인 자리다 — 「못 셈」과 달리 「없다」가 맞는 답이다."""
|
||
row = 줄(build_table(slope_rows=[{"fill_height_m": 1.0, "distance_m": 20.0}]), "수평 규준틀")
|
||
assert row["amount"] == 0.0
|
||
assert row["status"] == "값 있음"
|
||
|
||
|
||
# ── 표토제거 두께 — 품셈이 아니라 설계가 정함 (2026-09-07 ㉓) ───────
|
||
#
|
||
# 9-15 [주]② 「Q=Q1/T … T : 표토두께(m)」 — **공식의 입력 변수**다.
|
||
# ⚠ 안 넣으면 0 으로 때우지 않고 물량을 안 낸다.
|
||
|
||
|
||
def test_두께가_없으면_물량을_안_낼것() -> None:
|
||
row = 줄(build_table({"face_dressing_fill": 1000.0, "face_dressing_cut": 500.0}), "표토제거")
|
||
assert row["amount"] is None
|
||
# ⚠ 사유가 정정됐다(2026-09-09) — 종전에 「품셈이 정하는 값이 아닙니다」라고만 적었는데
|
||
# 품셈 9-15-2(답외구간) 원문에 **T=0.2m · L=20m 가 적용값으로 박혀 있다.** 두께를
|
||
# 자동으로 넣지는 않되 **원문에 있는 값을 없다고 말하지도 않는다.**
|
||
assert "두께는 설계가 정하는 값" in row["reason"]
|
||
assert "T=0.2m" in row["reason"] and "9-15-2" in row["reason"]
|
||
# 대상 면적은 참고로 보인다 — 무엇에 곱할지 사용자가 알아야 한다.
|
||
assert row["reference_amount"] == pytest.approx(1500.0)
|
||
|
||
|
||
def test_두께를_넣으면_물량이_설것() -> None:
|
||
table = build_table(
|
||
{"face_dressing_fill": 1000.0, "face_dressing_cut": 500.0}, topsoil_thickness_m=0.15
|
||
)
|
||
row = 줄(table, "표토제거")
|
||
assert row["amount"] == pytest.approx(225.0) # 1,500㎡ × 0.15m
|
||
assert row["status"] == "값 있음"
|
||
|
||
|
||
def test_두께가_0이면_안_낼것() -> None:
|
||
"""0 은 「안 넣음」과 같게 본다 — 0㎥ 를 내면 「없다」로 오해된다."""
|
||
row = 줄(build_table({"face_dressing_fill": 1000.0}, topsoil_thickness_m=0), "표토제거")
|
||
assert row["amount"] is None
|
||
|
||
|
||
def test_erosion_row_uses_registry_name_not_type_id():
|
||
"""사방공 줄 이름에 개발자 키가 새지 않는다 (2026-09-08 ㉕ 통과에서 `soil_guard` 로 뜬 자리)."""
|
||
rows = erosion_rows([{"type_id": "soil_guard"}], {"soil_guard": "흙막이"})
|
||
assert [row["item"] for row in rows] == ["흙막이"]
|
||
assert rows[0]["type_id"] == "soil_guard"
|
||
|
||
|
||
def test_erosion_row_falls_back_to_wording_table_without_registry():
|
||
"""이름표가 없어도 문구표가 받아 준다 — 없으면 키를 보이되 지어내지 않는다."""
|
||
assert erosion_rows([{"type_id": "soil_guard"}])[0]["item"] == "흙막이"
|
||
assert erosion_rows([{"type_id": "erosion_check"}])[0]["item"] == "골막이"
|