Files
Aislo/B08_Quantity/B08_Quantity_Build_WorkItemMaster_FormsConst.py
T
eomsangdonandClaude Opus 5 83220dc231 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
2026-09-18 09:07:49 +09:00

127 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""공종 마스터 — **건설 품셈 표 모양 판정**(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