브레인 일감. 표 모양 판정이 산림 표에 맞춰져 있어 건설 표를 못 가렸음(건설 미판정 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
127 lines
7.7 KiB
Python
127 lines
7.7 KiB
Python
"""공종 마스터 — **건설 품셈 표 모양 판정**(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
|