fix(axis): 표 읽기 1차 — 뭉친 줄 표기 맞추기·비고 거름·「-」 칸 · 지장목제거를 품셈에 이음
- 뭉친 줄 해석기: 「굴 삭 기 (무한궤도)」를 굴착기(무한궤도)로 맞춰 큰돌쌓기 메쌓기(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
This commit is contained in:
@@ -176,17 +176,21 @@ def build_rows(source: SummaryInput) -> list[SummaryRow]:
|
||||
# 1.9.2 잡관목제거 벌목(5m미만) 11,035㎡ @882 ← 같은 11,035㎡
|
||||
# ⚠⚠ **이중계상이 아니다** — 한 면적에 **다른 두 작업**이 얹히는 것이라 실무가 그렇게 적는다.
|
||||
# (같은 작업을 두 축에서 두 번 세는 것과는 다른 자리다.)
|
||||
# ⚠ 잡관목제거는 **품셈에 그 이름이 없다** — 실무는 별도 단가(영월 D00033)를 씀.
|
||||
# 공종 없는 줄 보류(확정 5차 3번)에 걸리므로 **코드 없이 서고 사유가 붙는다.**
|
||||
# ⭐ 2026-09-13 브레인 판정 — 지장목제거는 **품셈에 있다**(벌목 4장 + 제근 9-20~21 단계 합산).
|
||||
# 잡관목제거 = 단목베기 1,000㎡당 「5m 미만」(4-2-2) · 뿌리뽑기 = 제근(9-21, 판정 Ⓑ).
|
||||
# 준비공 「제근·뿌리다듬기」는 같은 면적이라 **참조로만** 보인다(이중계상 막이).
|
||||
for item, why in (
|
||||
(
|
||||
"뿌리뽑기",
|
||||
"확정 5차 2번 — 실무가 뿌리뽑기·잡관목제거 두 줄로 가름(같은 면적을 나눠 씀 · 이중계상 아님)",
|
||||
"확정 5차 2번 — 실무가 뿌리뽑기·잡관목제거 두 줄로 가름"
|
||||
"(같은 면적을 나눠 씀 · 이중계상 아님). 품셈 9-21 제근으로 이음 —"
|
||||
" 준비공 「제근·뿌리다듬기」는 참조로만 보임(2026-09-13 판정)",
|
||||
),
|
||||
(
|
||||
"잡관목제거",
|
||||
"확정 5차 2번 — 같은 면적에 얹히는 다른 작업(이중계상 아님)."
|
||||
" ⚠ 품셈에 그 이름이 없어 실무는 별도 단가를 씀(영월 D00033) — 공종 보류 대상",
|
||||
" 품셈 4-2-2 단목베기(1,000㎡당) 「5m 미만」으로 이음"
|
||||
"(2026-09-13 판정 · 실무 영월 1.9.2)",
|
||||
),
|
||||
):
|
||||
rows.append(
|
||||
|
||||
@@ -111,6 +111,7 @@ def build_handoff(
|
||||
concrete_placing_method: str | None = None,
|
||||
bench_cut_depth_m: float | None = None,
|
||||
structure_trench_water: str | None = None,
|
||||
stand_volume_class: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""B09 가 그대로 받는 모양. 없는 표는 건너뛰되 **빈 표와 구별해 적는다**."""
|
||||
table = mapping or load_mapping()
|
||||
@@ -119,7 +120,11 @@ def build_handoff(
|
||||
|
||||
methods = {key: value for key, value in (ground_methods or {}).items() if value}
|
||||
if summary_table:
|
||||
rows, misses = _earthwork_rows(summary_table, table, methods, bench_cut_depth_m)
|
||||
# 임목축적 등급 — 지장목제거 뿌리뽑기(9-21 제근)의 품 갈래(2026-09-13 판정 Ⓑ).
|
||||
variant_inputs = {"stand_volume_class": stand_volume_class}
|
||||
rows, misses = _earthwork_rows(
|
||||
summary_table, table, methods, bench_cut_depth_m, variant_inputs
|
||||
)
|
||||
work_items.extend(rows)
|
||||
unmatched.extend(misses)
|
||||
if haul_table:
|
||||
|
||||
@@ -222,17 +222,30 @@ class WorkItemMapping:
|
||||
found[str(code)] = str(unit)
|
||||
return found
|
||||
|
||||
def for_earthwork(self, group: str, ground: str | None) -> dict[str, Any] | None: # noqa: D401
|
||||
"""공종+지반유형으로 찾는다. 지반이 갈리지 않는 공종은 `ground` 없는 줄이 받는다."""
|
||||
def for_earthwork(
|
||||
self, group: str, ground: str | None, item: str | None = None
|
||||
) -> dict[str, Any] | None: # noqa: D401
|
||||
"""공종+지반유형으로 찾는다. 지반이 갈리지 않는 공종은 `ground` 없는 줄이 받는다.
|
||||
|
||||
`item` 은 **작업 갈래**(지장목제거의 「잡관목제거」) — 매핑 줄에 `item` 이 있으면 같아야 함.
|
||||
"""
|
||||
exact = [
|
||||
row
|
||||
for row in self.earthwork
|
||||
if row.get("group") == group and row.get("ground") == ground
|
||||
if row.get("group") == group
|
||||
and row.get("ground") == ground
|
||||
and ("item" not in row or row.get("item") == item)
|
||||
]
|
||||
if exact:
|
||||
return exact[0]
|
||||
# 지반을 안 가르는 공종(성토·층따기 등)은 `ground` 칸이 없는 줄로 맞춘다.
|
||||
loose = [row for row in self.earthwork if row.get("group") == group and "ground" not in row]
|
||||
loose = [
|
||||
row
|
||||
for row in self.earthwork
|
||||
if row.get("group") == group
|
||||
and "ground" not in row
|
||||
and ("item" not in row or row.get("item") == item)
|
||||
]
|
||||
return loose[0] if loose else None
|
||||
|
||||
def for_haul(self, equipment: str) -> dict[str, Any] | None:
|
||||
|
||||
@@ -93,9 +93,12 @@ def _earthwork_rows(
|
||||
mapping: WorkItemMapping,
|
||||
methods: dict[str, str | None],
|
||||
bench_cut_depth_m: float | None = None,
|
||||
variant_inputs: dict[str, str | None] | None = None,
|
||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
"""토공집계표 줄을 내역 줄로 옮긴다.
|
||||
|
||||
`variant_inputs` — 매핑 줄이 `variant_from` 으로 가리키는 **설정값**(임목축적 등급 등).
|
||||
|
||||
⚠ 「보정량계」 같은 합계 줄은 **내역 줄이 아니다** — 빼지 않고 `in_bill: False` 로 넘긴다.
|
||||
빼 버리면 B09 가 검산할 때 합이 안 맞는 까닭을 알 수 없다.
|
||||
"""
|
||||
@@ -115,7 +118,9 @@ def _earthwork_rows(
|
||||
# ⚠ 운반은 **집계에도 오르고 운반표에도 오른다** — 내역 줄은 운반표 쪽 하나뿐이다.
|
||||
is_subtotal = group in SUBTOTAL_GROUPS or group in HAUL_SUMMARY_GROUPS
|
||||
lookup_ground, method_note = _mapping_ground(ground, methods)
|
||||
entry = mapping.for_earthwork(group, lookup_ground) if method_note == "" else None
|
||||
entry = (
|
||||
mapping.for_earthwork(group, lookup_ground, work_kind) if method_note == "" else None
|
||||
)
|
||||
code = (entry or {}).get("work_item_code")
|
||||
# ⚠ 품셈 밑수와 우리 단위가 다른 자리 — **곱하면 금액이 틀린다**(층따기 9-18).
|
||||
# 면적 값을 버리지 않고 `spec_detail` 에 남겨 되짚을 수 있게 한다.
|
||||
@@ -169,9 +174,12 @@ def _earthwork_rows(
|
||||
"spec_detail": spec_detail,
|
||||
"composite_parts": None,
|
||||
"structure_kind": None,
|
||||
# 토공·운반 줄에는 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양).
|
||||
"variant_axis": None,
|
||||
"variant_value": None,
|
||||
# 토공 줄은 대개 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양).
|
||||
# 매핑이 갈래를 적은 작업 갈래(잡관목제거 → 단목베기 「5m 미만」)만 값을 싣는다.
|
||||
"variant_axis": (entry or {}).get("variant_axis"),
|
||||
"variant_value": (entry or {}).get("variant_value")
|
||||
or (variant_inputs or {}).get(str((entry or {}).get("variant_from") or ""))
|
||||
or None,
|
||||
"secondary_axes": None,
|
||||
"spec_class": None,
|
||||
"spec_class_basis": "",
|
||||
|
||||
@@ -134,9 +134,8 @@ def preparation_rows(
|
||||
"대상 면적에 **소단면이 포함**됨(별표2 타.(1) 「노출되는 면은 전체면적 녹화」 — "
|
||||
"원문이 가르지 않고 실무도 한 덩이로 셈). "
|
||||
"토공집계의 「지장목제거」로 이미 섬 — 여기서 또 세우면 이중계상. "
|
||||
"⚠ 다만 **공종 미확정** — 품셈 4장이 벌목을 목적별로 갈라(수확베기·단목베기·"
|
||||
"위험목 베기) 임도 지장목이 어디에 붙는지 원본이 말하지 않음."
|
||||
" 지금은 공종코드 없이 감."
|
||||
"공종은 토공집계 줄이 가짐 — 잡관목제거는 품셈 4-2-2 단목베기(FP-04-02-02)"
|
||||
" 「5m 미만」, 뿌리뽑기는 9-21 제근(FP-09-21)(2026-09-13 판정)."
|
||||
),
|
||||
"work_item_code": None,
|
||||
},
|
||||
@@ -259,6 +258,12 @@ ROOT_REMOVAL_BASIS = (
|
||||
"밑수는 **면적 축**(사용자 확정 5차 6번) — 산림품셈 9-21 에 밑수 표기가 없어"
|
||||
" ⚠ **교차 참조**: 건설공사 표준품셈 **3-9-2 뿌리뽑기 「1,000㎡당」**을 빌려 씀"
|
||||
)
|
||||
#: ⭐ 2026-09-13 판정 Ⓑ — 제근은 **토공 줄**이다(지식DB `토공_수량.md:30` 「뿌리다듬기·적재·제근 |
|
||||
#: 9-20~21 | 벌개제근 연동」). 실무 서식(영월 1.9.1 뿌리뽑기)대로 토공집계가 세고 여기는 참조.
|
||||
ROOT_COUNTED_IN_SUMMARY = (
|
||||
"토공집계 「지장목제거 · 뿌리뽑기」가 품셈 9-21 제근(FP-09-21)으로 셈 — 같은 면적·같은 작업이라"
|
||||
" 여기서 또 세우면 이중계상(2026-09-13 판정)"
|
||||
)
|
||||
ROOT_REMOVAL_CLASS_MISSING = (
|
||||
"임목축적 등급이 아직 입력되지 않았습니다 — 품셈 9-21 [주]① 이 소림(30~60㎥/㏊)·"
|
||||
"중림(60~90)·밀림(90 이상)으로 가름. ⚠ 본수가 아니라 **축적**이고, 산림조사부·영림계획에서"
|
||||
@@ -273,7 +278,11 @@ def _root_removal_row(slope: dict[str, float], stand_volume_class: str | None) -
|
||||
"""
|
||||
area = float(slope.get("tree_removal_fill", 0.0)) + float(slope.get("tree_removal_cut", 0.0))
|
||||
picked = str(stand_volume_class or "").strip()
|
||||
reasons = [ROOT_REMOVAL_BASIS, "대상 면적은 지장목제거와 같은 자리(벌개제근 연동)"]
|
||||
reasons = [
|
||||
ROOT_COUNTED_IN_SUMMARY,
|
||||
ROOT_REMOVAL_BASIS,
|
||||
"대상 면적은 지장목제거와 같은 자리(벌개제근 연동)",
|
||||
]
|
||||
if picked in STAND_VOLUME_CLASSES:
|
||||
reasons.append(f"임목축적 등급 「{picked}」 — 품셈 9-21 [주]① 이 품을 그 축으로 가름")
|
||||
else:
|
||||
@@ -285,10 +294,12 @@ def _root_removal_row(slope: dict[str, float], stand_volume_class: str | None) -
|
||||
"item": "제근·뿌리다듬기",
|
||||
"unit": "㎡",
|
||||
"amount": area if area > 0 else None,
|
||||
"status": STATUS_READY if area > 0 else STATUS_PENDING,
|
||||
# ⭐ 2026-09-13 판정 Ⓑ — 셈은 토공집계 「지장목제거 · 뿌리뽑기」(FP-09-21)가 한다.
|
||||
# 여기는 **보이되 안 실린다**(같은 면적 · 같은 작업 — 또 세면 이중계상).
|
||||
"status": STATUS_COUNTED_ELSEWHERE if area > 0 else STATUS_PENDING,
|
||||
"reason": " · ".join(reasons),
|
||||
"reference_amount": area,
|
||||
"work_item_code": "FP-09-21",
|
||||
"work_item_code": None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -295,6 +295,8 @@ async def get_handoff(project_id: UUID) -> JSONResponse:
|
||||
bench_cut_depth_m=settings.get("bench_cut_depth_m"),
|
||||
# 용수 유무 — 기본 「육상」은 **통상값**이다(확정 3차 ④). 사유·화면에 그 사실이 뜬다.
|
||||
structure_trench_water=settings.get("structure_trench_water"),
|
||||
# 임목축적 등급 — 지장목제거 뿌리뽑기(제근 9-21)의 품 갈래. 안 넣으면 B09 가 후보를 보임.
|
||||
stand_volume_class=settings.get("stand_volume_class"),
|
||||
)
|
||||
handoff["summary"] = summarize(handoff)
|
||||
handoff["skipped_structures"] = skipped
|
||||
|
||||
Reference in New Issue
Block a user