feat(b08): 건설 표 모양 판정층 — 미판정 891 → 276 · 건설 밑수 미판독 184 → 90 · 산림 0 그대로

브레인 일감. 표 모양 판정이 산림 표에 맞춰져 있어 건설 표를 못 가렸음(건설 미판정 891 · 산림 0).

- `B08_Quantity_Build_WorkItemMaster_FormsConst.py` — **산림 잣대가 미판정으로 떨어질 때만** 도는 건설 층
  · 기계 손료표(머리 「분류번호」+「상각 비율」) 122 · 율·계수표(손율·요율·보정계수) · 값 없는 설명·서식표 ·
    건설 직종(특급·고급·중급·초급 기술자 · 지적기사 · 비계공 …) · 줄에 단위 칸+값 · 값 자리 계산식 ·
    줄 이름이 공식 기호뿐(β·E) · 값이 모두 백분율
  · ⚠ 비고 칸의 산출식은 값으로 안 봄 — 인력품 표가 계수로 뒤집히던 자리(9-5-4 수치지도)
  · ⚠ 꼬리 단위는 품·자재 단위만 값으로 셈 — `24hr`·`30톤`·`0.166일` 은 규격·기간 표기
  · ⚠ **못 가리면 미판정으로 남김**(276) — 지어내 채우지 않음
- 밑수 읽기 넓힘(`_Table.SOURCE_BASIS_RE`) — 건설은 절 제목 끝에 밑수를 적음
  · `(용접개소당)`·`(세면기 개당)`·`(절단길이 10m당)`·`(m당 : 관길이기준)`·`(일당)`·`(기당)` 을 읽음
  · 단위에 일·기·조·명·틀 더함 · ⚠ 「회」는 넣지 않음(2026-09-17 서브 판정)
  · ⚠ `(1ha당, 100본당)` 처럼 **밑수가 둘이면 안 고름** · `공㎥당` 이 `㎥` 로 깎이던 것 고침
  · ⚠ **작업조 표에는 「일」 밑수를 안 붙임**(c3da333e — 붙이면 유로폼 호표가 사라짐)
  · 건설 밑수 확보 537 → 639 · 미판독 184 → 90 · 산림 확보 251 → 253(원문에 있던 2-2-4 ha당 등)
- 브레인 ①[주] 17 중 3 — `md_notes` 이어짐에 「- 4. 고철공제 : A=…」 꼴(콜론·식) 과 중첩 [주] 를 더함
  · 남은 14 는 마스터가 뒤 표의 [주]까지 붙여 둔 자리 — 「애매하면 안 깎는 쪽」으로 까닭만 달아 둠
- 브레인 ②밑수 5 중 3 — `(단위 : ㎥)`(당 없음) · 줄 칸의 `(160ha당)` · 설명 글이 앞에 붙은 `(…, ha당)` 읽음
  · F0322·F0323 은 원문 밑수가 표 둘 아래 산출식에만 있어 사람 판정 유지
-  금액 불변 — 직접공사비 5,983,724 · 합계 10,124,000 그대로
- `test_form_const.py` 22 — 산림이 건설 층에 안 닿음 · 꼴 여섯 가름 · 못 가리면 미판정 · 밑수 아홉 꼴
- 곁가지(받은 md 손질이 부른 것)
  · 8-11 이동식 임목파쇄기 — 랩탑 서브가 8-4 를 줄마다 풀자 **연료 칸이 「-」 인 빈 레코드**가 생겨 산림
    8-11 [주]⑤(13.5ℓ·16%) 층이 통째로 빠졌음 → 「연료가 비면 산림 [주]가 정본」으로 두 자리 고침
  · 기계 벌 — 중유 기종(`중유487.2`) 을 경유로 적던 것 · 연료가 둘인 칸(`73.7+휘발유54.5`) 을 반쪽만 싣던 것 ·
    원문에 없는 기종을 옛 벌에서 이어받던 것 고침(연료 274)
  · ⚠ 이어받기는 **원문에 줄이 있는데 연료를 못 읽은 코드**까지 — 못 읽은 줄 + 주연료 칸이 수가 아닌 줄
    (양수기 7993-0020 은 원문이 규격을 연료 칸에 밀어 적음 `1.49㎾` · 사람이 맞춘 1.3ℓ 를 지킴)
  · 이름표 줄 수·건설 공종 축 셈·열쇠 지문 갱신(까닭을 파일에 적음)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-18 09:07:49 +09:00
