17 changed files with 120 additions and 503 deletions
+1 -42
View File
@@ -2477,6 +2477,7 @@
"input": "select",
"choices": [
"씨뿌리기(줄파종)",
"초류종자 살포",
"새심기",
"선떼붙이기",
"줄떼",
@@ -2488,50 +2489,8 @@
"나무심기"
],
"default": null,
"default_basis": "「초류종자 살포」는 여기 없음 — 토공집계에서 사면 전체로 섬(품셈 5-24 · 두 곳에 두면 이중계상 · 2026-09-15 브레인)",
"required": true,
"phase": "detail"
},
{
"key": "face",
"label": "대상 면",
"input": "select",
"choices": ["절토면", "성토면", "양쪽"],
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "cover_kind",
"label": "덮개(비탈덮기)",
"input": "select",
"choices": ["거적", "짚망", "방초·야자섬유매트"],
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비탈덮기일 때만 고름 — 비면 비탈덮기 줄이 안 섬(품셈 5-25 거적덮기 ㎡ · 5-28-1 짚망 · 5-28-2 매트 · 재료가 공종을 가름)"
},
{
"key": "step_height_m",
"label": "단 직고",
"input": "number",
"unit": "m",
"default": null,
"required": false,
"phase": "detail",
"empty_means": "선떼붙이기·조공의 단 사이 직고 — 교본은 범위로만 줌(선떼 직고 1~2m · 조공 1.0~1.2m) · 기본값 없음",
"not_in_table": "아직 표에 안 쓰임 — 길이형(선떼·조공) 산출식이 아직 없음(선떼는 사방기술교본 표 2-2-6 으로 다음 차례)"
},
{
"key": "sod_grade",
"label": "선떼 급수",
"input": "select",
"choices": ["1급", "2급", "3급", "4급", "5급", "6급", "7급", "8급", "9급"],
"default": null,
"required": false,
"phase": "detail",
"empty_means": "선떼붙이기의 급수 — 교본은 1~9급 범위만(6~7급을 많이 씀) · 기본값 없음",
"not_in_table": "아직 표에 안 쓰임 — 선떼붙이기 산출식이 아직 없음(사방기술교본 표 2-2-6 급별 m당 매수로 다음 차례)"
}
]
},
@@ -31,7 +31,6 @@ NOTE_ALL = (
)
NOTE_SOIL = (
"법령은 사질·점토를 가르는데 우리 프리셋은 토사 하나 — 토사 전부를 해당으로 봄(암은 비해당)"
" · 토사 = B06 측점 토사 토글(구성비는 암 몫만 가름)"
)
NOTE_GRADE = "종단 경사 = 종단 계획선에서 측점을 감싼 구간 경사 중 큰 값(B05 포장 제안과 같은 식)"
NOTE_WIDTH = "폭 = 차도 표준 폭 + 확폭(노견 제외 · 실무 소광 「혼합석부설 B=3.0」)"
@@ -41,8 +40,7 @@ NOTE_COMPACTION = (
)
NOTE_EXCAVATION = (
"사전 터파기 — 교본 10-1 「포설 전 10㎝ 터파기」 · 부록 7-2 적용범위 「노면의 토사구간」이라"
" 토사 측점만 · 산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음"
" · 파낸 흙의 운반·사토는 유토곡선 밖이라 안 셈"
" 토사 측점만 · 파낸 흙의 운반·사토는 유토곡선 밖이라 안 셈"
)
@@ -166,28 +166,19 @@ def _side_segments(
berm = design.get("berm") or {}
berm_width = _num(berm.get("width_m")) or 0.0
# 소단 안쪽 기울기(°) 만큼은 평탄부로 봄 — 기울인 소단이 비탈로 읽혀 끊기지 않게.
berm_grade = math.tan(math.radians(_num(berm.get("slope_deg")) or 0.0))
# 설계선 표고는 넷째 자리 반올림 — 기울인 소단 조각이 그만큼 더 가파르게 읽힘(0.0069/0.1953).
berm_slack = 2e-4 if berm_width > 0 else 0.0
segments: list[SlopeSegment] = []
started = False
# ⚠ 설계선이 격자(0.5m)로 찍혀 소단 평탄부가 **여러 조각**으로 쪼개짐 — 이어진 평탄 조각을 모아
# 합이 소단 폭일 때만 소단으로 넘김(2026-09-15 · 한 조각만 보다가 첫 소단에서 사면이 끊겼음).
flat_run = 0.0
for start, end, run, rise in _outward(line, float(edge), side):
if run <= 1e-9:
continue
if abs(rise) <= _FLAT_RISE_M + run * berm_grade + berm_slack:
# 평탄부 — 측구 바닥·소단. 사면이 시작된 뒤라면 모아 두고 다음 조각에서 판정.
if abs(rise) <= _FLAT_RISE_M:
# 평탄부 — 측구 바닥·소단. 사면이 시작된 뒤라면 소단으로 보고 이어 간다.
if started and berm_width > 0 and abs(run - berm_width) < 0.05:
continue
if started:
flat_run += run
continue
if flat_run > 0:
if not (berm_width > 0 and abs(flat_run - berm_width) < 0.05):
break # 사면이 끝나고 평지를 만난 것이다
flat_run = 0.0
continue
ratio = run / abs(rise)
# 절토는 바깥으로 갈수록 오르고, 성토는 내려간다.
role = "cut" if rise > 0 else "fill"
@@ -30,7 +30,6 @@
from __future__ import annotations
import importlib
from typing import Any, Iterable
from B08_Quantity.B08_Quantity_Engine_Formwork import annotate as annotate_formwork
@@ -120,9 +119,7 @@ EXPANDERS = {
# BOX암거 — m당 × 연장(산림과임업기술 예제 · 울진 2×2 · 2026-09-15 브레인 12장 D).
"box_culvert": lambda h, l, o, f=None, r="": _box_culvert()(o),
# 콘크리트 포장 — 면적 × 두께 · 거푸집 2L · 수축줄눈(교본 3-2 · 부록 4-7 · 소광 · 2026-09-15).
"pavement_concrete": lambda h, l, o, f=None, r="": _lazy("Pavement").concrete_pavement(l, o),
# 비탈면 녹화 ㎡ 넷 — 구간 사면 면적 × 고른 면(품셈 5-14·5-22·5-23·5-25·5-28 · 2026-09-15).
"revegetation": lambda h, l, o, f=None, r="": _lazy("Revegetation").revegetation(o),
"pavement_concrete": lambda h, l, o, f=None, r="": _pavement().concrete_pavement(l, o),
# ⚠⚠ **큰돌쌓기(`boulder_masonry`)를 여기에 두지 않는다** (2026-09-07 발견).
# 큰돌쌓기는 품셈 **13-6** 이고 돌쌓기는 **13-4** 다 — **규격 축이 다르다.**
# 돌쌓기는 **뒷길이**(35·45·55·60㎝), 큰돌쌓기는 **직경**(40~60·60~80·80~100㎝).
@@ -140,9 +137,11 @@ def _box_culvert():
return box_culvert
def _lazy(part: str):
"""`_UnitQuantity_<part>` 전개 모듈을 늦게 가져온다(포장·녹화 · 700줄이라 한 벌로)."""
return importlib.import_module(f"B08_Quantity.B08_Quantity_Engine_UnitQuantity_{part}")
def _pavement():
"""콘크리트 포장 모듈을 늦게 가져온다."""
from B08_Quantity import B08_Quantity_Engine_UnitQuantity_Pavement
return B08_Quantity_Engine_UnitQuantity_Pavement
def _soil_guard():
@@ -197,10 +196,7 @@ EXPANDER_BILLING: dict[str, Any] = {
"soil_guard": lambda options, length: ("개소", 1.0), # 정본 「떼흙막이」 개소당(평균 붙박이)
"bed_sill": lambda options, length: ("", _num(options.get("area_m2"), 0.0)), # 돌붙임 ㎡당
"box_culvert": lambda options, length: ("m", _num(options.get("length_m"), 0.0)), # 점 배치
"pavement_concrete": lambda options, length: _lazy("Pavement").billing(
length, options
), # 면적 ㎡
"revegetation": lambda options, length: _lazy("Revegetation").billing(options), # 사면 면적 ㎡
"pavement_concrete": lambda options, length: _pavement().billing(length, options), # 면적 ㎡
}
#: 연장을 **구간(끝 − 시작)** 에서 읽는 종류 — 포장은 물넘이로 나뉘면 길이 칸(10)이 조각마다
@@ -67,11 +67,6 @@ DESTINATION = {
"수축줄눈": "unit_price",
"비닐": "material",
"철망": "material",
# ⭐ 2026-09-15 비탈면 녹화 ㎡ 넷 — 공종 품(5-14·5-22·5-23·5-25·5-28) · 떼 재료는 위 「떼」(자재).
"새심기": "unit_price",
"평떼": "unit_price",
"줄떼": "unit_price",
"비탈덮기": "unit_price",
}
#: ⛔ **갈 곳 기본값 금지**(명세 13장 「빠진 것이 조용해짐」) — 표에 없는 이름은 막고 사유를 냄.
@@ -1,142 +0,0 @@
"""비탈면 녹화 전개식 — **㎡ 넷(새심기·평떼·줄떼·비탈덮기)** (2026-09-15 브레인 12장 D 녹화 ⓐ~ⓓ).
근거
산림품셈 5-14 새심기(㎡당 · 1㎡당 10주) · 5-22 평떼(5-22-1 떼 0.3×0.3 11매/㎡ · 5-22-2 붙임 ㎡ ·
5-22-3 떼꽂이 ㎡) · 5-23 줄떼(5-23-1 떼 3.33매/㎡ · 5-23-2 붙임 · 5-23-3 떼꽂이) ·
5-25 거적덮기(㎡당) · 5-28 비탈덮기(5-28-1 짚망 · 5-28-2 방초·야자섬유매트)
교본 7-1 그림 7-8~7-12 는 사진·개념도(치수 없음) — 표준도가 아님.
⭐ 면적 = 구간 안 사면 면적(B06 측점 사면길이 · 평균단면적법) × 고른 면(절토면/성토면/양쪽).
⚠ 길이형(선떼·조공)은 다음 차례(선떼 = 사방기술교본 표 2-2-6 급별 m당 매수) · 나머지 공법은 사유.
"""
from __future__ import annotations
from typing import Any
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import Component, _num, routed
CUT, FILL = "b06_cut_area_m2", "b06_fill_area_m2"
FACES = {"절토면": (CUT,), "성토면": (FILL,), "양쪽": (CUT, FILL)}
#: 공법 → (성분 이름, 떼 매수/㎡ 또는 None, 근거). 떼 매수 = 품셈 5-22-1·5-23-1 「소요 매수」.
AREA_METHODS: dict[str, tuple[str, float | None, str]] = {
"새심기": ("새심기", None, "품셈 5-14 새심기(㎡당 · 1㎡당 10주 · 요소·인산은 표 안)"),
"평떼": ("평떼", 11.0, "품셈 5-22 평떼 — 떼 0.3×0.3 11매/㎡ · 붙임 5-22-2 · 떼꽂이 5-22-3"),
"줄떼": (
"줄떼",
3.33,
"품셈 5-23 줄떼 — 떼 0.3×0.3(30㎝ 간격) 3.33매/㎡ · 붙임 5-23-2 · 떼꽂이 5-23-3",
),
"비탈덮기": (
"비탈덮기",
None,
"품셈 5-25 거적덮기 · 5-28 비탈덮기(짚망·매트) — 덮개가 공종을 가름",
),
}
SOD_SPEC = "0.3×0.3"
LENGTH_METHODS = ("선떼붙이기", "조공")
NOTE_LENGTH = (
"{method}은(는) 길이형(10m·100m당) — 산출식 아직 없음 · 떼 매수는 품셈이"
" 「사방기술교본 기준표」라 함(선떼 = 표 2-2-6 급별 m당 매수 · 다음 차례)"
" · 단 직고·급수 칸은 기본값 없음(브레인 ⓒ)"
)
NOTE_LATER = (
"{method}은(는) 산출식이 아직 없음 — 12장 D 녹화 차례 뒤(㎡ 넷 먼저 · 2026-09-15 브레인 ⓓ)"
)
NOTE_FACE = "대상 면을 안 고름 — 절토면/성토면/양쪽 중 고를 것(공법마다 달라 기본값 없음)"
NOTE_COVER = (
"비탈덮기 덮개를 안 고름 — 거적(5-25)·짚망(5-28-1)·방초·야자섬유매트(5-28-2) 중 고를 것"
)
NOTE_OVERLAP = (
"⚠ 같은 사면이 초류종자살포·면고르기에도 들어 있음 — 겹친 몫을 자동으로 빼지 않음"
"(파종 구간을 줄일지 설계자가 고름 · 2026-09-15 브레인 ⓑ)"
)
NOTE_STAKE = (
"평떼 5-22-3 [주] 「평떼(0.06인) 품에는 떼꽂이 포함」은 5-12 품에 걸린 것으로 읽음"
" — 5-22-3 떼꽂이는 따로 셈"
)
NOTE_FIGURES = "교본 그림 7-8·7-10·7-11 은 사진 · 7-9·7-12 는 개념도(치수 없음) — 표준도가 아님"
def _length_at(points: list[tuple[float, float]], at: float) -> float:
"""그 자리의 사면길이 — 앞뒤 측점 사이 직선 보간 · 끝 밖이면 가장 가까운 측점."""
if at <= points[0][0]:
return points[0][1]
for (s0, v0), (s1, v1) in zip(points, points[1:]):
if s0 <= at <= s1:
return v0 if s1 == s0 else v0 + (v1 - v0) * (at - s0) / (s1 - s0)
return points[-1][1]
def _interval_area(points: list[tuple[float, float]], start: float, end: float) -> float:
if len(points) < 1 or end <= start:
return 0.0
cuts = [start, *[s for s, _v in points if start < s < end], end]
return sum(
(_length_at(points, a) + _length_at(points, b)) / 2 * (b - a)
for a, b in zip(cuts, cuts[1:])
)
def revegetation_from_designs(
structure: dict[str, Any],
designs: list[dict[str, Any]] | None,
slopes: list[Any] | None = None,
) -> dict[str, Any]:
"""구간 안 절토·성토 사면 면적을 B06 측점에서 셈해 실은 **새** 구조물(칸 값은 안 건드림)."""
if slopes is None:
from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slopes
slopes = station_slopes(designs or [])
ordered = sorted(slopes, key=lambda s: s.chainage_m)
options = dict(structure.get("options") or {})
notes = list(structure.get("notes") or [])
start, end = sorted((_num(structure.get("start_m")), _num(structure.get("end_m"))))
if not ordered:
notes.append("B06 횡단 설계가 없어 구간 사면 면적을 못 읽음")
return {**structure, "options": options, "notes": notes}
for key, attr in ((CUT, "cut_length_m"), (FILL, "fill_length_m")):
points = [(s.chainage_m, float(getattr(s, attr))) for s in ordered]
options[key] = round(_interval_area(points, start, end), 4)
notes.append(
f"구간 {start:g}~{end:g}m 사면 면적 — 절토면 {options[CUT]:,.2f}㎡ · 성토면"
f" {options[FILL]:,.2f}㎡ (B06 측점 사면길이 · 평균단면적법)"
)
return {**structure, "options": options, "notes": notes}
def _area(options: dict[str, Any]) -> float | None:
keys = FACES.get(str(options.get("face") or ""))
return None if keys is None else sum(_num(options.get(key)) for key in keys)
def revegetation(options: dict[str, Any]) -> tuple[list[Component], list[str]]:
"""녹화 한 구간 — (성분, 사유)."""
method = str(options.get("method") or "")
if not method:
return [], ["녹화 공법을 안 고름 — 공법을 고르면 섬"]
if method not in AREA_METHODS:
template = NOTE_LENGTH if method in LENGTH_METHODS else NOTE_LATER
return [], [template.format(method=method), NOTE_FIGURES]
area = _area(options)
if area is None:
return [], [NOTE_FACE]
if method == "비탈덮기" and not options.get("cover_kind"):
return [], [NOTE_COVER]
name, sheets, basis = AREA_METHODS[method]
notes = [NOTE_OVERLAP, basis, NOTE_FIGURES] + ([NOTE_STAKE] if method == "평떼" else [])
rows: list[tuple[str, str, float, str, str]] = [
(name, "", area, f"{options.get('face')} 사면 면적 {area:,.2f}", "")
]
if sheets is not None:
rows.append(("", "", area * sheets, f"면적 {area:,.2f} × {sheets:g}매/㎡", SOD_SPEC))
components = [
Component(item, unit, amount, destination, text, spec=spec)
for item, unit, amount, text, spec in rows
if (destination := routed(item, notes))
]
return components, notes
def billing(options: dict[str, Any]) -> tuple[str, float]:
"""내역 줄 — 녹화 면적 ㎡."""
return "", _area(options) or 0.0
@@ -54,11 +54,6 @@ from common_util.common_util_structure_lengths import structure_lengths
from config.config_db import run_with_connection
logger = logging.getLogger(__name__)
#: 소단 — 원단위 줄을 안 세우는 까닭(2026-09-15 조사).
BERM_COUNTED = (
"횡단 설계의 계단으로 토공(절·성토)·사면 면적(면고르기·파종·녹화)에 이미 들어감"
" — 따로 줄 안 세움"
)
router = APIRouter(prefix="/api/projects", tags=["B08 Quantity"])
@@ -100,11 +95,6 @@ def _collect_structures(
f"{definition.name}: {definition.design_owner} 가 이미 셈 — 중복 계상 방지"
)
continue
if type_id == "berm":
# 소단 계단은 횡단 설계가 절·성토 면적·사면길이에 이미 넣음 — 줄을 세우면
# 「산출식 없음」 거짓 막힘이 내역에 섬(2026-09-15 조사 · 임도 소단 품셈 공종 없음).
skipped.append(f"{definition.name}: {BERM_COUNTED}")
continue
if definition.reference_only:
skipped.append(f"{definition.name}: 전문 상세설계 대상 — 배치까지만")
continue
@@ -113,13 +103,6 @@ def _collect_structures(
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Pavement import pavement_from_designs
payload = pavement_from_designs(payload, designs)
if type_id == "revegetation":
# 구간 안 절토·성토 사면 면적은 B06 측점 사면길이에서(2026-09-15 녹화 ⓑ).
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revegetation import (
revegetation_from_designs,
)
payload = revegetation_from_designs(payload, designs)
if definition.group == "B":
# ⚠ B군(종단배수)은 **연장표**로 간다 — `common_util_structure_lengths` 가
# 겹친 구간을 합쳐 주기 때문이다. 구조물별로 세면 겹친 구간을 두 번 센다.
@@ -1134,6 +1134,10 @@ def build_unit_prices(
from B09_Estimation.B09_Estimation_RockLoss import attach_rock_loss
attach_rock_loss(build, master)
# 제목도 사유도 없는 잎 공종 — 버린 표 번호·모양을 사유로(2026-09-15 · 모든 공종 모듈이 돈 뒤).
from B09_Estimation.B09_Estimation_UnreadTables import mark_unread_tables
mark_unread_tables(build, master)
build.labor_reliability = _labor_reliability_in_use(build.book)
# ③ 할증 포함 재료량을 준 공종에 자재가 재료비로 붙지 않았는가(명세 6장 · ㉠).
from B09_Estimation.B09_Estimation_Guards import check_materials_before_surcharge
@@ -0,0 +1,42 @@
"""B09 원가계산 — **사유 없이 버려지는 표에 사유를 남긴다** (2026-09-15 브레인 「버릴 때는 사유」).
일할 표(작업량·소요량·형식 미정)를 가진 잎 공종인데 제목도 사유도 없는 것 — 표를 읽는 길이 없어 한 줄도
안 서고 어디에도 안 남음(잰 수 49 · ㉯ 가르기에서 먼저 6). 내역서가 「일위대가가 아직 없습니다」 만 띄워
무엇을 손볼지 모름 → 표 번호·모양을 `component_gaps` 에 적음(내역서 「성분 미확보 — …」 로 섬).
⚠ 읽는 길을 만드는 것이 아님 — 그 표가 풀리면 제목이 서서 이 사유는 저절로 안 붙음.
"""
from __future__ import annotations
from typing import Any
#: 마스터 표 형식 → 사유에 쓰는 이름. 여기 없는 형식(참고·계수)은 일할 표가 아님.
FORM_LABELS = {
"productivity": "작업량 표",
"requirement": "소요량 표",
"undetermined": "형식 미정 표(모양 판정 안 됨)",
}
UNREAD = "표를 읽는 길이 없어 한 줄도 안 섬 — {tables}"
def mark_unread_tables(build: Any, master: dict[str, Any]) -> None:
"""제목·사유가 둘 다 없는 잎 공종에 표 번호·모양 사유를 단다."""
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
nodes = master.get("work_items", [])
parents = {node.get("parent_code") for node in nodes}
titled = {code[2:].split("#")[0] for code in build.book.titles if code.startswith("B-")}
for node in nodes:
code = node["work_item_code"]
tables = [t for t in node.get("tables") or [] if t.get("pum_form") in FORM_LABELS]
if not tables or code in titled or code in parents:
continue
if (
build.unattached.get(code)
or build.component_gaps.get(code)
or code in build.basis_missing
or known_gap_note(code)
):
continue
listed = " · ".join(f"{t.get('pum_table_id')} {FORM_LABELS[t['pum_form']]}" for t in tables)
build.component_gaps[code] = UNREAD.format(tables=listed)
+2 -1
View File
@@ -7,13 +7,14 @@
### 남은 것
- [ ] ⚠ 자원 이름이 겹치면 남의 공종 풀이가 바뀜(명세 2장 이름 맞춤의 부작용) — 자원 목록에 「양수기」 를 넣자 12-26 물푸기 「양수기(150m/m)」 줄 풀이가 바뀌어 제목이 사라짐 → 지금은 「유인헬기방제 양수기」 로 피함(임시) · 언젠가 공종 범위·코드로 맞출 자리
- [ ] ㉯ 나머지 — 제목 없음(지금 셈 34 · 같은 자) 가르기: ⓐ 참고 표 19(1-2 단위표준·운반 · 2장 소요재료·기계손료 17 — 다른 공종이 읽는 표라 제목이 안 서는 게 맞음) ⓑ choose_one 부모 2(4-1·4-2 — 자식이 섬 · 단어는 부록 설계예시표 F0475·F0476 에서 걸림) ⓒ 단어는 설계예시표에서만 · 자기 표는 인력뿐 3(3-1 경계표시 **사유 없음** · 3-2 작업로 선정 초급기술자 못 풂 · 3-3 작업로 설치 비율 인력 못 풂) ⓓ **예취기·분무기 인력 표 한 모양 4**(6-2-2 줄베기 · 6-2-3 모두베기 · 6-4-1 덩굴걷기 **사유 없음** · 6-4-2 덩굴 약제 — 「이름 | 도구 | 인/ha | 값 | 직종」 줄을 못 읽음 · 풀리면 2장 예취기·배부식분무기 1일 몫을 체인톱처럼) ⓔ 이름에만 드론·윈치 3(3-8 드론 영상 촬영 인력만 **사유 없음** · 7-3 아키야윈치 **사유 없음** · 8-6-2 드론방제 인력 줄 못 풂) ⓕ 거리 표 2(10-13-1 드론운반 · 10-13-2 인력운반 — 형식 미정으로 건너뜀 · **사유 없음**) ⓖ 13-10-2 나무 말뚝박기 1(뭉친 칸) · 사유 없이 버려지는 표 6(F0075 · F0082 · F0158 · F0174 · F0322 · F0323)
- [ ] ㉯ 나머지 — 제목 없음(지금 셈 34 · 같은 자) 가르기: ⓐ 참고 표 19(1-2 단위표준·운반 · 2장 소요재료·기계손료 17 — 다른 공종이 읽는 표라 제목이 안 서는 게 맞음) ⓑ choose_one 부모 2(4-1·4-2 — 자식이 섬 · 단어는 부록 설계예시표 F0475·F0476 에서 걸림) ⓒ 단어는 설계예시표에서만 · 자기 표는 인력뿐 3(3-1 경계표시 **사유 없음** · 3-2 작업로 선정 초급기술자 못 풂 · 3-3 작업로 설치 비율 인력 못 풂) ⓓ **예취기·분무기 인력 표 한 모양 4**(6-2-2 줄베기 · 6-2-3 모두베기 · 6-4-1 덩굴걷기 **사유 없음** · 6-4-2 덩굴 약제 — 「이름 | 도구 | 인/ha | 값 | 직종」 줄을 못 읽음 · 풀리면 2장 예취기·배부식분무기 1일 몫을 체인톱처럼) ⓔ 이름에만 드론·윈치 3(3-8 드론 영상 촬영 인력만 **사유 없음** · 7-3 아키야윈치 **사유 없음** · 8-6-2 드론방제 인력 줄 못 풂) ⓕ 거리 표 2(10-13-1 드론운반 · 10-13-2 인력운반 — 형식 미정으로 건너뜀 · **사유 없음**) ⓖ 13-10-2 나무 말뚝박기 1(뭉친 칸) · 사유 없이 버려지던 것은 전체 49 로 넓혀 사유 붙음(490cab5e) · 남은 차례: 예취기 한 모양 4 → 나머지(3-2 · 3-3 · 8-6-2 · 13-10-2)
- [ ] 막힌 셋 풀기(원문에 값이 없음 · 입력·견적 칸이 서야 함): 12-26 물푸기 가동시간 · 10-7-4 모노레일 ps·임대료·운반거리 · 10-8-3 케이블크레인 ℓ/kWh · 8-6-3 지상방제 1일 작업량·1톤 트럭
- [ ] 손료계수 없는 기계 222종 되살림(미룸 · 브레인 승인 — 지금 막는 공종 0) — 가르기 끝: 원문 없음 0 · 원천 한 줄 누락 1(3201-0003 「-0003」 표기) · 뭉친 칸이지만 열마다 개수 맞고 상각+정비+관리=계 검증 통과 196(크레인(무한궤도) 15 · 골재생산(컨베이어·크러셔·스크린·빈) 55 · 해상(준설선·예선·대선 등) 66 · 그 밖 60) · 글줄로 붙음 25(3601 콘크리트 피니셔 5 · 9020 준설선 7 · 9060 토운선 6 · 7) · 원문 md 한 줄 읽기가 기존 계수 383/383 일치 · 8-4 운전경비 버려진 줄 3 = 81기종(포장 27 · 기타 36 · 기타 18) 중 공종이 쓰는 것은 물탱크 하나(되살림 끝)
- [ ] 10-7-4 모노레일 제목의 보통인부 두 번 셈(4인) — 막힌 채 사유만 적음 · 풀리는 날 운전 단가표 2인을 빼고 셀 것
- [ ] 소형 장비 1일 소모 표(㉯) 가운데 2-1-2 경유 표(집재장비) · 천공기 · 예취기 — 쓰는 공종이 풀릴 때 같은 한 벌로
### 검증완료(2026-09-14 밤 ~ 09-15 · 커밋 해시)
- [X] 사유 없이 버려지던 잎 공종 49 에 사유(브레인 차례 ① · ㉯ 자로 본 6 을 전체로 넓혀 셈) — `B09_Estimation_UnreadTables`: 일할 표(작업량·소요량·형식 미정)가 있는데 제목도 사유(못 붙은 줄·성분 빠짐·밑수·[주])도 없는 잎 공종에 「표를 읽는 길이 없어 한 줄도 안 섬 — 표 번호 모양」 을 성분 미확보로 · 읽는 길은 안 만듦(그 표가 풀리면 저절로 안 붙음) · 전후: 사유 +49 · 제목·금액 변화 0 · 프로젝트 내역에 뜨는 것 0(인계 24 공종과 안 겹침 — 화면 묶어 보이기는 아직 필요 없음) · 49 = 2-1-12 · 3장 3 · 4장 4 · 5장 6(5-26 흙갈이 셋은 형식 미정) · 6장 3 · 7장 14 · 8장 9 · 9장 4(콘크리트 깨기) · 10장 4 · 12-29 1 = 이 목록이 곧 「읽는 길」 차례표. 검증완료(490cab5e)
- [X] 조종원 직종 8-1-2 5호 원문대로(브레인 「지금 끼울 것」) — 원문이 이름·규격으로 가르는 기종만(덤프트럭 12t · 콘크리트 믹서 0.55㎥ · 공기압축기(이동식) 2.83㎥/min · 괄호 앞 이름 끝말로 · 살수차는 괄호 속) · 바뀐 레코드 정확히 12(덤프 15·20·24·32t 화물차→건설기계 · 콘크리트 믹서 0.10~0.45 · 래머 · 플레이트 콤팩터 → 일반기계) · 원문으로 가름 161 · 잠정 56(콘크리트 펌프차 · 파일천공전용장비 · 타워크레인 · 트랙터(타이어) · 크러셔 · 크롤러드릴 …) 은 옛 규칙 + 중기경비 장 사유 · 곁: 파쇄기 중기경비 장이 연료·조종원 없다고 거짓 공백을 띄우던 것 걷음 · 전후(본당): 9-14-2 14,523 → 9,177 · 10-12 L164.23m 3,249 → 3,519 · 9,172 → 9,912 · 10,520 → 11,369 · 프로젝트 본체 109,671,600 → 109,682,519(+10,919 · 덤프 운반 토사) · 거창 실무 「덤프 15Ton 건설기계 · 8·4.5Ton 화물차 · 플레이트 콤팩터 일반기계운전사」 = 원문. 검증완료(22692669)
- [X] 초류종자살포 5-24-1·2 둘이 섬(브레인 판정 ①②′④) — ① 트럭 4.5ton → 덤프트럭 4.5(범위 별칭 · 옆 칸 규격이 대상 규격과 같으면 덮는 것 아님) ②′ 종자살포기 → 취부기 11.94㎾ 손료만(`LOSS_ONLY_MACHINES` · 8-4 에 줄 없음 · 거창도 손료 20,295 만 · 디젤엔진은 원문 둘 다에 없어 안 넣음) ④ 종자 후보 넷 칸 · 넣은 하나만 · 둘이면 사유(`B09_Estimation_SeedSpray`) · 어긋남 사유 나란히 · 233 → 527(③) → 744원/㎡(재료 단가 전) · 다른 공종·프로젝트 내역 변화 0. 검증완료(3d896dec)
- [X] ③ 물탱크(살수차) 운전경비 되살림 — 8-4-8 셀 병합으로 줄째 버려지던 7204 다섯 원문 값(8.2·8.6·9.3·9.4·12.9 · 잡재료 30 · 1인) · 조종원 8-1-2 5호 살수차 = 화물차운전사 · 거창 5,500 일치. 검증완료(078af37d)
+2 -28
View File
@@ -2,17 +2,6 @@
임시 · 2026-09-15 시놀로지 덮어쓰기 때문 · 사용자 확인 전 (브레인 지시 · git 에 명시 커밋 · 브레인이 나중에 PLAN.md 한 벌로 합침 · PLAN.md 는 읽기만)
## ⚠ 2026-09-15 사용자 지시 — 「화면을 STmate 와 최대한 가깝게」 취소 (브레인 전달)
화면 모양만 취소 · 계산·수량·금액의 STmate 재현(골든셋 검산)은 그대로. 화면 모양 때문에 잡아 둔 줄 표시(PLAN.md 는 읽기만이라 여기 적음 · 합칠 때 반영):
- 랩탑 서브 몫 열린 줄 — **없음**(이 파일의 12장 D·B08 훑기 줄은 전부 수량·금액 일감 · 화면 모양 줄 아님)
- PLAN.md 12장 「화면 — STmate 를 본뜬 B08·B09 프론트엔드」에서 화면 모양 때문에 선 줄(표시만)
- 🏷 화면모양 — 장 제목 「STmate 를 본뜬」 · 사용자 방침 「최대한 가깝게 구성」 · 「만들 화면 — STmate 이름을 짝지어」 표(`wQtyList`·`wBoq`·`wExpnd`·`wM_Edit_iLWi`·`wM_Edit_San`·`wM_JabPum`·`wM_KanJub`·`wM_Boxa`)
- 🏷 화면모양 — 「지킬 것 · STmate 를 본뜨되 값은 우리 명세대로 — 화면만 닮음」
- 🏷 화면모양(닫힘 · 되돌릴 일 없음) — 내역서·일위대가표·단산 목록표 틀 · 집계표 넷 · 목록표 일곱 · 옛 탭 여섯 → 새 틀 · 자재단가대비표 「변동없음 / 1~5 단가 / 최소단가」 고르개
- ❓ 갈래 판정 필요(브레인) — 「범위 — 전체를 비슷하게 · STmate 가 가진 것을 되도록 다 갖춤」 · 열린 줄 「내역 종류 전부(`PM_Prn_Ord` 목록 그대로)」 — 기능 범위인지 화면 모양인지 · 「원가계산 4형식 다」는 계산이라 그대로로 읽음
## 12장 — B08 훑기 고칠 차례 이어 (PLAN.md 12장 「고칠 차례(브레인 2026-09-14)」)
- [X] ㉳ (가) 미교차 측점 지반 샘플을 지표면 끝까지 넓힘(+5m 씩 · 상한 = 지표면 끝) 8bf24c26. 검증완료
@@ -58,24 +47,9 @@
④ 제안값 소광 관측 C 0.85 · L 1.25 · 원문 1-2-3 역(礫) L 1.10~1.20 · C 1.05~1.10 병기 · ⚠ 소광 C(줄어듦)와 역 C(늘어남)는 방향이 반대 — 사유에 적음
⑤ 폭 = 차도 표준 폭 + 확폭(노견 제외 · 소광 「B=3.0」)
- [X] 토공집계 두 줄(혼합석부설 11-4 ㎥ = 다짐 후 ÷ C × L · 사전터파기 토사 9-3-2) · 자재총괄 혼합석 ㎥(사급) · 산출 조건 칸(범위 · 연약·습윤 구간 · 두께·C·L 비면 제안값) · 종단 경사는 계획선에서 B05 포장 제안과 같은 한 벌(`local_grade_pct` 로 뺌 — 종단 파일 측점 경사에 비정규 측점 8곳이 없었음) · 산출 조건 받는 모양은 `_Router_Earthwork_Settings` 로 가름 196bd73a. 검증완료(시험 · 936be972 화면 조작 후 되돌림)
- 936be972 — 법령 조건 0㎡(토사 토글 켠 측점 0 — 데이터대로) · 전 구간 3,533.18㎡ · 519.58㎥ · 법령 + 연약·습윤 100~200 = 112.42㎡ · 되돌림(키는 false·[]·null 로 남음)
- 브레인 답 — ① 토사/암 = B06 토사 토글이 정본(리핑암은 「암」 자리표시 · 구성비는 암 몫만) · 사유 「토사 = B06 측점 토사 토글(구성비는 암 몫만 가름)」 ② 사전터파기 9-3-2 승인 · 사유 「산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음」
- 936be972 — 법령 조건 0㎡(62측점 모두 리핑암 자리표시) · 전 구간 3,533.18㎡ · 519.58㎥ · 법령 + 연약·습윤 100~200 = 112.42㎡ · 되돌림(키는 false·[]·null 로 남음)
- ⚠ 물음 — B06 토사 토글 끔이면 측점 지반이 「리핑암」 자리표시라 법령 자동이 영영 0 · 사전터파기 공종 9-3-2 로 이은 것 확인 대기
- [ ] 녹화 — 교본 7-1 · 그림 7-8~7-12 「표준도」가 모식도인지 먼저
- 확인 — 7-8·7-10·7-11 사진 · 7-9·7-12 개념도(치수 없음) ⇒ 표준도 아님 · 치수는 교본 본문 · 품셈 5장 공종·밑수(5-14 새심기 ㎡ · 5-16 단끊기 100m·선떼 10m · 5-17 조공 · 5-18 씨뿌리기 m · 5-22 평떼 ㎡ · 5-23 줄떼 ㎡ · 5-25 거적 · 5-28 비탈덮기) · 선떼 떼 매수는 사방기술교본 표 2-2-6(급별 1m당 매수 · 2.5매/m·6매/m 두 규격)
- 12장 D 표(PLAN.md) 고칠 것 — 녹화 「표준도 그림 7-8~7-12」 → 「그림 7-8·7-10·7-11 사진 · 7-9·7-12 개념도(치수 없음) — 표준도 아님」(PLAN.md 읽기만이라 여기 적음)
- 판정(브레인) — ⓐ 초류종자 살포는 공법에서 빼고 칸 밑에 까닭 한 줄 ⓑ 대상 면 칸(절토면/성토면/양쪽) ⓒ 선떼 떼 매수 = 사방기술교본 표 2-2-6(급별) · 단 직고·급수 칸 기본값 없음 ⓓ ㎡ 넷(새심기·평떼·줄떼·비탈덮기)부터 · 길이형은 선떼가 조공·씨뿌리기보다 앞
- [X] ㎡ 넷 — 등록부 칸(공법 · 대상 면 · 덮개 · 단 직고·급수 「아직 표에 안 쓰임」) · 면적 = 구간 안 사면 면적(B06 측점 사면길이 · 끝 보간) · 떼 11매/㎡(5-22-1)·3.33매/㎡(5-23-1) 자재 · 묶음 5-14 · 5-22-02+03 · 5-23-02+03 · 5-25 · 5-28-01 · 5-28-02. 검증완료(시험 · 936be972 전 구간 면적 = 사면표 합계)
- 남은 막힘(B09) — 5-28-01 밑수 없음 · 5-28-02 호표 없음
- ⚠ B09 묶음 줄 단가가 줄 수량만큼 부풂(조각 수량 = 합계인데 1단위로 봄) — 브레인이 데스크탑 서브에 넘김(B09 가 줄 수량으로 나눔 · 시험을 수량 ≠ 1 로)
- 브레인 답 — ⓐ 떼꽂이 읽기 승인 · 사유 「[주]는 5-12 품에 걸린 것으로 읽음 — 5-22-3 떼꽂이는 따로 셈」 ⓑ 겹친 사면은 사유만(맨 앞 ⚠) · 자동으로 안 뺌
- [ ] 사용자 판단 거리 — 「녹화 구간은 초류종자살포(파종)에서 뺄까요」 칸을 둘지
- [ ] 선떼붙이기(길이형) — 표 2-2-6 급별 m당 매수 · 단끊기 연장 = 직고 ÷ 단 직고 × 연장(5-16-1 [주]②)
- [ ] 조공·씨뿌리기·편책·녹생토·나무심기
- [ ] 소단 — 임도 조문(사면길이 2~3m 마다 폭 50~100㎝) · 사방 기준 아님
- 12장 D 표(PLAN.md) 고칠 것 — 소단 근거 「사방사업 세부기준 제3조1호(절·성토고 3~5m · 폭 0.5m 이상)」 → 임도 「시행규칙 별표2 Ⅰ.2.차.(5) 절토·성토한 경사면이 붕괴 또는 밀려 내려갈 우려가 있는 지역에는 사면길이 2~3m 마다 폭 50~100㎝」(조건부 · 우려 지역)
- 지금 서는 자리(조사) — 등록부 「소단」(C군 구간 · 폭 0.5 · 간격 3 · 안쪽 0° — 별표2 범위 안 가장 적게 파는 조합) → B06 계단 기하(`common_util_cross_berm`) → 절·성토 면적(토공)·사면길이에 들어감 · 자동 배치 없음(설계자가 놓음) · 품셈에 임도 소단 공종 없음
- [X] 흠 둘 고침 — ① 설계선 0.5m 격자에 소단 평탄부가 쪼개져 사면길이가 첫 소단에서 끊김(측점 20 성토 8.91 → 3.0m) → 이어진 평탄 조각을 모아 소단 폭과 대조 · 안쪽 기울기 반영 ② 소단 구조물이 원단위에 「산출식 없음」 거짓 막힘으로 섬 → 건너뜀 + 까닭. 검증완료(시험 · 936be972 읽기만)
- 936be972 전 측점 소단(0.5·3·0°) 가정 전후(읽기만) — 성토 18,247.12 → 41,179.33㎥(+125.7%) · 절토 6,686.30 → 6,719.03㎥ · 면고르기·파종·지장목 성토면 15,919.95 → 16,979.02㎡(+6.7% · 고치기 전엔 3,136.23 80%) · 절토면 5,588.88 → 6,258.46㎡(+12.0%) · 미폐합 측점 2 → 26
- ⚠ 녹화 화면 확인 보류 — 구조물 저장이 B06 이후 단계를 STALE 로 돌림(`_invalidate_downstream`) · 지워도 단계 상태는 안 돌아옴
- [ ] 개소형 — 대피소·작업장·차돌림곳·노출형 횡단수로
- [ ] 부대 — 가드레일·반사경·표지(그림 11-6~11-8 표준도 확인 · 번호판 500m 제안)
@@ -195,7 +195,7 @@
"work_item_code": "FP-09-03-02",
"basis_unit": "㎥",
"master_name": "토사깍기 > 기계",
"note": "2026-09-15 — 교본 10-1 「포설 전 10㎝ 터파기」 · 부록 7-2 「노면의 토사구간」이라 토사 측점만 · 브레인 ① 토공 축. 산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음(같은 일에 맞는 공종을 고름 · 2026-09-15 브레인 ② 승인)."
"note": "2026-09-15 — 교본 10-1 「포설 전 10㎝ 터파기」 · 부록 7-2 「노면의 토사구간」이라 토사 측점만 · 브레인 ① 토공 축. ⚠ 노면 10㎝ 걷기를 토사깍기(기계)로 이음 — 산림품셈에 노면 사전 터파기 공종이 따로 없음(판정 확인 대기)."
}
],
"haul": [
@@ -534,68 +534,6 @@
],
"why": "실무 내역은 「콘크리트포장 T=20 ㎡」 한 줄이고 품셈은 포설(12-6 ㎥ · 비닐·철망깔기·양생 포함) · 거푸집(12-8 연장 m) · 줄눈(12-7) 을 따로 둠 — 거푸집·줄눈 제외라 묶음으로 셈(2026-09-15 브레인 12장 D 포장 둘). 콘크리트·비닐·철망 재료는 자재 축.",
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
},
{
"type_id": "revegetation",
"when": {"method": "새심기"},
"parts": [
{"code": "FP-05-14", "name": "새심기", "unit": "㎡", "from_components": ["새심기"]}
],
"why": "비탈면 녹화 한 줄(등록부 공법 고르기)이 공법마다 다른 품셈 공종으로 감 — 새심기 5-14 ㎡당(2026-09-15 브레인 녹화 ⓓ).",
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
},
{
"type_id": "revegetation",
"when": {"method": "평떼"},
"parts": [
{"code": "FP-05-22-02", "name": "평떼 붙임", "unit": "㎡", "from_components": ["평떼"]},
{
"code": "FP-05-22-03",
"name": "떼꽂이 제작 및 설치",
"unit": "㎡",
"from_components": ["평떼"],
"why": "5-22-3 [주] 「평떼(보통인부 0.06인)품에는 떼꽂이가 포함」은 5-12 품에 걸린 것으로 읽음 — 5-22-3 떼꽂이는 따로 셈(5-22-2 붙임을 씀 · 2026-09-15 브레인 ⓐ 승인 · 뒤집을 수 있음)"
}
],
"why": "평떼 5-22 — 떼(0.3×0.3 11매/㎡)는 자재 축 · 붙임·떼꽂이 ㎡ 묶음(2026-09-15 브레인 녹화 ⓓ).",
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
},
{
"type_id": "revegetation",
"when": {"method": "줄떼"},
"parts": [
{"code": "FP-05-23-02", "name": "떼붙임", "unit": "㎡", "from_components": ["줄떼"]},
{"code": "FP-05-23-03", "name": "떼꽂이 제작 및 설치", "unit": "㎡", "from_components": ["줄떼"]}
],
"why": "줄떼 5-23 — 떼(0.3×0.3 · 30㎝ 간격 3.33매/㎡)는 자재 축 · 붙임·떼꽂이 ㎡ 묶음(2026-09-15 브레인 녹화 ⓓ).",
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
},
{
"type_id": "revegetation",
"when": {"method": "비탈덮기", "cover_kind": "거적"},
"parts": [
{"code": "FP-05-25", "name": "거적덮기", "unit": "㎡", "from_components": ["비탈덮기"]}
],
"why": "비탈덮기(거적) — 품셈 5-25 거적덮기 ㎡당(거적 1.1㎡ 표 안 · 앙카핀 등 부속은 별도).",
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
},
{
"type_id": "revegetation",
"when": {"method": "비탈덮기", "cover_kind": "짚망"},
"parts": [
{"code": "FP-05-28-01", "name": "짚망", "unit": "㎡", "from_components": ["비탈덮기"]}
],
"why": "비탈덮기(짚망) — 품셈 5-28-1 · ⚠ 원문 표에 밑수(단위) 표기가 없음 — B09 가 밑수로 막으면 그 사유가 섬 · 재료량은 설계수량 별도([주]②).",
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
},
{
"type_id": "revegetation",
"when": {"method": "비탈덮기", "cover_kind": "방초·야자섬유매트"},
"parts": [
{"code": "FP-05-28-02", "name": "방초매트 및 야자섬유매트 포장", "unit": "㎡", "from_components": ["비탈덮기"]}
],
"why": "비탈덮기(매트) — 품셈 5-28-2 일당 시공량(폭 1.5m 이하 90㎡ · 2.0m 이하 130㎡) · 폭 갈래는 B09 몫.",
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
}
],
"unit_conversion": {
@@ -42,11 +42,6 @@ MARKED: dict[str, tuple[dict, list[str]]] = {
"weep_hole_area_m2",
],
),
# 2026-09-15 녹화 ㎡ 넷 — 단 직고·선떼 급수는 길이형(선떼·조공) 산출식이 설 때 읽힘.
"revegetation": (
{"method": "평떼", "face": "양쪽", "b06_cut_area_m2": 40.0, "b06_fill_area_m2": 60.0},
["step_height_m", "sod_grade"],
),
"soil_guard": (
{"height_m": 1.0, "length_m": 10.0, "form": ""},
[
@@ -47,8 +47,6 @@ def test_법령_조건만_자동_토사_8퍼센트_초과() -> None:
assert result["excavation_soil_m3"] == pytest.approx(9.0)
text = " ".join(result["notes"])
assert "별표2" in text and "토사 전부를 해당으로 봄" in text
# 토사/암은 B06 토사 토글이 정본 — 리핑암은 「암」의 자리표시(브레인 2026-09-15 ① 정정).
assert "토사 = B06 측점 토사 토글(구성비는 암 몫만 가름)" in text
assert "원문 표에 혼합석 줄 없음" in text and "방향이 반대" in text
assert "다짐" in text
@@ -68,10 +66,6 @@ def test_비포장_전_구간_고르개는_포장만_뺀다_암은_사전터파
assert result["excavation_soil_m3"] == pytest.approx(170.0 * 0.10)
assert any("교본 3-2" in note for note in result["notes"])
assert any("토사구간" in note for note in result["notes"])
assert any(
"산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음" in note
for note in result["notes"]
)
def test_칸이_두께_C_L_을_덮어쓴다() -> None:
@@ -1,115 +0,0 @@
"""비탈면 녹화 ㎡ 넷 — 새심기·평떼·줄떼·비탈덮기 (2026-09-15 브레인 12장 D 녹화 ⓐ~ⓓ).
근거
산림품셈 5-14 새심기(㎡당) · 5-22 평떼(떼 0.3×0.3 11매/㎡ · 붙임 ㎡ · 떼꽂이 ㎡) ·
5-23 줄떼(떼 3.33매/㎡ · 붙임 ㎡ · 떼꽂이 ㎡) · 5-25 거적덮기(㎡당) · 5-28 비탈덮기(짚망 · 매트)
판정 — 면적 = 구간 안 사면 면적(칸: 절토면/성토면/양쪽 · 기본값 없음) · 초류종자 살포는 공법에서 뺌
(토공집계가 5-24 로 사면 전체를 셈) · 단 직고·선떼 급수 칸은 기본값 없음 · 길이형은 다음 차례.
"""
from __future__ import annotations
import pytest
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revegetation import (
revegetation_from_designs,
)
AREAS = {"b06_cut_area_m2": 40.0, "b06_fill_area_m2": 60.0}
def _row(**options) -> dict:
structure = {
"structure_id": "g1",
"type_id": "revegetation",
"start_m": 0.0,
"end_m": 20.0,
"options": {**AREAS, **options},
}
table = build_table([structure], {"revegetation": "비탈면 녹화"})
return table
def _parts(table: dict) -> dict:
row = next(
r for r in build_handoff(unit_quantity_table=table)["work_items"] if "녹화" in r["name"]
)
return {part["code"]: part for part in row["composite_parts"] or []}
def test_평떼_양쪽은_면적과_떼_11매() -> None:
table = _row(method="평떼", face="양쪽")
row = table["structures"][0]
got = {(c["name"], c["unit"]): c["amount"] for c in row["components"]}
assert (row["billing_unit"], row["billing_quantity"]) == ("", pytest.approx(100.0))
assert got[("평떼", "")] == pytest.approx(100.0)
assert got[("", "")] == pytest.approx(1100.0)
text = " ".join(row["notes"])
assert "겹친 몫을 자동으로 빼지 않음" in text and row["notes"][0].startswith("")
assert "5-12 품에 걸린 것으로 읽음 — 5-22-3 떼꽂이는 따로 셈" in text
parts = _parts(table)
assert parts["FP-05-22-02"]["quantity"] == pytest.approx(100.0)
assert parts["FP-05-22-03"]["quantity"] == pytest.approx(100.0)
def test_줄떼_절토면은_3_33매() -> None:
table = _row(method="줄떼", face="절토면")
got = {(c["name"], c["unit"]): c["amount"] for c in table["structures"][0]["components"]}
assert got[("줄떼", "")] == pytest.approx(40.0)
assert got[("", "")] == pytest.approx(40.0 * 3.33)
assert set(_parts(table)) == {"FP-05-23-02", "FP-05-23-03"}
def test_새심기_성토면() -> None:
table = _row(method="새심기", face="성토면")
assert _parts(table)["FP-05-14"]["quantity"] == pytest.approx(60.0)
def test_비탈덮기는_덮개를_골라야_선다() -> None:
table = _row(method="비탈덮기", face="양쪽", cover_kind="거적")
assert _parts(table)["FP-05-25"]["quantity"] == pytest.approx(100.0)
unset = _row(method="비탈덮기", face="양쪽")["structures"][0]
assert not unset["components"] and any("덮개" in n for n in unset["notes"])
def test_면을_안_고르면_사유() -> None:
row = _row(method="평떼")["structures"][0]
assert not row["components"] and any("절토면" in n and "성토면" in n for n in row["notes"])
def test_길이형은_떼_매수표를_기다린다() -> None:
row = _row(method="선떼붙이기", face="양쪽")["structures"][0]
assert not row["components"]
assert any("사방기술교본" in n for n in row["notes"])
def test_구간_안_사면_면적은_B06_측점에서_보간() -> None:
designs = [
{"chainage_m": 0.0, "design": {}},
{"chainage_m": 20.0, "design": {}},
]
from B08_Quantity.B08_Quantity_Engine_SlopeLength import StationSlope
slopes = [
StationSlope(chainage_m=0.0, cut_length_m=2.0, fill_length_m=4.0),
StationSlope(chainage_m=20.0, cut_length_m=4.0, fill_length_m=0.0),
]
structure = {"type_id": "revegetation", "start_m": 5.0, "end_m": 20.0, "options": {}}
filled = revegetation_from_designs(structure, designs, slopes)
# 절토 5→2.5, 20→4 : (2.5+4)/2×15 · 성토 5→3, 20→0 : (3+0)/2×15
assert filled["options"]["b06_cut_area_m2"] == pytest.approx(48.75)
assert filled["options"]["b06_fill_area_m2"] == pytest.approx(22.5)
def test_등록부_공법에서_초류종자_살포를_빼고_까닭을_보인다() -> None:
options = {o.key: o for o in structure_type_map()["revegetation"].options}
assert "초류종자 살포" not in options["method"].choices
assert "토공집계" in (options["method"].default_basis or "")
assert (
options["face"].choices == ["절토면", "성토면", "양쪽"] and options["face"].default is None
)
for key in ("step_height_m", "sod_grade"):
assert options[key].default is None and "아직 표에 안 쓰임" in options[key].not_in_table
@@ -1,51 +0,0 @@
"""소단이 선 사면의 사면길이 — 첫 소단에서 끊기지 않을 것 (2026-09-15 소단 착수 전 조사).
(936be972 읽기만) 측점에 소단(0.5m · 3m · 0°) 놓자 성토 사면길이가 측점 20 에서
8.91 3.0m 끊겨 면고르기·파종 성토면 15,919.95 3,136.23(80%) 줄었음.
까닭 설계선이 0.5m 격자로 찍혀 소단 평탄부 0.5m 0.195 + 0.305 조각으로 쪼개짐
평탄부 조각 = 소단 판정이 맞아 거기서 사면이 끝난 것으로 . 안쪽 기울기(°) 주면
평탄부가 아예 평탄으로 읽혀 같은 자리에서 끊김.
"""
from __future__ import annotations
import pytest
from B06_Section.B06_Section_Engine_Design import compute_cross_design
from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slope
from common_util.common_util_cross_berm import BermSpec
# 노면보다 8m 낮은 평지 — 양쪽 성토 사면이 길게 섬.
SAMPLES = [{"offset_m": x / 2, "elevation_m": -8.0} for x in range(-160, 161)]
def _design(berm: BermSpec | None) -> dict:
return compute_cross_design(
SAMPLES, 0.0, ground_type="soil", section_mode="both_fill", berm=berm
)
@pytest.mark.parametrize("slope_deg", [0.0, 2.0])
def test_소단이_있어도_사면길이가_끝까지_이어진다(slope_deg: float) -> None:
plain = station_slope(0.0, _design(None))
stepped_design = _design(BermSpec(0.5, 3.0, slope_deg))
stepped = station_slope(0.0, stepped_design)
assert plain.fill_length_m > 15 # 양쪽 합 — 시험이 뜻을 가지는 크기
# 소단 평탄부는 사면길이에 안 들고, 비탈 조각은 끝까지 셈 — 같은 낙차라 거의 같음.
assert stepped.fill_length_m == pytest.approx(plain.fill_length_m, rel=0.05)
assert stepped.fill_height_m == pytest.approx(plain.fill_height_m, abs=0.2)
def test_소단_구조물은_원단위_줄을_안_세우고_까닭을_남긴다(monkeypatch, tmp_path) -> None:
"""소단 계단은 횡단 설계가 절·성토 면적·사면길이에 이미 넣음 — 따로 줄을 세우면 「산출식 없음」
거짓 막힘(formula_missing) 내역에 (2026-09-15 조사)."""
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
from B08_Quantity import B08_Quantity_Router_Material as material
berm = StructureInstance(
structure_id="b", type_id="berm", placement="interval", start_m=100.0, end_m=120.0
)
monkeypatch.setattr(material, "load_structures", lambda root: (1, [berm]))
targets, _names, skipped = material._collect_structures(str(tmp_path))
assert targets == []
assert any("소단" in note and "토공" in note and "사면" in note for note in skipped)
@@ -0,0 +1,55 @@
"""사유 없이 버려지는 표 — 2026-09-15 브레인 차례 ① (㉯ 가르기에서 6 · 전체로 재면 49).
일할 (작업량·소요량·형식 미정) 가진 공종인데 **제목도 사유도 없는** 표를 읽는 길이 없어
줄도 서고 `unattached`·`component_gaps`·밑수·[] 사유 어디에도 남아, 내역서엔 일위대가가 아직
없습니다 (무엇을 손볼지 모름). : 3-1 경계표시 F0075 0.2 | 보통인부(값이 이름 ) ·
6-4-1 덩굴걷기 F0158 · 7-3 아키야윈치 F0174 · 10-13-1 드론운반 F0322(형식 미정).
읽는 길을 만드는 것은 (예취기 모양 4 ) 여기서는 **버린 표를 사유로 남김**(버릴 때는 사유).
"""
from __future__ import annotations
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
WORK_FORMS = {"productivity", "requirement", "undetermined"}
def test_제목_없는_잎_공종은_모두_사유가_있음() -> None:
build = cached_build()
master = load_work_item_master()
parents = {n.get("parent_code") for n in master["work_items"]}
titled = {t[2:].split("#")[0] for t in build.book.titles if t.startswith("B-")}
silent = [
n["work_item_code"]
for n in master["work_items"]
if n["work_item_code"] not in titled
and n["work_item_code"] not in parents
and any(t.get("pum_form") in WORK_FORMS for t in n.get("tables") or [])
and not (
build.unattached.get(n["work_item_code"])
or build.component_gaps.get(n["work_item_code"])
or n["work_item_code"] in build.basis_missing
or known_gap_note(n["work_item_code"])
)
]
assert silent == [], silent
def test_사유는_표_번호와_모양을_가리킴() -> None:
gaps = cached_build().component_gaps
for code, table in (
("FP-03-01", "F0075"),
("FP-06-04-01", "F0158"),
("FP-07-03", "F0174"),
("FP-03-08", "F0082"),
):
assert table in gaps[code] and "한 줄도" in gaps[code], (code, gaps.get(code))
assert "F0322" in gaps["FP-10-13-01"] and "형식 미정" in gaps["FP-10-13-01"]
def test_부모와_제목_선_공종에는_안_붙음() -> None:
build = cached_build()
assert "FP-04-01" not in build.component_gaps # choose_one 부모 — 잎이 섬
assert "FP-05-24-01" not in build.component_gaps # 제목 선 공종