fix(b09): 조종원 직종을 8-1-2 5호 원문대로 — 덤프 12t 이상 건설기계 · 콤팩터·래머·소형믹서 일반기계(틀림 12 · 조립에 선 것 2) · 원문이 안 가르는 기종은 잠정 사유 · 파쇄기 중기경비 장 거짓 공백 걷음

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-15 04:48:36 +09:00
co-authored by Claude Opus 5
parent 329bdc5c90
commit 2269266910
4 changed files with 156 additions and 6 deletions
@@ -47,6 +47,32 @@ _RE_FUEL_WITH_KIND = re.compile(r"^(휘발유|중유|경유)?(\d+(?:\.\d+)?)$")
_OPERATOR_TRUCK_WORDS = ("덤프트럭", "트럭", "트레일러", "화물", "살수차")
_OPERATOR_ALIAS_TRUCK = "labor_op_truck"
_OPERATOR_ALIAS_CONSTRUCTION = "labor_op_const"
_OPERATOR_ALIAS_GENERAL = "labor_op_general"
#: ⭐ 건설품셈 8-1-2 5호 「운전사의 구분」 — **원문이 이름으로 가르는 기종만** (2026-09-15 브레인).
#: 옛 잠정 규칙이 덤프 15t 을 화물차 · 콤팩터·래머·소형믹서를 건설기계로 세우던 틀림 12(조립에 선 것 2).
#: 거창 실무 「덤프 15Ton 건설기계 · 8·4.5Ton 화물차 · 플레이트 콤팩터 일반기계운전사」 = 원문.
#: ⚠ 원문이 안 가르는 기종(콘크리트 펌프차 · 트랙터(타이어) · 믹서트럭 …)은 **우리가 가르지 않음** — 옛 규칙 + 잠정 사유.
_CLASS_CONSTRUCTION_WORDS = (
"불도저", "굴착기", "로더", "지게차", "스크레이퍼", "기중기", "모터그레이더", "롤러",
"노상안정기", "콘크리트배치플랜트", "콘크리트피니셔", "콘크리트스프레더", "아스팔트믹싱플랜트",
"아스팔트피니셔", "아스팔트살포기", "슬러리실", "골재살포기", "쇄석기", "천공기", "사리채취기",
"노면파쇄기",
) # fmt: skip
_CLASS_TRUCK_WORDS = ("화물트럭", "살수차", "제설차", "노면청소차", "트럭탑재형크레인")
_CLASS_GENERAL_WORDS = (
"양수기",
"윈치",
"벨트컨베이어",
"발전기",
"래머",
"콤팩터",
"콘크리트파쇄기",
)
OPERATOR_PROVISIONAL_NOTE = (
"조종원 직종 잠정 — 건설품셈 8-1-2 5호 「운전사의 구분」 이 이 기종을 이름으로 안 가름"
" (이름에 트럭이 들면 화물차운전사 · 그 밖은 건설기계운전사로 둠)"
)
#: ⚠ **셀 병합으로 줄째 버려지는 표에서 원문 값을 되살린다** (2026-09-15 초류종자살포 ③ 브레인).
@@ -181,14 +207,45 @@ def _parse_person_days(token: str) -> Decimal | None:
return Decimal(text) if _RE_DECIMAL.match(text) else None
def _operator_code(machine_name: str, aliases: dict[str, str]) -> str:
"""기종 이름으로 운전사 직종을 고른다 — **잠정 규칙**.
def operator_class(machine_name: str, specification: str = "") -> str | None:
"""8-1-2 5호가 이름·규격으로 가르는 직종 별칭 키 — 원문이 안 가르면 `None`."""
name = re.sub(r"\s", "", machine_name)
found = re.search(r"\d+(?:\.\d+)?", str(specification).replace(",", ""))
size = Decimal(found.group(0)) if found else None
if name.startswith("덤프트럭"):
# 「덤프트럭(12ton이상)」 건설기계 · 「12ton미만의 덤프트럭」 화물차
if size is None:
return None
return _OPERATOR_ALIAS_CONSTRUCTION if size >= 12 else _OPERATOR_ALIAS_TRUCK
if name.startswith("콘크리트믹서") and "트럭" not in name:
# 「콘크리트 믹서(0.55㎥ 이상)」 건설기계 · 「소형믹서」 일반기계
if size is None:
return None
return _OPERATOR_ALIAS_CONSTRUCTION if size >= Decimal("0.55") else _OPERATOR_ALIAS_GENERAL
if name.startswith("공기압축기"):
# 「공기압축기(이동식, 2.83㎥/min 이상)」 건설기계 · 「소형의 공기압축기」 일반기계
if size is None or "이동식" not in name:
return None
return _OPERATOR_ALIAS_CONSTRUCTION if size >= Decimal("2.83") else _OPERATOR_ALIAS_GENERAL
# 괄호 앞 이름의 **끝말**로 — 속에 든 글자로 보면 「크롤러드릴」 이 롤러 · 「타워크레인」 이 기중기로 걸림.
base = re.sub(r"[(].*", "", name)
if any(word in name for word in _CLASS_TRUCK_WORDS): # 「물탱크(살수차)」 — 괄호 속 이름
return _OPERATOR_ALIAS_TRUCK
if base.endswith(_CLASS_GENERAL_WORDS):
return _OPERATOR_ALIAS_GENERAL
if base == "크레인" or base.endswith(_CLASS_CONSTRUCTION_WORDS): # 「기중기(차륜 및 무한궤도)」
return _OPERATOR_ALIAS_CONSTRUCTION
return None
def _operator_code(machine_name: str, aliases: dict[str, str], specification: str = "") -> str:
"""기종 이름으로 운전사 직종을 고른다 — 8-1-2 5호가 가르면 원문, 아니면 **잠정 규칙**.
품셈 8-4 표는 「조종원 인/일」 수만 주고 직종명을 안 준다. 노임표의 `aliases` 가
운전사 직종 셋(`labor_op_const`·`labor_op_truck`·`labor_op_general`)을 들고 있
트럭 계열만 화물차운전사로, 나머지는 건설기계운전사로 **잠정** 매핑한다.
운전사 직종 셋(`labor_op_const`·`labor_op_truck`·`labor_op_general`)을 들고 있음.
원문이 안 가르는 기종은 트럭 계열만 화물차운전사로, 나머지는 건설기계운전사로 **잠정** 매핑한다.
"""
key = (
key = operator_class(machine_name, specification) or (
_OPERATOR_ALIAS_TRUCK
if any(word in machine_name for word in _OPERATOR_TRUCK_WORDS)
else _OPERATOR_ALIAS_CONSTRUCTION
@@ -288,7 +345,13 @@ def enrich_with_catalog(
fuel_kind=record.fuel_kind,
misc_material_percent=record.misc_material_percent,
operator_person_days=record.operator_person_days,
operator_occupation_code=_operator_code(machine.name, aliases),
operator_occupation_code=_operator_code(
machine.name, aliases, machine.specification or record.specification
),
operator_mapping_is_provisional=operator_class(
machine.name, machine.specification or record.specification
)
is None,
)
)
result.records = enriched