co-authored by Claude Opus 5
parent 137f329b59
commit 83220dc231
22 changed files with 15587 additions and 24783 deletions
@@ -0,0 +1,126 @@
"""공종 마스터 — **건설 품셈 표 모양 판정**(2026-09-18 브레인 일감 · 랩탑 메인).
왜 — 표 모양 판정(`_Table.detect_form`)이 산림 표에 맞춰져 있어 건설 표를 못 가렸음
(건설 미판정 891 · 산림 0). 건설은 같은 성격이라도 꼴이 다름 —
직종이 「특급·고급·중급·초급 기술자」로 적히고, 기계손료표가 「분류번호·상각 비율·시 간 당(10-7)」이며,
가설재가 「손율(%)·요율」로 적힘.
⚠ **산림 판정을 흔들지 않는다** — 여기 규칙은 `detect_form` 이 **미판정으로 떨어질 때만** 본다.
산림은 미판정이 0 이므로 이 층이 산림 결과를 바꿀 수 없다.
⚠ **못 가리면 미판정으로 남긴다** — 지어내 채우지 않는다(틀린 판정보다 미판정이 낫다).
⚠ **md 모양에 안 기댄다** — 줄 자리·표 차례가 아니라 **머리글·칸 글자**로만 가른다(원문 md 가 계속 고쳐짐).
"""
from __future__ import annotations
import re
from typing import Any
#: 건설 직종 — 산림 직종(`_Table.OCCUPATION_RE`)에 없는 이름. 측량·설계·기계 쪽이 여기 걸림.
CONST_WORKER_RE = re.compile(
r"기술자|기능사|기능공|기능인|기\s*사(?!용)|조종원|운전원|비계공|형틀목공|도장공|보통인부|특별인부|"
r"작업반장|배관공|전기공|통신공|측량사|제도사"
)
#: 값이 계산에 쓰이는 비율·계수 — 품이 아니라 **계수표**.
RATE_RE = re.compile(
r"\s*율|손\s*률|요\s*율|부자재율|잔존율|가산율|감가율|환산계수|보정계수|계\s*수"
)
#: 기계 손료표 머리(건설 8-3) — 「분류 번호 … 상각 비율 … 시 간 당(10-7)」.
LOSS_HEAD = ("분류번호", "상각")
#: 셀 안에 적힌 계산식 — `A=0.4×√N(㎡)` 처럼 기호와 `=` 가 든 긴 글.
FORMULA_RE = re.compile(r"[A-Za-z가-힣]\s*=\s*[\d.A-Za-z(√]")
#: 자원 수량이 붙는 단위 칸(칸 전체가 단위). 「인」은 품, 나머지는 자재·장비.
RESOURCE_UNITS = {
"", "인·일", "", "", "시간", "hr", "h", "", "", "", "", "", "kg", "", "", "ton",
"", "", "m", "", "", "L", "", "개소",
} # fmt: skip
_NUMBER_RE = re.compile(r"^\d[\d,]*(\.\d+)?$")
#: 한 칸에 값이 여럿 뭉친 것도 값으로 셈(`0.099 0.071`) — 뭉쳤다고 「값 없는 표」로 보면 안 됨.
#: ⚠ 꼬리 단위는 **품·자재 단위만** — `24hr`·`30톤`·`0.166일` 은 규격·기간 표기라 그 표의 값이 아님
#: (그런 표까지 값이 있다고 보면 기준·규격표가 미판정으로 남는다).
_NUMBERS_RE = re.compile(r"^[\d,.\s]*\d[\d,.\s]*(?:인|명|매|본|개|대|조|㎡|㎥|㎜|kg|㎏||%)?$")
#: 값이 아니라 근거·풀이를 적는 칸 — 여기 든 산출식·수는 그 표의 값이 아님.
_ASIDE_HEAD_RE = re.compile(r"비고|적용기준|산출근거|산출식|참고")
#: 비목 이름 — 첫 칸이 이것뿐인 표는 「비목 구성표」라 성격을 사람이 가름.
COST_ITEMS = {"재료비", "설치비", "노무비", "경비", "제작비", "운송비", "자재비", "주재료비"}
#: 공식 기호 한 글자(아래첨자·숫자 꼬리 포함) — `β` · `E` · `t 1` · `q₀`.
_SYMBOL_RE = re.compile(r"[A-Za-zα-ωΑ-Ω][₀-₉0-9]?")
def _squeeze(text: Any) -> str:
return re.sub(r"\s+", "", str(text or ""))
def _cells(table: dict[str, Any]) -> list[str]:
rows = [table.get("headers") or [], *(table.get("rows") or [])]
return [str(c) for row in rows for c in row]
def _value_cells(table: dict[str, Any]) -> list[str]:
"""비고·적용기준 칸을 뺀 칸들 — 그 표가 **값으로 내놓는 것**만."""
headers = [_squeeze(h) for h in table.get("headers") or []]
aside = {i for i, h in enumerate(headers) if _ASIDE_HEAD_RE.search(h)}
out = []
for row in [table.get("headers") or [], *(table.get("rows") or [])]:
out += [str(c) for i, c in enumerate(row) if i not in aside]
return out
def _has_number(cells: list[str]) -> bool:
return any(_NUMBERS_RE.match(_squeeze(c)) for c in cells if _squeeze(c))
def judged_const_form(table: dict[str, Any]) -> tuple[str, str] | None:
"""건설 꼴로 본 `(형태, 까닭)`. 못 가리면 `None` — 미판정으로 남김.
차례가 뜻을 가진다 — 좁은 표지부터. 계수·기준표를 먼저 걸러야 직종 낱말이 계수표를 품으로 오인하지 않는다.
"""
headers = [_squeeze(h) for h in table.get("headers") or []]
first_col = [_squeeze(row[0]) for row in table.get("rows") or [] if row]
# ① 기계 손료표 — 머리에 분류번호와 상각 비율이 함께 있음(건설 8-3, 122표)
if any(LOSS_HEAD[0] in h for h in headers) and any(LOSS_HEAD[1] in h for h in headers):
return "coefficient", "머리 '분류번호'+'상각 비율' — 기계 손료 계수표"
# ② 율·계수 표 — 손율·요율·부자재율·보정계수가 머리나 첫 칸에 있음(가설재·측량 보정)
if found := next((x for x in headers + first_col if RATE_RE.search(x)), None):
return "coefficient", f"'{found}' — 비율·계수표"
# ③ 값이 한 칸도 없는 표 — 자원이 없으니 품이 아님(설명·서식·이름 목록)
# ⚠ 첫 칸이 모두 **비목**인 표(`재료비 | JOINT FILLER`)는 빼둔다 — 참조냐 품이냐가 설계 결정이라
# **사람이 봐야 하는 자리**다(2026-09-17 비목 구성표 · `test_b08_work_item_master` 약속).
if not _has_number(_value_cells(table)):
if first_col and all(x in COST_ITEMS for x in first_col):
return None
return "reference", "값(숫자) 칸이 없음 — 설명·서식 표"
# ④ 건설 직종이 머리나 줄 이름에 있고 값이 있음 — 인력품 표(측량·설계 「특급 기술자」 …)
# ⚠ 계산식보다 **먼저** 본다 — 인력품 표의 비고에 산출식이 적힌 자리가 많다(9-5-4 수치지도).
if found := next((x for x in headers + first_col if CONST_WORKER_RE.search(x)), None):
return "requirement", f"건설 직종 '{found}'"
# ⑤ 단위 칸이 줄에 있고 같은 줄에 값이 있음 — 자원 소요량 표(`비계공 | 인 | 1.40`)
for row in table.get("rows") or []:
unit = next((c for c in row if _squeeze(c) in RESOURCE_UNITS), None)
if unit and any(_NUMBER_RE.match(_squeeze(c)) for c in row):
return "requirement", f"줄에 단위 칸 '{_squeeze(unit)}' + 값"
if any(_squeeze(h) in RESOURCE_UNITS for h in headers):
return "requirement", "머리에 단위 칸 + 값"
# ⑥ 값 자리에 계산식이 든 표 — 값이 아니라 식(현장사무소 규모 `A=0.4×√N` · 준설선 환산능력)
values = [c for c in _value_cells(table) if len(_squeeze(c)) > 6]
if found := next((c for c in values if FORMULA_RE.search(c)), None):
return "coefficient", f"값 자리에 계산식 '{_squeeze(found)[:20]}'"
# ⑦ 줄 이름이 공식 기호뿐 — `β`·`E`·`t₁` 처럼 한 글자 기호는 품 이름이 아님(시공능력 공식 밑값)
named = [x for x in first_col if x]
if named and all(_SYMBOL_RE.fullmatch(x) for x in named):
return "coefficient", f"줄 이름이 공식 기호뿐 {sorted(set(named))[:4]}"
# ⑧ 값이 모두 백분율 — 품·자재 수량이 아니라 요율(층별 할증 `5% 8% 12%`)
percents = [_squeeze(c) for c in _value_cells(table) if _squeeze(c)]
figures = [x for x in percents if _NUMBERS_RE.match(x) or x.endswith("%")]
if figures and all(x.endswith("%") for x in figures) and len(figures) >= 3:
return "coefficient", "값이 모두 백분율 — 요율표"
return None
@@ -11,6 +11,7 @@ import re
from typing import Any
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Forms import judged_basis, judged_form
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_FormsConst import judged_const_form
# ── 형태 판정 ────────────────────────────────────────────────────────────
# 헤더·비고 문자열에서 찾는 표지. 앞의 것이 먼저 걸린다(생산량형이 더 좁은 표현이라 우선).
@@ -189,6 +190,10 @@ def detect_form(table: dict[str, Any], chapter: str | None) -> tuple[str, str]:
if mark in body:
return "reference", f"분류 딱지 표의 '{mark}' — 값이 아니라 비율 지시"
# 산림 잣대로 못 가른 표만 **건설 꼴**로 다시 본다(2026-09-18 브레인 — 건설 미판정 891).
# ⚠ 여기까지 왔다는 것은 산림 판정이 이미 못 가렸다는 뜻이라, 산림 결과는 흔들리지 않는다.
if hit := judged_const_form(table):
return hit[0], f"건설 꼴 — {hit[1]}"
return "undetermined", "헤더·첫 행에 단위·밑수·직종 표지 없음"
@@ -207,12 +212,21 @@ SOURCE_BASIS_RATIO_RE = re.compile(
r"(㎥|m3|㎡|m2|㏊|ha|㎞|km|m|매|본|개소|개|인|공|kg|㎏|톤|ton|주|식|대)\s*당\s*[)]"
)
#: 밑수 단위 — 건설은 「일·기·조·명·틀」도 씀(2026-09-18 브레인 · 건설 밑수 미판독 162).
#: ⚠ 「회」는 넣지 않는다 — 2026-09-17 데스크탑 서브가 원문을 열어 **밑수 단위가 아니라고** 판정(10-4 중기운반).
_BASIS_UNIT = (
"공㎥|㎥|m3|㎡|m2|㏊|ha|㎞|km|m|매|본|개소|개|인|공|kg|㎏|톤|ton|주|식|대|일|기|조|명|틀"
)
#: ⚠ **괄호 앞뒤에 글이 붙은 건설 꼴**도 읽는다 — `(용접개소당)` · `(세면기 개당)` · `(절단길이 10m당)` ·
#: `(m당 : 관길이기준)`. 산림은 괄호 안이 밑수뿐이라 이 넓히기로 값이 달라지지 않는다.
SOURCE_BASIS_RE = re.compile(
r"[(]\s*(?:"
r"단위\s*[:]\s*([\d,]*\.?\d*)\s*(?P<u1>㎥|m3|㎡|m2|㏊|ha|㎞|km|m|매|본|개소|개|인|공|kg|㎏|톤|ton|주|식|대)\s*당?"
r"|([\d,]*\.?\d*)\s*(?P<u2>㎥|m3|㎡|m2|㏊|ha|㎞|km|m|매|본|개소|개|인|공|kg|㎏|톤|ton|주|식|대)\s*당"
r")\s*[)]"
rf"단위\s*[:]\s*([\d,]*\.?\d*)\s*(?P<u1>{_BASIS_UNIT})\s*당?"
rf"|[^()]*?([\d,]*\.?\d*)\s*(?P<u2>{_BASIS_UNIT})\s*당"
r")\s*(?:[:][^()]*)?[)]"
)
#: 한 괄호 안의 밑수 토막 — 둘 이상이면 못 고른다(`(1ha당, 100본당)`).
_BASIS_TOKEN_RE = re.compile(rf"[\d,]*\.?\d*\s*(?:{_BASIS_UNIT})\s*당")
#: 표 위로 몇 줄까지 거슬러 볼 것인가. 더 올라가면 **앞 표의 밑수**를 잘못 물어 온다.
SOURCE_LOOKBACK = 6
@@ -304,6 +318,9 @@ def basis_from_source(lines: list[str], line_no: int) -> tuple[float | None, str
quantity = 1.0
return quantity, m.group(2)
if m := SOURCE_BASIS_RE.search(text):
# ⚠ 한 괄호에 밑수가 둘이면(`(1ha당, 100본당)`) **한쪽만 집지 않는다** — 조용히 틀린다.
if len(_BASIS_TOKEN_RE.findall(m.group(0))) > 1:
continue
unit = m.group("u1") or m.group("u2")
raw = (m.group(1) if m.group("u1") else m.group(3)) or ""
raw = raw.replace(",", "")
@@ -728,6 +745,10 @@ def table_entry(
name_qty, name_unit = basis_from_name(node_name)
if name_unit:
basis_qty, basis_unit, basis_source = name_qty, name_unit, "절 이름"
# ⚠ **작업조 표에는 「일」 밑수를 안 붙인다**(2026-09-17 c3da333e). 밑수가 시공량 열이라
# 「1일」을 박으면 그 길이 막혀 호표가 통째로 사라진다 — 원문 「(일당)」이 있어도 안 붙인다.
if basis_unit == "" and crew_table(table):
basis_qty = basis_unit = basis_source = None
if basis_unit:
report["basis_found"] += 1
if basis_quantity_is_grouped(basis_qty):
@@ -92,7 +92,8 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
catalog = load_machine_catalog()
operating = {row.machine_code: row for row in load_operating_records().records}
# 파쇄기는 8-4 칸이 「-」 라 8-11 [주]⑤ 레코드로 섬(단가와 같은 값) — 없으면 장이 「연료·조종원 없음」 으로 틀리게 뜸.
if f"X-{CHIPPER}" in build.book.titles and CHIPPER not in operating:
# ⚠ 8-4 에 줄이 생겨도 연료 칸이 「-」 면 값이 없는 것 — 산림 8-11 [주]⑤ 가 정본(2026-09-18).
if f"X-{CHIPPER}" in build.book.titles and getattr(operating.get(CHIPPER), "fuel_liters_per_hour", None) is None: # fmt: skip
chipper = chipper_record()
if chipper is not None:
operating[CHIPPER] = chipper
+3 -1
View File
@@ -301,7 +301,9 @@ def _add_machine_layers(
from B09_Estimation.B09_Estimation_WoodChipping import MACHINE as CHIPPER
from B09_Estimation.B09_Estimation_WoodChipping import operating_record as chipper_record
if CHIPPER in machine_codes and CHIPPER not in operating:
# ⚠ 8-4 에 그 기종 줄이 생겨도 **연료 칸이 「-」 면 값이 없는 것** — 산림 8-11 [주]⑤ 가 그대로 정본
# (2026-09-18 랩탑 서브가 8-4 를 줄마다 풀자 빈 레코드가 생겨 연료 층이 통째로 빠졌음).
if CHIPPER in machine_codes and getattr(operating.get(CHIPPER), "fuel_liters_per_hour", None) is None: # fmt: skip
chipper = chipper_record()
if chipper is not None:
operating[CHIPPER] = chipper
+16 -3
View File
@@ -254,14 +254,22 @@ def parse_fuel(
specs = split_values(row[spec_col], len(codes)) if spec_col is not None and spec_col < len(row) else None # fmt: skip
for n, code in enumerate(codes):
raw = values["fuel_rate_l_per_hour"][n]
if _num(raw) is None:
# ⚠ 연료가 둘인 칸(`73.7+휘발유54.5`)은 한쪽만 적으면 틀림 — 안 싣고 셈만 남김
if _num(raw) is None or "+" in raw:
# ⚠ 주연료가 수가 아닌 기종(전기 `㎾` · `-`)은 **연료 목록에 안 실음** — ℓ/hr 자료임.
skipped.append({"machine_code": code, "what": raw.strip()[:20]})
continue
record = {
"machine_code": code,
"machine_name": names.get(code[:4], ""),
"fuel_type": "gasoline" if re.search(r"휘발유|가솔린", raw) else "diesel",
# 표시 없는 것은 경유([주]① ㉰) · `휘발유0.7` · `중유487.2`
"fuel_type": (
"gasoline"
if re.search(r"휘발유|가솔린", raw)
else "heavy_oil"
if "중유" in raw
else "diesel"
),
"fuel_rate_l_per_hour": _num(raw),
}
for field in ("misc_material_percent_of_fuel", "operator_person_per_day"):
@@ -291,8 +299,13 @@ def extract(old_doc: dict, data: bytes) -> tuple[dict, dict]:
# 원문에서 못 읽은 줄은 옛 벌 값을 이어받음(손으로 맞춘 줄 · 손질 사유 · 규격)
old_fuel = {r["machine_code"]: r for r in old["mach_fuel_rate"]["parsed_records"]}
got = {r["machine_code"] for r in fuel}
# ⚠ **원문에 줄이 있는데 연료를 못 읽은 코드만** 이어받는다 —
# 못 읽은 줄(토막 수 안 맞음) · 주연료 칸이 수가 아닌 줄(`1.49㎾` 처럼 칸이 밀린 양수기).
# 원문에 아예 없는 기종은 안 이어받는다(원문에 없는 줄이 자료에 남는다).
unread = {c for row in bad_fuel for c in codes_in(row["what"], None)[0]}
unread |= {r["machine_code"] for r in no_fuel}
for code, record in old_fuel.items():
if code not in got:
if code not in got and code in unread:
fuel.append(record)
carried["fuel"] += 1
fuel.sort(key=lambda r: r["machine_code"])
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4813,7 +4813,7 @@
"name_ko": "기계 연료소모량·운전경비",
"summary": "기계마다 한 시간에 쓰는 연료량과 잡재료비 비율, 운전원 수.",
"shape": "list",
"rows": 214,
"rows": 274,
"columns": [
{
"key": "machine_code",
@@ -7267,7 +7267,7 @@
"name_ko": "형태 미판정 표",
"summary": "생산량형인지 소요량형인지 못 정한 표.",
"shape": "list",
"rows": 891,
"rows": 276,
"columns": [
{
"key": "pum_table_id",
@@ -7323,7 +7323,7 @@
"name_ko": "밑수 미확보 표",
"summary": "기준 수량을 못 찾은 표.",
"shape": "list",
"rows": 439,
"rows": 423,
"columns": [
{
"key": "pum_table_id",
@@ -7552,7 +7552,7 @@
"name_ko": "갈래 열쇠 장부",
"summary": "공종 열쇠 아래 갈래마다 번호를 매긴 것. 사라진 갈래의 번호는 다시 쓰지 않는다.",
"shape": "map",
"rows": 112,
"rows": 194,
"columns": [
{
"key": "@key",
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-18T05:20:39+09:00",
"generated_at": "2026-09-18T07:51:58+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_forest",
@@ -12,8 +12,8 @@
"files": [
{
"file": "work_item_master_2026-01-01.json",
"sha256": "8bf71f9eefcfdf970eec4bf82e0249cf94620188b54ce5f3ae4df15b8f33dab7",
"size_bytes": 1081872
"sha256": "b1b6e40841d84b3281dcd3c3965ec5c69cfad390111da5a5b0e8435c1db30421",
"size_bytes": 1081879
},
{
"file": "form_undetermined_2026-01-01.json",
@@ -1,29 +1,29 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-18T05:21:44+09:00",
"generated_at": "2026-09-18T07:52:45+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_const",
"effective_date": "2026-01-01",
"sha256": "67285a536e7edd6655fceb6844c08fa237c443fdea720a4454fd53ac6d297d1d",
"sha256": "9176db34cc516e6e6baffcc6a92cf9ab8975df83e85f0685f101eb23e03e19f1",
"file": "pum_const_2026.json"
},
"files": [
{
"file": "const_work_item_master_2026-01-01.json",
"sha256": "c8446c68afa521c6f1817346371c359304075d218f79fb38b3507d1e9f07cf42",
"size_bytes": 4508691
"sha256": "9e16c33506535e5c3f422a32ec19c74e463011411e875a291928cc9132757d03",
"size_bytes": 4508625
},
{
"file": "const_form_undetermined_2026-01-01.json",
"sha256": "aa75542987d4421fe26e0e6653c5fd7d393a153b5ad9470fcd7b605b5f9c5cb8",
"size_bytes": 493520
"sha256": "d34ee8ece0799a4e7dc0ccf4088ddad29f97108cacbacf9f296144bd6816b1d1",
"size_bytes": 154500
},
{
"file": "const_basis_missing_2026-01-01.json",
"sha256": "e5f1683980debbb3a3d83eedffc32533f7305313cd668e7ac40ae171287ee3b0",
"size_bytes": 110670
"sha256": "3c9986fadd382dc6eba3a8b729c41967c817e65e00b920df150acb00890d9ac6",
"size_bytes": 107743
}
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
"effective_date": "2026-01-01",
"pum_edition": "2026-01-01",
"toc_edition": "산림청고시제2025-82호",
"generated_at": "2026-09-18T05:20:39+09:00",
"generated_at": "2026-09-18T07:51:58+09:00",
"dataset_version": {
"dataset_id": "pum_forest",
"effective_date": "2026-01-01",
@@ -25,7 +25,7 @@
"tables_attached": 453,
"tables_orphan": 22,
"form_undetermined": 0,
"basis_found": 251,
"basis_found": 253,
"basis_missing": 79,
"basis_absent_in_source": 69,
"basis_unparsed": 10,
@@ -5986,9 +5986,9 @@
"source_line": 1651,
"pum_form": "coefficient",
"form_basis": "헤더 '손료계수'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "본문",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -23521,9 +23521,9 @@
"source_line": 4977,
"pum_form": "coefficient",
"form_basis": "사람 판정 — 공식 명세 — 시공능력 공식 파라미터(안티그래비티 판정 `formula`)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "일",
"basis_source": "본문",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -16,16 +16,15 @@
"data": "",
"why": "자료 이름이 빔 — 원문 손료표 머리 `(코드) 이름` 엔 이름 있음"
},
"셈 연료 코드 수": {
"source": 272,
"data": 274,
"why": "원문이 `7360-0055 0090` 꼴로 앞자리를 이어 적은 줄 하나(추출기는 읽고 이 시험 읽기는 못 따라감) + 원문이 규격을 연료 칸에 밀어 적은 양수기 7993-0020(사람이 맞춘 값을 이어받음) — 둘 다 값은 원문 그대로"
},
"손료 8201-0100 machine_name": {
"source": "3D GNSS 머신 가이던스(굴착기용)",
"data": "배관파이프시 간 당(10-7)분류번호규 격7995-0050ø50-2.6m5,0008-3-9 [80]스마트 건설장비(8201) 3D GNSS 머신 가이던스(굴착기용)",
"why": "자료 이름 칸에 원문 뭉개진 손료 글이 통째로 들어감 — 자료 이름 틀림"
},
"셈 연료 코드 수": {
"source": 201,
"data": 214,
"why": "⬜ **기계 자료 재추출 대기** — 코덱스가 8-4 운전경비 92종을 md 에 전사했으나(2026-09-18) `mach_base_2026.json` 은 그 앞 판이다. 그 벌을 만드는 추출기가 이 저장소에 없어 우리가 못 돌린다. ⇒ 고칠 자리는 마스터가 아니라 **기계 원천 벌 재추출**이다.",
"class": "㉯ 남이 고쳐야 풀림"
}
},
"material": {},
@@ -93,7 +92,7 @@
},
"안 잰 칸 mach_fuel_rate.parse_method": {
"what": "원문과 안 맞댐",
"why": "빌더가 남긴 읽기 방법 표시 — 원문 칸 아님"
"why": "사람이 손으로 맞춘 줄이라는 표시 — 원문에 없는 칸(2026-09-18 기준 7993-0020 하나)"
},
"안 잰 칸 mach_fuel_rate.source_tables.section": {
"what": "원문과 안 맞댐",
@@ -139,64 +138,20 @@
"what": "원문과 안 맞댐",
"why": "[주]① 예외 글을 프로그램 말로 옮긴 칸 — 글자로 안 맞댐"
},
"연료 못 읽은 줄 7505-0025": {
"연료 못 읽은 줄 7360-0090": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0050": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0100": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0125": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0150": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0200": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0250": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0350": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0450": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0500": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 못 읽은 줄 7505-0700": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
"why": "원문이 `7360-0055 0090` 꼴로 앞자리를 이어 적음 — 이 시험 읽기가 못 따라가는 자리(추출기는 읽음)"
},
"연료 못 읽은 줄 7993-0020": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
"why": "8-4 양수기 줄이 `| 7993-0020 | 양수기 | 1.49㎾ | 1.49㎾ | - | - |` — 원문이 규격을 연료 칸에 밀어 적음 · 사람이 맞춘 1.3ℓ(휘발유)를 옛 벌에서 이어받음"
},
"연료 못 읽은 줄 7994-0050": {
"what": "원문 표 토막 수가 안 맞음",
"why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄"
},
"연료 표 3108-0040": {
"연료 표 3450-0642": {
"codes": [
"3108-0040",
"3901-0300"
"3450-0642",
"3450-0642"
],
"why": "원문 운전경비 표 토막 수가 안 맞음 — 이 표 줄은 못 읽음"
"why": "원문 8-4 가 코드·값을 한 칸에 뭉쳐 적어 토막 수가 안 맞음 — 시험 읽기가 건너뜀(랩탑 서브 전사 뒤 새로 생긴 자리)"
},
"연료 표 5801-0045": {
"codes": [
@@ -222,19 +177,61 @@
"why": "⬜ **기계 자료 재추출 대기** — 코덱스가 8-4 운전경비 92종을 md 에 전사했으나(2026-09-18) `mach_base_2026.json` 은 그 앞 판이다. 그 벌을 만드는 추출기가 이 저장소에 없어 우리가 못 돌린다. ⇒ 고칠 자리는 마스터가 아니라 **기계 원천 벌 재추출**이다.",
"class": "㉯ 남이 고쳐야 풀림"
},
"연료 표 7101-0450": {
"연료 표 7202-0008": {
"codes": [
"7101-0450",
"7811-0120"
"7202-0008",
"7202-0008"
],
"why": "원문 운전경비 표 토막 수가 안 맞음 — 이 표 줄은 못 읽음"
"why": "원문 8-4 가 코드·값을 한 칸에 뭉쳐 적어 토막 수가 안 맞음 — 시험 읽기가 건너뜀(랩탑 서브 전사 뒤 새로 생긴 자리)"
},
"연료 표 7812-0005": {
"연료 표 7202-0010": {
"codes": [
"7812-0005",
"7994-0050"
"7202-0010",
"7202-0010"
],
"why": "원문 운전경비 표 토막 수가 안 맞음 — 이 표 줄은 못 읽음"
"why": "원문 8-4 가 코드·값을 한 칸에 뭉쳐 적어 토막 수가 안 맞음 — 시험 읽기가 건너뜀(랩탑 서브 전사 뒤 새로 생긴 자리)"
},
"연료 표 7360-0090": {
"codes": [
"7360-0090",
"7360-0090"
],
"why": "원문이 `7360-0055 0090` 꼴로 앞자리를 이어 적음 — 이 시험 읽기가 못 따라가는 자리(추출기는 읽음)"
},
"연료 표 7991-0050": {
"codes": [
"7991-0050",
"7991-0050"
],
"why": "원문 8-4 가 코드·값을 한 칸에 뭉쳐 적어 토막 수가 안 맞음 — 시험 읽기가 건너뜀(랩탑 서브 전사 뒤 새로 생긴 자리)"
},
"연료 표 7991-0100": {
"codes": [
"7991-0100",
"7991-0100"
],
"why": "원문 8-4 가 코드·값을 한 칸에 뭉쳐 적어 토막 수가 안 맞음 — 시험 읽기가 건너뜀(랩탑 서브 전사 뒤 새로 생긴 자리)"
},
"연료 표 7991-0500": {
"codes": [
"7991-0500",
"7991-0500"
],
"why": "원문 8-4 가 코드·값을 한 칸에 뭉쳐 적어 토막 수가 안 맞음 — 시험 읽기가 건너뜀(랩탑 서브 전사 뒤 새로 생긴 자리)"
},
"연료 표 7992-0001": {
"codes": [
"7992-0001",
"7992-0001"
],
"why": "원문 8-4 가 코드·값을 한 칸에 뭉쳐 적어 토막 수가 안 맞음 — 시험 읽기가 건너뜀(랩탑 서브 전사 뒤 새로 생긴 자리)"
},
"연료 표 7993-0020": {
"codes": [
"7993-0020",
"7993-0020"
],
"why": "원문 8-4 가 코드·값을 한 칸에 뭉쳐 적어 토막 수가 안 맞음 — 시험 읽기가 건너뜀(랩탑 서브 전사 뒤 새로 생긴 자리)"
},
"운전사 기종별 판단": {
"what": 121,
@@ -101,49 +101,25 @@
"class": "㉮ 영영 할 일 아님",
"class_why": "작업조 표 — 밑수가 시공량 열이라 일부러 비움"
},
"F0173": {
"section": "7-2",
"md": null,
"md_evidence": "",
"master": [
1.0,
"㎥"
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스 md 읽기 넓히기"
},
"F0188": {
"section": "7-10",
"md": null,
"md_evidence": "",
"master": [
1.0,
"㎥"
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "위와 같음"
},
"F0225": {
"section": "8-6-1",
"md": null,
"md_evidence": "",
"master": [
160.0,
"ha"
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "위와 같음(원문 「160ha당」)"
},
"F0257": {
"section": "9-11-1",
"F0228": {
"section": "8-6-2",
"md": [
1.0,
""
"ha"
],
"md_evidence": "(단위: 일당)",
"md_evidence": "표·머리 무인헬기 (ha당)",
"master": null,
"class": "㉮ 영영 할 일 아님",
"class_why": "「일당」 · 계수표"
"why": "원문 줄 칸 「무인헬기 (ha당)」 — 마스터가 아직 안 읽음(2026-09-09 「인」 오독 판정과 같은 자리, 밑수는 ha)"
},
"F0229": {
"section": "8-6-2",
"md": [
1.0,
"ha"
],
"md_evidence": "표·머리 멀티콥터 (ha당)",
"master": null,
"why": "위와 같음(멀티콥터)"
},
"F0322": {
"section": "10-13-1",
@@ -253,7 +229,8 @@
"- 3. 박피품은 별도 적용하여야 한다. 다만 작업조건에 따라 1일 작업량은 변동 적용한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0104": {
"section": "5-3-4",
@@ -268,7 +245,8 @@
"- ⑦ 굴착기 규격은 0.4㎥를 기준으로 한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0106": {
"section": "5-3-5",
@@ -284,7 +262,8 @@
"- ⑧ 장비 규격은 다음을 기준으로 한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0189": {
"section": "7-11",
@@ -296,7 +275,8 @@
"- ④ 할인․할증은 경사도(1-4-5), 주행 장애물 상태(1-4-13)를 반영한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0223": {
"section": "8-4",
@@ -306,7 +286,8 @@
"- ④ 할인․할증은 경사도(1-4-5), 방제 대상목 분포(1-4-17)를 반영한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0251": {
"section": "9-8-1",
@@ -318,45 +299,8 @@
"[주] 「건설공사 표준품셈 공통부문, 8-2-13 대형브레이커(2025)」, 「건설공사 표준품셈 유지관리부문, 3-1-3 철골재 철거(인력)(2025)」, 「건설공사 표준품셈 유지관리부문, 3-1-4 철골재 철거(기계)(2025)」, 「건설공사 표준품셈 공통부문, 1-2-3 토질, 3. 체적환산계수적용(2025)」, 「국토건설 설계실무요령, 수량산출요령(2021)」 참조한다"
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
},
"F0252": {
"section": "9-8-2",
"md_only": [],
"master_only": [
"- 2. 철근절단 : 철근콘크리트(T=30㎝ 미만)품을 적용한다.",
"- 3. 집토 : Q=3600×q×k×f×E/㎝= ㎥/시간",
"[주]① 장비는 무한궤도 굴착기(0.7㎥)를 적용한다.",
"- ② 굴착기작업능력은 무근콘크리트(T=30㎝미만) 값 적용한다.",
"- 4. 고철공제 : A=1㎥×0.008×7,850kg/㎥×80% = 50.24 kg/㎥≒50 kg/㎥",
"[주]① 적사 및 운반은 폐기물 상차 및 운반비에서 적용한다.",
"- ② 「건설공사 표준품셈 유지관리부문, 3-1-3 철골재 철거(인력)(2025)」, 「건설공사 표준품셈 유지관리부문., 3-1-4 철골재 철거(기계)(2025)」 참조한다"
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
},
"F0254": {
"section": "9-10-1",
"md_only": [],
"master_only": [
"- 2. 집토 : Q=3600×q×k×f×E/㎝= ㎥/시간",
"[주]① 장비는 무한궤도 굴착기(0.7㎥)를 적용한다.",
"- ② 굴착기작업능력은 무근콘크리트(T=30㎝미만) 값 적용한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
},
"F0255": {
"section": "9-10-2",
"md_only": [],
"master_only": [
"- 2. 집토 : Q=3600×q×k×f×E/㎝= ㎥/시간",
"[주]① 장비는 무한궤도 굴착기(0.7㎥)를 적용한다.",
"- ② 굴착기작업능력은 무근콘크리트(T=30㎝미만) 값을 적용한다.",
"- ③ 「건설공사 표준품셈 공통부문, 8-2-13 대형브레이커(2025)」 참조한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0281": {
"section": "9-15-1",
@@ -365,7 +309,8 @@
"- ③ 「건설공사 표준품셈 공통부문, 8-2-3 굴착기(2025)」 참조한다. (불도저작업이 어려운 경우)"
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0282": {
"section": "9-15-2",
@@ -374,7 +319,8 @@
"- ③ 「건설공사 표준품셈 공통부문, 8-2-1 불도저(2025)」 참조(표토제거 두께 T=0.2m)"
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0285": {
"section": "9-16-3",
@@ -383,7 +329,8 @@
"- ④ 토목은 동상방지층, 보조기층, 기층 등 층별 조건이나 본선, 길어깨 등에 따라 임도에는 대형장비(5,500ℓ)를 사용한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0286": {
"section": "9-17-2",
@@ -393,7 +340,8 @@
"- ④ 「건설공사 표준품셈 공통부문 8-2-1 불도저(2025)」 참조한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0295": {
"section": "9-22",
@@ -402,7 +350,8 @@
"- ④ 0.2㎥ 또는 0.4㎥ 용량의 굴착기를 사용하는 경우에는「건설공사 표준품셈 공통부문, 8-2-3 굴착기(2025)」를 참조하여 적용계수를 달리 적용하도록 한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0319": {
"section": "10-11",
@@ -411,7 +360,8 @@
"- ③ 「건설공사 표준품셈 공통부문. 8-2-1 불도저(2025)」 참조한다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0323": {
"section": "10-13-2",
@@ -420,7 +370,8 @@
"[주] 위 단가산출은 일본 임야청에서 묘목 5,000본의 운반을 가정하여 현장 검증을 통해 산출한 내용이므로, 묘목 이외의 다른 화물을 운반할 시에는 달라질 수 있다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
},
"F0393": {
"section": "12-38-2",
@@ -430,7 +381,8 @@
"- ② 플랫 타이(Flat Tie) 대신 폼 타이(Form Tie) 사용 시 소요 수량은 12-4 합판 거푸집 자재수량을 따른다."
],
"class": "㉯ 남이 고쳐야 풀림",
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중"
"class_why": "마스터가 맞고 md 읽기가 덜 읽음 — 코덱스가 md 읽기를 넓히는 중",
"why": "마스터가 **뒤 표의 [주]**까지 끌어다 붙임 — 잘라내면 진짜 계수가 나갈 수 있어 붙여 둔 쪽(2026-09-18 브레인 「애매하면 안 깎는 쪽」)"
}
},
"form": {
+26 -7
View File
@@ -5,7 +5,7 @@
- **줄이 밀려도 값이 그대로** 자리는 번호가 아니라 **기계 코드** 잡음.
- 칸에 뭉친 (`0101-0007 0010 `) 줄마다 판이 **같은 ** (코덱스 8-4 손질이 ).
- 토막 수가 맞으면 **지어내지 않고** `parse_audit` 적음.
- 사람이 맞춘 (`parse_method`)·손질 사유(`note`) ** 벌에서 이어받음**.
- 사람이 맞춘 (`parse_method`)·손질 사유(`note`) ** 벌에서 이어받음** 원문에 줄이 있는 것만.
금액 불변 뽑은 `test_work_item_key_gate.py` 원가계산서 시험(직접공사비 5,983,724) .
"""
@@ -113,16 +113,32 @@ def test_토막_수가_안_맞으면_지어내지_않고_적어_둠() -> None:
def test_사람이_맞춘_줄과_사유는_이어받음() -> None:
"""원문에 줄은 있으나 연료를 못 읽은 자리(`1.49㎾` — 칸 밀림)만 이어받음.
원문에 아예 없는 기종은 이어받음 벌에만 있는 줄이 자료에 남으면 .
"""
md = GLUED.replace(
"| 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 |",
"| 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 |\n"
"| 7993-0020 | 양수기 | 1.49㎾ | 1.49㎾ | - | - |",
)
old = json.loads(json.dumps(BARE))
old["variables"]["mach_fuel_rate"]["parsed_records"] = [
{
"machine_code": "7505-0025",
"machine_name": "발전",
"machine_code": "7993-0020",
"machine_name": "양수",
"fuel_type": "gasoline",
"fuel_rate_l_per_hour": 1.3,
"parse_method": "verified_source_row_alignment",
},
{
"machine_code": "9999-0001",
"machine_name": "원문에 없는 기계",
"fuel_type": "diesel",
"fuel_rate_l_per_hour": 4.3,
"parse_method": "verified_source_row_alignment",
} # fmt: skip
]
},
] # fmt: skip
old["variables"]["mach_loss_coef"]["records"] = [
{
"machine_code": "0101-0007",
@@ -130,8 +146,11 @@ def test_사람이_맞춘_줄과_사유는_이어받음() -> None:
"note": "칸 밀림 바로잡음",
}
]
doc = _extract(GLUED, old)
assert _of(doc, "mach_fuel_rate", "parsed_records")["7505-0025"]["parse_method"] == "verified_source_row_alignment" # fmt: skip
doc = _extract(md, old)
fuel = _of(doc, "mach_fuel_rate", "parsed_records")
assert fuel["7993-0020"]["parse_method"] == "verified_source_row_alignment"
assert fuel["7993-0020"]["fuel_rate_l_per_hour"] == 1.3
assert "9999-0001" not in fuel
assert _of(doc, "mach_loss_coef")["0101-0007"]["note"] == "칸 밀림 바로잡음"
+160
View File
@@ -0,0 +1,160 @@
"""건설 표 모양 판정·밑수 읽기 — 2026-09-18 브레인 일감(랩탑 메인).
못박는
- **산림은 건설 층에 닿지 않음** 건설 규칙은 산림 잣대가 가른 표에서만 돌고, 산림 미판정은 0.
- 건설 여섯(기계 손료표 · ·계수 · 없는 설명표 · 건설 직종 · 단위 · 공식 기호) 가름.
- ** 가리면 미판정** 지어내지 않음.
- 밑수 읽기: 건설 `(용접개소당)`·`(세면기 개당)`·`(m당 : 관길이기준)`·`(일당)` 읽되
`(1ha당, 100본당)` 처럼 **둘이면 고름** · `` `` 깎임 · 밑수가 아님.
- **작업조 표에는 밑수를 붙임**(c3da333e 붙이면 유로폼 호표가 사라짐).
금액 불변은 `test_work_item_key_gate.py` 원가계산서 시험(직접공사비 5,983,724) .
"""
from __future__ import annotations
import json
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_Build_WorkItemMaster_FormsConst import ( # noqa: E402
judged_const_form,
)
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Table import ( # noqa: E402
basis_from_source,
crew_table,
detect_form,
new_report,
table_entry,
)
FOREST = ROOT / "resources/data_cost_input_value/pum_forest_2026.json"
MASTER = ROOT / "resources/data_work_item_master"
def table(headers: list[str], rows: list[list[str]]) -> dict:
return {"table_id": "T0001", "section": "시험", "line": 3, "headers": headers, "rows": rows}
# ── 산림을 흔들지 않음 ────────────────────────────────────────────────────
def test_산림_표는_건설_층에_닿지_않음() -> None:
"""산림 표는 산림 잣대에서 이미 갈림 — 건설 규칙이 산림 값을 바꿀 수 없음(마스터 산출로 잼)."""
master = json.loads((MASTER / "work_item_master_2026-01-01.json").read_text(encoding="utf-8"))
reached = [
t["pum_table_id"]
for w in master["work_items"]
for t in w.get("tables", [])
if str(t.get("form_basis", "")).startswith("건설 꼴")
]
assert reached == [], f"산림 표가 건설 층까지 내려감: {reached[:5]}"
def test_산림_마스터는_형태_미판정이_없음() -> None:
master = json.loads((MASTER / "work_item_master_2026-01-01.json").read_text(encoding="utf-8"))
assert master["stats"]["form_undetermined"] == 0
# ── 건설 꼴 가름 ─────────────────────────────────────────────────────────
def test_기계_손료표는_계수() -> None:
got = judged_const_form(
table(
["분류 번호", "규격 (ton)", "내용 시간", "상각 비율", "시 간 당(10-7)"],
[["0101-0007", "7", "12,000", "0.9", "1,811"]],
)
)
assert got and got[0] == "coefficient"
@pytest.mark.parametrize(
"headers, rows",
[
(
["사용기간별 구 분", "3개월 (%)", "2개년초과 평균손율 (%)"],
[["철 물", "30", "85"]],
), # 손율
(["지형종류", "시가지"], [["계수", "0.58"]]), # 보정계수
(["설치간격", "0.6m이하"], [["요 율", "17%"]]), # 요율
],
)
def test_율_계수표는_계수(headers: list[str], rows: list[list[str]]) -> None:
got = judged_const_form(table(headers, rows))
assert got and got[0] == "coefficient"
def test_값이_없는_설명표는_참조() -> None:
got = judged_const_form(
table(["노 임 구 분", "산 정 식"], [["노임합계", "P+PO"], ["기본노임", "P"]])
)
assert got == ("reference", "값(숫자) 칸이 없음 — 설명·서식 표")
def test_건설_직종이_있으면_소요량() -> None:
got = judged_const_form(
table(["구 분", "특급 기술자", "고급 기술자"], [["작업계획", "0.5", "1.1"]])
)
assert got and got[0] == "requirement"
def test_비고의_산출식은_계수로_안_뒤집음() -> None:
"""인력품 표의 비고에 산출식이 있어도 **품 표**다(9-5-4 수치지도)."""
got = judged_const_form(
table(
["구 분", "고급 기술자", "비 고"],
[["자동독취", "0.016인", "4매×20분/60분/8시간=0.166일"]],
)
)
assert got and got[0] == "requirement"
def test_단위_칸과_값이_한_줄이면_소요량() -> None:
got = judged_const_form(table(["구 분", "단 위", "브라켓형"], [["비 계 공", "", "1.40"]]))
assert got and got[0] == "requirement"
def test_줄_이름이_공식_기호뿐이면_계수() -> None:
got = judged_const_form(table(["구 분", "R.C.D", "올케이싱"], [["β", "1.14", "1.08"]]))
assert got and got[0] == "coefficient"
def test_못_가리면_미판정으로_남김() -> None:
"""지역·월별 숫자만 있는 표 — 무엇의 값인지 원문으로 못 가름."""
assert judged_const_form(table(["지역별", "1월", "2월"], [["춘천", "7", "5"]])) is None
assert detect_form(table(["지역별", "1월"], [["춘천", "7"]]), None)[0] == "undetermined"
# ── 밑수 읽기 ────────────────────────────────────────────────────────────
@pytest.mark.parametrize(
"line, want",
[
("1-2-1 용접접합('93년 보완)(용접개소당)", (1.0, "개소")),
("7-1-5 카운터형 세면기 설치(분리형)('26년 보완)(세면기 개당)", (1.0, "")),
("- 1. 이중보온관 부설(m당 : 관길이기준)", (1.0, "m")),
("1-3-10 플륨관 해체('22년 보완)(일당)", (1.0, "")),
("6-8-1 주철제 게이트 제수밸브 부설 및 접합('23년 보완)(기당)", (1.0, "")),
("1-3-1 데크플레이트 가스절단('18년 보완)(절단길이 10m당)", (10.0, "m")),
("### 12-19. 강관비계 (단위: 공㎥당)", (1.0, "공㎥")),
("### 6-2-1. 둘레베기 (1ha당, 100본당)", (None, None)), # 밑수가 둘 — 안 고름
("### 10-4. 중기운반 (단위: 회당)", (None, None)), # 「회」는 밑수 단위가 아님
],
)
def test_건설_꼴_밑수_읽기(line: str, want: tuple) -> None:
assert basis_from_source([line, "", "| 머리 |"], 3) == want
def test_작업조_표에는_일_밑수를_안_붙임() -> None:
"""원문이 `(일당)` 이라도 작업조 표면 밑수를 비움 — 밑수는 시공량 열."""
crew = table(
["구 분", "단 위", "수 량", "시 공 량 (㎡)"],
[["형틀목공 보통인부", "인 인", "4 1", "25"]],
)
assert crew_table(crew)
lines = ["### 12-38-3 설치 및 해체 (일당)", "", "| 머리 |"]
entry = table_entry(crew, "12-38-3", "12-38-3", "12", lines, None, new_report())
assert (entry["basis_quantity"], entry["basis_unit"]) == (None, None)
plain = table(["구 분", "단 위", "수 량"], [["보통인부", "", "1.4"]])
entry = table_entry(plain, "2-1", "2-1", "2", lines, None, new_report())
assert (entry["basis_quantity"], entry["basis_unit"]) == (1.0, "")
@@ -61,7 +61,11 @@ MASTER = OUT_DIR / "work_item_master_2026-01-01.json"
#: 2026-09-17 랩탑 메인 둘째 — 헛단위 뺌(인용을 제목으로 읽은 13-3 표 셋 · 부록 사례 번호 겹침 일곱)으로 다시 뜸
#: (옛 be88f730…). 바뀐 줄은 13-3·4-1·4-2·2-1·2-2·3-1·3-2·3-3·13-4-1·13-5-2 의 표 귀속뿐 —
#: `test_work_item_master_toc` 가 자리를 잼 · 단가표 대조는 그 파일 머리.
BASELINE_WORK_ITEMS_SHA = "803898a371e82c8a66692807af189a646c9b6fb3f077a4054cf6ef6030d8341b"
#: 2026-09-18 랩탑 메인 — **밑수 읽기를 넓혀** 원문에 있던 두 자리를 더 읽어 다시 뜸(옛 803898a3…).
#: 2-2-4 배부식분무기 「(덩굴 약제처리, ha당)」 · 9-11-1 「(단위: 일당)」 — 둘 다 원문 그대로다.
#: ⚠ 건설 표 판정층은 **산림이 미판정일 때만** 도므로 산림 형태는 한 글자도 안 바뀌었고,
#: 원가계산서(직접공사비 5,983,724)도 그대로다 — 값이 움직인 것이 아니라 **못 읽던 밑수를 읽은 것**이다.
BASELINE_WORK_ITEMS_SHA = "837eb8aa229490ffac95b591864a96109d48eae3f8d5ff9c6fe223b92784bd21"
#: 열쇠 작업이 새로 얹은 칸. 지문을 잴 때만 떼어낸다.
ADDED_FIELDS = (
@@ -100,9 +100,13 @@ def parse_basis(text: str) -> tuple[float, str] | str | None:
- `ha당 소요량` · `1km 소요량기준` 소요량 앞이 밑수
- 숫자가 붙을 때만(`(단위 : )` 품의 단위 2026-09-09 원문 대조)
"""
bare = re.match(r"^\s*단\s*위\s*[:]\s*([\d.,]*)\s*(\S+)\s*$", text)
t = re.sub(r"^\s*단\s*위\s*[:]?\s*", "", text).strip()
t = re.sub(r"(\d),(\d{3})", r"\1\2", t)
t = re.sub(r"\s*당?\s*소요량\s*(기준)?$", "", t)
# ⚠ 한 괄호에 밑수가 둘이면 못 고름 — `(1ha당, 100본당)` 에서 한쪽만 집으면 조용히 틀림.
if len(re.findall(rf"[\d,]*\.?\d*\s*(?:{_UNIT})\s*당", t)) > 1:
return ""
if "/" in t:
denominators = t.split("/")[1:]
den = re.sub(r"\s*당$", "", denominators[-1]).strip()
@@ -110,9 +114,16 @@ def parse_basis(text: str) -> tuple[float, str] | str | None:
return ""
elif t.endswith(""):
den = t[:-1].strip()
elif bare: # `(단위 : ㎥)` — 「당」 없이 단위만 적은 자리(7-2 수라집재 · 7-10 우드그랩)
den = t
else:
return None
m = _ONE.match(den)
if m is None and "," in den or "" in den:
# `덩굴 약제처리, ha당` — 앞이 **숫자 없는 설명 글**일 때만 뒤를 밑수로 봄.
head, _, tail = den.replace("", ",").rpartition(",")
if not re.search(r"\d", head):
m = _ONE.match(tail.strip())
if m is None:
return None
raw, unit = m.group(1), m.group(2)
@@ -143,13 +154,35 @@ def md_basis(lines: list[str], table: dict) -> tuple[tuple[float, str] | None, s
for text in [m.group(1) for m in _PAREN_ANY.finditer(head)] + [head]:
if isinstance(hit := parse_basis(text), tuple):
found.setdefault(hit, head)
# 첫 두 줄의 칸 — **괄호 안이 「…당」일 때만** 봄(8-6-1 「소형헬기 (160ha당)」).
# ⚠ 넓히면 값 칸의 몫(`5인/km` · `Q=3.5㎥/hr`)이 밑수로 둔갑함.
if not found:
for cell in [c for row in table["rows"][:2] for c in row]:
for m in _PAREN_ANY.finditer(cell):
if m.group(1).strip().endswith("") and isinstance(hit := parse_basis(m.group(1)), tuple): # fmt: skip
found.setdefault(hit, cell)
if len(found) == 1:
((hit, head),) = found.items()
return hit, f"표머리 {head}"
return hit, f"·머리 {head}"
# 표 아래 산출식 글에만 적힌 밑수 — `∙ 드론운반공 / 1,000본 = ④ × X ÷ 360 ÷ 5`(10-13)
for k in range(table["end"], min(len(lines), table["end"] + 12)):
text = lines[k].strip()
if text.startswith(("|", "#")):
break
if m := _PER_IN_FORMULA.search(text):
hit = parse_basis(f"{m.group(1)}{m.group(2)}")
if isinstance(hit, tuple):
return hit, text
return None, ""
#: 표 아래 산출식에 「/ N단위 =」 꼴로만 적힌 밑수(10-13 드론·인력 운반).
_PER_IN_FORMULA = re.compile(rf"/\s*([\d,]+)\s*({_UNIT})\s*=")
_NOTE_CONT = re.compile(r"^(-\s*[①-⑳]|-\s*-|[※*·])")
#: `- 4. 고철공제 : A=1㎥×0.008×…` — 번호 목록이지만 **[주] 안쪽 규칙**. 콜론·등호로 알맹이가 이어짐.
#: ⚠ `- 3. 노상 및 노반재료` 처럼 알맹이 없는 짧은 줄은 **새 절 제목**이라 [주]가 아님
#: (2026-09-18 데스크탑 서브 제안 · 랩탑 서브가 원문 전수로 「1.」「가.」 꼴 [주] 0건 확인).
_NOTE_NUMBERED = re.compile(r"^-\s*(?:\d+|[가-하])\.\s*[^:=]*[:=]")
def md_notes(lines: list[str], table: dict) -> list[str]:
@@ -166,7 +199,8 @@ def md_notes(lines: list[str], table: dict) -> list[str]:
continue
if not out and s.startswith("[주"):
out.append(s)
elif out and _NOTE_CONT.match(s):
elif out and (_NOTE_CONT.match(s) or _NOTE_NUMBERED.match(s) or s.startswith("[주")):
# 번호 항목 안에 다시 `[주]①` 이 붙는 자리(9-8-2 집토) — 끊지 않고 이어 봄
out.append(s)
else:
break
+2 -1
View File
@@ -81,7 +81,8 @@ def test_건설_공종_축도_같은_길(client: TestClient) -> None:
table = _get(client, "work_item_const", size=500, axis_role="")
# 2026-09-18 997/1,293 그대로 — 앞서 991/1,287 로 보였던 것은 **산출 파일이 옛 것**이어서였다
# (기계 다리가 선 뒤 다시 지으니 제자리로 돌아옴).
assert table["stats"] == {"work_items": 997, "units": 1293} and table["total"] == 1293
# 2026-09-18 랩탑 메인 — 건설 표 모양 판정층으로 미판정 891 → 276 이 되며 품 표가 늘어 줄도 늘었다.
assert table["stats"] == {"work_items": 997, "units": 1334} and table["total"] == 1334
assert sum(1 for r in table["rows"] if "#" in r["@id"]) > 0
rows = table["rows"]
assert all(r["@id"].startswith("CW-") for r in rows)