feat(b09): 9-19-1 토사면 고르기 두 표를 갈래로 읽음 — 절토면 토질 6 · 성토면 인력 2(기계 굴착기 0.6㎥ 는 무한궤도·타이어 둘이라 규격 미정) · [주]① 규격은 범위 별칭 · 옆 칸이 규격을 적으면 별칭이 안 덮음 · 성토면 밀린 줄은 사람 판정표로만 · [주]②③ 는 비고(브레인 판정 Ⓐ~Ⓔ)
섞인 본표 B-FP-09-19-01(보통인부 한 줄 2,925원) 걷힘 · 일위대가 나머지 452 제목 금액 변화 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -44,13 +44,13 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
|
|||||||
"ⓘ 커플링밴드는 값이 표에 없지만 [주]① 이 「**1EA/6m**」를 줍니다 — 자재가 서면 "
|
"ⓘ 커플링밴드는 값이 표에 없지만 [주]① 이 「**1EA/6m**」를 줍니다 — 자재가 서면 "
|
||||||
"m 당 1/6 EA 로 셀 수 있습니다(다만 「필요시 별도 산정」이라 조건이 하나 더 붙습니다).",
|
"m 당 1/6 EA 로 셀 수 있습니다(다만 「필요시 별도 산정」이라 조건이 하나 더 붙습니다).",
|
||||||
),
|
),
|
||||||
# 2026-09-14 브레인 — 조용히 틀린 값이 이미 서 있어 **표 모양 본작업보다 사유를 먼저** 닮.
|
# 2026-09-14 ⑤ 판정 Ⓓ·Ⓔ — 두 표를 갈래로 읽은 뒤 남는 [주] 둘. 값을 짓지 않고 말만 둠.
|
||||||
"FP-09-19-01": (
|
"FP-09-19-01": (
|
||||||
"값 불완전",
|
"원문 [주]",
|
||||||
"⚠ 이 값은 **불완전**합니다 — 원문 9-19-1 은 표 둘"
|
"ⓘ 절토면 암 갈래(연암·보통암ㆍ경암): [주]③ 「소형브레이커 조작 인력품은 착암공으로 한다」"
|
||||||
"(① 절토면 고르기: 토질 6갈래 × 보통인부·공기압축기·소형브레이커·굴착기, 10㎡당"
|
" — 표에 착암공 수량이 없어 줄을 안 세움(보통인부는 표 그대로). "
|
||||||
" · ② 성토면 고르기: 인력 2갈래·기계 1갈래)인데 지금은 두 표가 섞여 보통인부 한 줄만"
|
"ⓘ 절토면 풍화암: [주]② 「소형브레이커를 사용할 시는 연암 고르기 품을 **준용할 수 있다**」"
|
||||||
" 섰습니다. 토질·절토/성토 갈래 읽기 전에는 이 값을 쓰지 말 것.",
|
" — 선택지라 따로 갈래를 안 만듦(브레이커를 쓰면 연암 갈래를 고를 것).",
|
||||||
),
|
),
|
||||||
"FP-12-25": (
|
"FP-12-25": (
|
||||||
"운반거리 미정",
|
"운반거리 미정",
|
||||||
|
|||||||
@@ -440,6 +440,11 @@ def match_table(
|
|||||||
if match_unit_basis_table(node, table, catalog, result):
|
if match_unit_basis_table(node, table, catalog, result):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 사람이 모양을 읽어 둔 표(9-19-1 절토면·성토면 · 2026-09-14 판정) — 적어 둔 표만.
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis_JudgedTable import match_judged_table
|
||||||
|
if match_judged_table(node, table, catalog, result, basis_quantity, unit):
|
||||||
|
return
|
||||||
|
|
||||||
# ⚠ **자원이 열 머리에 오는 표가 따로 있다** (2026-09-08 발견, 39 표).
|
# ⚠ **자원이 열 머리에 오는 표가 따로 있다** (2026-09-08 발견, 39 표).
|
||||||
# 「구 분 | 콘크리트공(인) | 보통인부(인)」처럼 **열이 자원**이고 행은 규격 갈래
|
# 「구 분 | 콘크리트공(인) | 보통인부(인)」처럼 **열이 자원**이고 행은 규격 갈래
|
||||||
# (무근·철근·소형구조물)다. 행을 자원으로 읽는 길로 보내면 통째로 못 맞춘다 —
|
# (무근·철근·소형구조물)다. 행을 자원으로 읽는 길로 보내면 통째로 못 맞춘다 —
|
||||||
@@ -575,8 +580,8 @@ def match_table(
|
|||||||
# 지우고 있었음). 카탈로그에 있는 이름은 **정의상 자원**이다.
|
# 지우고 있었음). 카탈로그에 있는 이름은 **정의상 자원**이다.
|
||||||
# 범위 별칭 — 그 공종 범위 안에서만 카탈로그 쪽 이름으로 바꾼다(「화약공」 → 화약취급공).
|
# 범위 별칭 — 그 공종 범위 안에서만 카탈로그 쪽 이름으로 바꾼다(「화약공」 → 화약취급공).
|
||||||
# 별칭이 코드를 적었으면 그 항목으로 곧장(규격이 조인 키인 기계가 다시 흐려지지 않게).
|
# 별칭이 코드를 적었으면 그 항목으로 곧장(규격이 조인 키인 기계가 다시 흐려지지 않게).
|
||||||
entry = scoped_alias_entry(catalog, name_cell, node["work_item_code"])
|
entry = scoped_alias_entry(catalog, name_cell, node["work_item_code"], value_cells)
|
||||||
name_cell = apply_scoped_alias(catalog, name_cell, node["work_item_code"])
|
name_cell = apply_scoped_alias(catalog, name_cell, node["work_item_code"], value_cells)
|
||||||
entry = entry or _resolve_cell(catalog, name_cell, [name_cell, *value_cells])
|
entry = entry or _resolve_cell(catalog, name_cell, [name_cell, *value_cells])
|
||||||
if entry is None:
|
if entry is None:
|
||||||
if is_non_resource_label(name_cell):
|
if is_non_resource_label(name_cell):
|
||||||
|
|||||||
@@ -107,14 +107,30 @@ def load_scoped_aliases() -> list[dict[str, str]]:
|
|||||||
return load_aliases("resource")
|
return load_aliases("resource")
|
||||||
|
|
||||||
|
|
||||||
|
#: 옆 칸 규격 — 「0.6㎥」·「10.3㎥/min」. 값「0.09」·식「(2.45+3.05)/2/10㎡」·단위「hr」 는 아님.
|
||||||
|
_RE_SIDE_SPEC = re.compile(r"^\d+(?:\.\d+)?(?:㎥|m3|㎥/min|㎥/분|ton|톤|㎾|kW|㎜|mm|㎝|cm|인치)$")
|
||||||
|
|
||||||
|
|
||||||
|
def _writes_spec(side_cells: list[str] | tuple[str, ...]) -> bool:
|
||||||
|
"""옆 칸이 규격을 적었나 — 이름이 같아도 별칭이 그 규격을 덮으면 안 됨."""
|
||||||
|
return any(_RE_SIDE_SPEC.match(_normalize(cell)) for cell in side_cells)
|
||||||
|
|
||||||
|
|
||||||
def scoped_alias_entry(
|
def scoped_alias_entry(
|
||||||
catalog: ResourceCatalog, name_cell: str, work_item_code: str
|
catalog: ResourceCatalog,
|
||||||
|
name_cell: str,
|
||||||
|
work_item_code: str,
|
||||||
|
side_cells: list[str] | tuple[str, ...] = (),
|
||||||
) -> CatalogEntry | None:
|
) -> CatalogEntry | None:
|
||||||
"""범위 별칭이 가리키는 **항목 그 자체**(코드로 곧장) — 없으면 `None`.
|
"""범위 별칭이 가리키는 **항목 그 자체**(코드로 곧장) — 없으면 `None`.
|
||||||
|
|
||||||
⚠ 이름만 돌려주면 규격이 조인 키인 기계(소형브레이커 공압식 넷)는 다시 「규격 미정」 으로
|
⚠ 이름만 돌려주면 규격이 조인 키인 기계(소형브레이커 공압식 넷)는 다시 「규격 미정」 으로
|
||||||
흐려짐 — 별칭이 코드를 적었으면 그 코드가 곧 답(2026-09-14 · 9-19-3 소형브레이커).
|
흐려짐 — 별칭이 코드를 적었으면 그 코드가 곧 답(2026-09-14 · 9-19-3 소형브레이커).
|
||||||
|
⚠ 별칭은 **빈 곳을 채움** — 옆 칸이 규격을 적었으면 안 덮음(같은 9-19-1 성토면 「굴착기 |
|
||||||
|
0.6㎥」 가 절토면 [주]① 0.7 로 덮이던 자리 · 2026-09-14 판정 Ⓐ).
|
||||||
"""
|
"""
|
||||||
|
if _writes_spec(side_cells):
|
||||||
|
return None
|
||||||
wanted = _normalize(name_cell)
|
wanted = _normalize(name_cell)
|
||||||
for row in catalog.scoped_aliases:
|
for row in catalog.scoped_aliases:
|
||||||
if _normalize(row["from"]) != wanted or not in_scope(work_item_code, row["scope"]):
|
if _normalize(row["from"]) != wanted or not in_scope(work_item_code, row["scope"]):
|
||||||
@@ -123,8 +139,15 @@ def scoped_alias_entry(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def apply_scoped_alias(catalog: ResourceCatalog, name_cell: str, work_item_code: str) -> str:
|
def apply_scoped_alias(
|
||||||
|
catalog: ResourceCatalog,
|
||||||
|
name_cell: str,
|
||||||
|
work_item_code: str,
|
||||||
|
side_cells: list[str] | tuple[str, ...] = (),
|
||||||
|
) -> str:
|
||||||
"""범위 안이면 카탈로그 쪽 **이름**으로 바꾼다. 범위 밖이거나 대상 코드가 없으면 원문 그대로."""
|
"""범위 안이면 카탈로그 쪽 **이름**으로 바꾼다. 범위 밖이거나 대상 코드가 없으면 원문 그대로."""
|
||||||
|
if _writes_spec(side_cells):
|
||||||
|
return name_cell # 이름만 갈면 옆 칸 규격으로 대상 계열(무한궤도)이 조용히 골라짐
|
||||||
wanted = _normalize(name_cell)
|
wanted = _normalize(name_cell)
|
||||||
for row in catalog.scoped_aliases:
|
for row in catalog.scoped_aliases:
|
||||||
if _normalize(row["from"]) != wanted or not in_scope(work_item_code, row["scope"]):
|
if _normalize(row["from"]) != wanted or not in_scope(work_item_code, row["scope"]):
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
"""B09 원가계산 — **사람이 모양을 읽어 둔 표** (자원 축 보조 · 2026-09-14 ⑤ 브레인 판정 Ⓐ~Ⓔ).
|
||||||
|
|
||||||
|
9-19-1 토사면 고르기(고시 2025-82 원문 L5407~5436)는 한 절에 표가 둘이고 둘 다 일반 길에 안 맞음.
|
||||||
|
|
||||||
|
F0288 1. 절토면 고르기 자원 이름이 **첫 자료 줄**(「보통인부 (인)」 …) · 규격은 [주]① 에만
|
||||||
|
· 「·」 = 그 토질엔 그 자원 없음
|
||||||
|
F0289 2. 성토면 고르기 병합 첫 칸(시공) 탓에 둘째 줄이 한 칸 앞당겨 옴
|
||||||
|
|
||||||
|
⚠ 일반 보정으로 넓히지 않음 — 밀린 줄 고쳐 읽기는 목재틀흙막이 503만원 전례(`_Transposed` 머리말).
|
||||||
|
**적어 둔 표만** 읽고, 칸이 판정과 다르면 한 줄도 안 세우고 막음.
|
||||||
|
⚠ 규격은 범위 별칭(`data_aliases` scope FP-09-19-01 · [주]①)이 코드로 이음 — 칸이 규격을 적었으면
|
||||||
|
별칭이 안 덮음(`scoped_alias_entry`). 성토면 굴착기 0.6㎥ 는 무한궤도·타이어 둘이라 규격 미정(Ⓑ).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis import (
|
||||||
|
AxisResult,
|
||||||
|
ResourceCatalog,
|
||||||
|
ResourceRow,
|
||||||
|
UnmatchedRow,
|
||||||
|
_resolve_cell,
|
||||||
|
parse_amount,
|
||||||
|
)
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis_Join import scoped_alias_entry, unmatched_reason
|
||||||
|
|
||||||
|
#: 표 번호 → (공종 · 모양 · 갈래 앞말 · 판정 근거). 여기 없는 표는 이 길로 안 읽음.
|
||||||
|
JUDGED_TABLES: dict[str, dict[str, Any]] = {
|
||||||
|
"F0288": {
|
||||||
|
"code": "FP-09-19-01",
|
||||||
|
"shape": "header_row",
|
||||||
|
"prefix": "절토면",
|
||||||
|
"why": "원문 L5414 「1. 절토면 고르기 (단위: 10㎡당)」 — 자원 머리가 첫 자료 줄",
|
||||||
|
},
|
||||||
|
"F0289": {
|
||||||
|
"code": "FP-09-19-01",
|
||||||
|
"shape": "merged_first",
|
||||||
|
"prefix": "성토면",
|
||||||
|
# 원문 L5430 표 — 「인력시공」 칸이 두 줄 병합이라 둘째 줄(모래 또는 사질토)이 앞당겨 옴.
|
||||||
|
"merged_rows": (1,),
|
||||||
|
"why": "원문 L5430 「2. 성토면 고르기 (단위: 10㎡당)」 — 시공 칸 병합으로 둘째 줄 앞당김",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#: 「보통인부 (인)」 의 단위 꼬리 — 규격이 아님.
|
||||||
|
_UNIT_TAIL = re.compile(r"\s*[((](?:인|시간)[))]\s*$")
|
||||||
|
_ABSENT = frozenset({"·", "ㆍ", "-", "-", "–"})
|
||||||
|
|
||||||
|
|
||||||
|
def _entry(catalog: ResourceCatalog, name_cell: str, code: str, side: tuple[str, ...] = ()):
|
||||||
|
return scoped_alias_entry(catalog, name_cell, code, side) or _resolve_cell(
|
||||||
|
catalog, name_cell, [name_cell, *side]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _row(
|
||||||
|
code: str, table: dict[str, Any], entry, amount: Decimal, unit: str, index: int, variant: str
|
||||||
|
):
|
||||||
|
return ResourceRow(
|
||||||
|
work_item_code=code,
|
||||||
|
pum_table_id=str(table.get("pum_table_id", "")),
|
||||||
|
pum_form=str(table.get("pum_form", "")),
|
||||||
|
resource_kind=entry.kind,
|
||||||
|
resource_code=entry.code,
|
||||||
|
resource_name=entry.name,
|
||||||
|
resource_spec=entry.spec,
|
||||||
|
amount=amount,
|
||||||
|
amount_unit=unit,
|
||||||
|
raw_row_index=index,
|
||||||
|
variant=variant,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def match_judged_table(
|
||||||
|
node: dict[str, Any],
|
||||||
|
table: dict[str, Any],
|
||||||
|
catalog: ResourceCatalog,
|
||||||
|
result: AxisResult,
|
||||||
|
basis_quantity: Decimal | None,
|
||||||
|
unit: str,
|
||||||
|
) -> bool:
|
||||||
|
"""적어 둔 표를 판정대로 읽음. 그 표가 아니면 `False`."""
|
||||||
|
code = str(node.get("work_item_code") or "")
|
||||||
|
table_id = str(table.get("pum_table_id", ""))
|
||||||
|
judged = JUDGED_TABLES.get(table_id)
|
||||||
|
if judged is None or judged["code"] != code:
|
||||||
|
return False
|
||||||
|
rows = [[str(c).strip() for c in row] for row in (table.get("raw_row") or [])]
|
||||||
|
|
||||||
|
def block(why: str) -> bool:
|
||||||
|
result.unmatched.append(UnmatchedRow(code, table_id, judged["prefix"], why))
|
||||||
|
result.partial_items[code] = why
|
||||||
|
return True
|
||||||
|
|
||||||
|
if basis_quantity in (None, 0):
|
||||||
|
return block("판정 표에 밑수가 없습니다")
|
||||||
|
if judged["shape"] == "header_row":
|
||||||
|
staged = _header_row(code, table, judged, rows, catalog, basis_quantity, unit)
|
||||||
|
else:
|
||||||
|
staged = _merged_first(code, table, judged, rows, catalog, basis_quantity, unit)
|
||||||
|
if isinstance(staged, str):
|
||||||
|
return block(f"{staged} — 판정({judged['why']})과 칸이 달라 안 읽음")
|
||||||
|
for item in staged:
|
||||||
|
if isinstance(item, UnmatchedRow):
|
||||||
|
result.unmatched.append(item)
|
||||||
|
else:
|
||||||
|
result.rows.append(item)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _header_row(code, table, judged, rows, catalog, basis, unit) -> list | str:
|
||||||
|
"""첫 줄 = 자원 머리 · 이후 줄 = [토질, 값 …]. 머리 자원을 하나라도 못 풀면 표째 막음."""
|
||||||
|
names = [_UNIT_TAIL.sub("", cell) for cell in (rows[0] if rows else []) if cell]
|
||||||
|
entries = [_entry(catalog, name, code) for name in names]
|
||||||
|
if len(names) < 2 or not all(entries):
|
||||||
|
missing = [name for name, entry in zip(names, entries) if entry is None]
|
||||||
|
return f"자원 머리 {missing or names} 을 못 풂"
|
||||||
|
staged: list = []
|
||||||
|
for index, cells in enumerate(rows[1:], start=1):
|
||||||
|
values, tail = cells[1 : 1 + len(names)], cells[1 + len(names) :]
|
||||||
|
readable = all(v in _ABSENT or parse_amount(v) is not None for v in values)
|
||||||
|
if not cells[0] or len(values) != len(names) or any(tail) or not readable:
|
||||||
|
return f"{index}째 줄 「{cells[0] if cells else ''}」 값 칸"
|
||||||
|
variant = f"{judged['prefix']} · {cells[0]}"
|
||||||
|
for value, entry in zip(values, entries):
|
||||||
|
if value in _ABSENT:
|
||||||
|
continue # 그 토질엔 이 자원이 안 듦
|
||||||
|
staged.append(
|
||||||
|
_row(code, table, entry, parse_amount(value) / basis, unit, index, variant)
|
||||||
|
)
|
||||||
|
return staged
|
||||||
|
|
||||||
|
|
||||||
|
def _merged_first(code, table, judged, rows, catalog, basis, unit) -> list | str:
|
||||||
|
"""[시공, 토질, 구분, 규격, 단위, 수량] — 병합 줄은 앞 줄 시공을 이어받아 한 칸 되돌림."""
|
||||||
|
width = len(table.get("condition_note") or [])
|
||||||
|
if width != 6:
|
||||||
|
return f"머리 {width} 칸"
|
||||||
|
lines: list[list[str]] = []
|
||||||
|
for index, cells in enumerate(rows):
|
||||||
|
if index in judged["merged_rows"]:
|
||||||
|
if not lines or cells[-1]:
|
||||||
|
return f"{index}째 줄 병합 칸"
|
||||||
|
cells = [lines[-1][0], *cells[:-1]]
|
||||||
|
lines.append(cells)
|
||||||
|
methods = [cells[0] for cells in lines]
|
||||||
|
staged: list = []
|
||||||
|
for index, cells in enumerate(lines):
|
||||||
|
if len(cells) != width:
|
||||||
|
return f"{index}째 줄 칸 {len(cells)}"
|
||||||
|
method, soil, name, size, _unit_word, value = cells
|
||||||
|
amount = parse_amount(value)
|
||||||
|
if not method or not name or amount is None:
|
||||||
|
return f"{index}째 줄 「{method}」 값 칸"
|
||||||
|
short = method.removesuffix("시공")
|
||||||
|
variant = " · ".join(
|
||||||
|
[judged["prefix"], short, *([soil] if methods.count(method) > 1 else [])]
|
||||||
|
)
|
||||||
|
entry = _entry(catalog, name, code, (size,) if size else ())
|
||||||
|
if entry is None:
|
||||||
|
# 그 갈래만 안 섬(자원이 하나뿐) — 사유는 못 맞춘 줄로(Ⓑ 굴착기 0.6㎥ 형식 둘).
|
||||||
|
label = f"{name} {size}".strip()
|
||||||
|
reason = unmatched_reason(catalog, name)
|
||||||
|
staged.append(UnmatchedRow(code, str(table.get("pum_table_id", "")), label, reason))
|
||||||
|
continue
|
||||||
|
staged.append(_row(code, table, entry, amount / basis, unit, index, variant))
|
||||||
|
return staged
|
||||||
@@ -19,6 +19,30 @@
|
|||||||
"pum_edition": "2026-01-01",
|
"pum_edition": "2026-01-01",
|
||||||
"basis": "9-19-3 비탈면 고르기(발파암) 표가 규격을 안 적음 — 그 값 「(2.45+3.05)/2/10㎡」 은 9-19-1 토사면 고르기 표(연암 2.45·보통암+경암 3.05)에서 왔고 9-19-1 [주]① 「소형브레이커는 1㎥/분」 → 소형브레이커(공압식) 1.0㎥/min(9-19-3 ← 9-19-1 표 ← [주]①, 값의 출처가 규격을 정함). 2026-09-14 브레인 승인"
|
"basis": "9-19-3 비탈면 고르기(발파암) 표가 규격을 안 적음 — 그 값 「(2.45+3.05)/2/10㎡」 은 9-19-1 토사면 고르기 표(연암 2.45·보통암+경암 3.05)에서 왔고 9-19-1 [주]① 「소형브레이커는 1㎥/분」 → 소형브레이커(공압식) 1.0㎥/min(9-19-3 ← 9-19-1 표 ← [주]①, 값의 출처가 규격을 정함). 2026-09-14 브레인 승인"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"axis": "resource",
|
||||||
|
"from": "공기압축기",
|
||||||
|
"to": "5205-0035",
|
||||||
|
"scope": "FP-09-19-01",
|
||||||
|
"pum_edition": "2026-01-01",
|
||||||
|
"basis": "9-19-1 토사면 고르기 절토면 표(원문 L5414)가 자원 머리에 규격을 안 적음 — [주]① 「공기압축기는 3.5㎥/분, 소형브레이커는 1㎥/분, 굴착기는 0.7㎥를 기준한 것이다」 → 공기압축기(이동식) 3.5. 칸이 규격을 적은 줄(성토면 굴착기 0.6㎥)은 안 덮음. 2026-09-14 브레인 승인(⑤ Ⓐ)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"axis": "resource",
|
||||||
|
"from": "소형브레이커",
|
||||||
|
"to": "5210-0010",
|
||||||
|
"scope": "FP-09-19-01",
|
||||||
|
"pum_edition": "2026-01-01",
|
||||||
|
"basis": "9-19-1 토사면 고르기 절토면 표(원문 L5414)가 자원 머리에 규격을 안 적음 — [주]① 「공기압축기는 3.5㎥/분, 소형브레이커는 1㎥/분, 굴착기는 0.7㎥를 기준한 것이다」 → 소형브레이커(공압식) 1.0㎥/min. 칸이 규격을 적은 줄(성토면 굴착기 0.6㎥)은 안 덮음. 2026-09-14 브레인 승인(⑤ Ⓐ)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"axis": "resource",
|
||||||
|
"from": "굴착기",
|
||||||
|
"to": "0201-0070",
|
||||||
|
"scope": "FP-09-19-01",
|
||||||
|
"pum_edition": "2026-01-01",
|
||||||
|
"basis": "9-19-1 토사면 고르기 절토면 표(원문 L5414)가 자원 머리에 규격을 안 적음 — [주]① 「공기압축기는 3.5㎥/분, 소형브레이커는 1㎥/분, 굴착기는 0.7㎥를 기준한 것이다」 → 굴착기(무한궤도) 0.7(카탈로그 0.7 은 무한궤도뿐). 칸이 규격을 적은 줄(성토면 굴착기 0.6㎥)은 안 덮음. 2026-09-14 브레인 승인(⑤ Ⓐ)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"axis": "variant",
|
"axis": "variant",
|
||||||
"from": "리핑암",
|
"from": "리핑암",
|
||||||
|
|||||||
@@ -61,13 +61,85 @@ def test_비탈면_고르기_발파암_소형브레이커는_1_0_규격으로_
|
|||||||
|
|
||||||
|
|
||||||
def test_토사면_고르기는_두_표가_섞여_값이_불완전하다고_먼저_말함() -> None:
|
def test_토사면_고르기는_두_표가_섞여_값이_불완전하다고_먼저_말함() -> None:
|
||||||
"""⑤ 사유 먼저(2026-09-14 브레인) — 원문은 절토면·성토면 두 표인데 지금은 섞여 보통인부 한 줄만
|
"""⑤ 사유 먼저(2026-09-14 브레인) — 원문은 절토면·성토면 두 표인데 섞여 보통인부 한 줄만
|
||||||
섬(조용히 틀린 값). 표 모양 본작업 전에 누가 그 값을 믿고 쓰지 않게 비고·본표에 뜸."""
|
섰었음(조용히 틀린 값). ⑤ 본작업(같은 날)으로 두 표를 갈래로 읽어 「불완전」 사유와 섞인
|
||||||
|
본표(B-FP-09-19-01)가 함께 걷힘 — 갈래 없는 섞인 값이 다시 서면 빨강."""
|
||||||
|
assert "불완전" not in known_gap_note("FP-09-19-01")
|
||||||
|
assert "B-FP-09-19-01" not in cached_build().book.titles
|
||||||
|
|
||||||
|
|
||||||
|
#: ⑤ 9-19-1 원문 L5414~5436 — (단위: 10㎡당)이라 ㎡당 = 표값 ÷ 10.
|
||||||
|
CUT = "B-FP-09-19-01#절토면·" # 갈래 키는 공백을 지움
|
||||||
|
FILL = "B-FP-09-19-01#성토면·"
|
||||||
|
|
||||||
|
|
||||||
|
def _per_m2(code: str) -> dict[str, float]:
|
||||||
|
"""자원 줄(비율 줄 「%」 뺌) → ㎡당 수량."""
|
||||||
from B09_Estimation.B09_Estimation_UnitPrice import detail_of
|
from B09_Estimation.B09_Estimation_UnitPrice import detail_of
|
||||||
|
|
||||||
|
rows = detail_of(cached_build(), code)["rows"]
|
||||||
|
return {str(r["ref_code"]): float(r["quantity"]) for r in rows if r.get("unit") != "%"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_토사면_고르기_절토면은_토질_여섯_갈래로_규격까지_이어_섬() -> None:
|
||||||
|
"""[주]① 공기압축기 3.5㎥/분 · 소형브레이커 1㎥/분 · 굴착기 0.7㎥ — 범위 별칭이 코드로 이음
|
||||||
|
(카탈로그 0.7 은 무한궤도뿐). 「·」 는 그 갈래에 없음(칸 자리만 지킴)."""
|
||||||
|
titles = cached_build().book.titles
|
||||||
|
cut = sorted(code for code in titles if code.startswith(CUT))
|
||||||
|
assert len(cut) == 6, cut
|
||||||
|
sand = _per_m2(CUT + "모래ㆍ사질토ㆍ점토ㆍ점질토")
|
||||||
|
assert sand == {"1002": 0.005, "X-0201-0070": 0.015}, sand
|
||||||
|
soft_rock = _per_m2(CUT + "연암")
|
||||||
|
assert soft_rock == {"1002": 0.046, "X-5210-0010": 0.245}, soft_rock
|
||||||
|
# 공기압축기 3.5 는 코드까지 이어지나 기계 단가 층(손료) 미확보 — 9-19-3 과 같은 종전 자리
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis import (
|
||||||
|
build_resource_axis,
|
||||||
|
load_combined_catalog,
|
||||||
|
)
|
||||||
|
|
||||||
|
axis = build_resource_axis(load_work_item_master(), load_combined_catalog())
|
||||||
|
rock = {
|
||||||
|
(r.resource_code, float(r.amount))
|
||||||
|
for r in axis.rows
|
||||||
|
if r.work_item_code == "FP-09-19-01" and r.variant == "절토면 · 연암"
|
||||||
|
}
|
||||||
|
assert rock == {("1002", 0.046), ("5205-0035", 0.125), ("5210-0010", 0.245)}, rock
|
||||||
|
|
||||||
|
|
||||||
|
def test_토사면_고르기_성토면_인력_두_갈래_기계는_규격_미정으로_막힘() -> None:
|
||||||
|
"""Ⓒ 둘째 줄이 병합 칸 탓에 한 칸 밀려 옴 — 사람 판정표로만 읽음(일반 보정 금지 · 목재틀흙막이
|
||||||
|
503만원 전례). Ⓑ 굴착기 0.6㎥ 는 무한궤도·타이어 둘이라 안 고름 — 그 갈래는 안 섬."""
|
||||||
|
build = cached_build()
|
||||||
|
assert _per_m2(FILL + "인력·점토또는점질토") == {"1002": 0.019}
|
||||||
|
assert _per_m2(FILL + "인력·모래또는사질토") == {"1002": 0.017}
|
||||||
|
assert not [code for code in build.book.titles if code.startswith(FILL + "기계")]
|
||||||
|
left = build.unattached.get("FP-09-19-01", [])
|
||||||
|
assert any("굴착기" in label for label in left), left
|
||||||
|
|
||||||
|
|
||||||
|
def test_범위_별칭은_칸이_규격을_적었으면_덮지_않음() -> None:
|
||||||
|
"""Ⓐ 별칭은 빈 곳을 채우는 것 — 같은 공종 성토면 「굴착기 0.6㎥」 가 0.7 로 덮이면 안 됨."""
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis import load_combined_catalog
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis_Join import (
|
||||||
|
apply_scoped_alias,
|
||||||
|
scoped_alias_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
catalog = load_combined_catalog()
|
||||||
|
assert scoped_alias_entry(catalog, "굴착기", "FP-09-19-01").code == "0201-0070"
|
||||||
|
side = ["0.6㎥", "시간", "0.09"]
|
||||||
|
assert scoped_alias_entry(catalog, "굴착기", "FP-09-19-01", side) is None
|
||||||
|
# 이름만 갈아도 옆 칸 0.6 으로 무한궤도가 조용히 골라짐 — 이름도 그대로
|
||||||
|
assert apply_scoped_alias(catalog, "굴착기", "FP-09-19-01", side) == "굴착기"
|
||||||
|
# 값·식 칸은 규격이 아님 — 9-19-3 소형브레이커 줄(값 0.275 · 식)은 그대로 이음
|
||||||
|
value = ["hr", "0.275", "(2.45+3.05)/2/10㎡"]
|
||||||
|
assert scoped_alias_entry(catalog, "소형브레이커", "FP-09-19-03", value).code == "5210-0010"
|
||||||
|
|
||||||
|
|
||||||
|
def test_토사면_고르기_원문_주는_값을_안_짓고_비고로() -> None:
|
||||||
|
"""Ⓓ [주]③ 착암공은 수량이 없어 줄을 안 지음 · Ⓔ [주]② 풍화암 연암 준용은 선택이라 갈래 아님."""
|
||||||
note = known_gap_note("FP-09-19-01")
|
note = known_gap_note("FP-09-19-01")
|
||||||
assert "절토면" in note and "성토면" in note and "불완전" in note
|
assert "착암공" in note and "준용" in note, note
|
||||||
assert "불완전" in detail_of(cached_build(), "B-FP-09-19-01")["known_gap_note"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_제근은_빌린_밑수가_확정이라_갈래가_서면_내역_금액이_섬() -> None:
|
def test_제근은_빌린_밑수가_확정이라_갈래가_서면_내역_금액이_섬() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user