tmp/ 가 창끼리 안 건너가는 것이 확정돼(랩탑이 시간 두고 두 번 확인) 시험·예외가 저절로 건너가도록 git 안으로 옮김. 사용자 확정. - 내용은 하나도 안 고침 — 자리만 옮김. tmp/tests 는 남겨 둠. - 같은 이름이 이미 있던 64 개는 랩탑 것을 그대로 두고 건너뜀. - helper_b05_*.js 둘은 랩탑이 .cjs 로 이미 올린 것과 **줄바꿈만 다른 같은 내용**이라 복사본을 도로 뺌(시험이 .cjs 를 부름). - resources/tester/ 에서 전체 1176 통과 · 29 건너뜀 · 실패 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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:
|
||
row = 줄(build_table(), "벌목·지장목제거")
|
||
assert "공종 미확정" in row["reason"]
|
||
assert "수확베기" 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"] == "골막이"
|