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:
@@ -82,12 +82,20 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
|
||||
from B09_Estimation.B09_Estimation_MachineOperating import load_fuel_price
|
||||
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||||
LOSS_ONLY_MACHINES,
|
||||
OPERATOR_PROVISIONAL_NOTE,
|
||||
load_operating_records,
|
||||
load_operator_wages,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_WoodChipping import MACHINE as CHIPPER
|
||||
from B09_Estimation.B09_Estimation_WoodChipping import operating_record as chipper_record
|
||||
|
||||
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:
|
||||
chipper = chipper_record()
|
||||
if chipper is not None:
|
||||
operating[CHIPPER] = chipper
|
||||
wages = load_operator_wages()
|
||||
loss = _loss_records()
|
||||
|
||||
@@ -170,6 +178,13 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
|
||||
str(COMBINED_MISC_PERCENT) if variant == "조합" else _money(misc_percent)
|
||||
),
|
||||
"operator_code": occupation,
|
||||
# 8-1-2 5호가 이 기종을 이름으로 안 가르면 잠정 사유(원문이 안 가르는 것을 우리가 안 가름).
|
||||
"operator_note": (
|
||||
OPERATOR_PROVISIONAL_NOTE
|
||||
if wage is not None
|
||||
and getattr(record, "operator_mapping_is_provisional", False)
|
||||
else ""
|
||||
),
|
||||
"operator_daily_wage": _money(wage),
|
||||
# 식 좌→우 순차 + 원 미만 절사(명세 7장) — 계수로 접으면 1원 틀림.
|
||||
"operator_krw_per_hour": _money(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface MachineExpenseDto {
|
||||
fuel_scope: string;
|
||||
misc_material_percent: string | null;
|
||||
operator_code: string;
|
||||
operator_note: string;
|
||||
operator_daily_wage: string | null;
|
||||
operator_krw_per_hour: string | null;
|
||||
material_krw: string | null;
|
||||
@@ -110,6 +111,7 @@ function machineExpenseSheet(sheet: MachineExpenseDto["sheets"][number]): HTMLEl
|
||||
);
|
||||
}
|
||||
if (sheet.attachment_note) box.append(note(sheet.attachment_note));
|
||||
if (sheet.operator_note) box.append(note(sheet.operator_note));
|
||||
for (const gap of sheet.gaps) box.append(note(`⚠ ${gap}`));
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""조종원 직종 — 건설품셈 8-1-2 5호 「운전사의 구분」 (2026-09-15 브레인 · 잰 틀림 12 · 조립에 선 것 2).
|
||||
|
||||
원문: 건설기계운전사 「…덤프트럭(12ton이상) … 콘크리트 믹서(0.55㎥ 이상) … 공기압축기(이동식, 2.83㎥/min 이상)」
|
||||
화물차운전사 「12ton미만의 덤프트럭, 화물트럭, 살수차 …」
|
||||
일반기계운전사 「소형의 공기압축기, 양수기, 소형믹서, 윈치 … 발전기, 래머, 콤팩터 …」
|
||||
⚠ 옛 잠정 규칙(이름에 트럭 들면 화물차 · 그 밖 전부 건설기계) — 덤프 15t 이 화물차 · 콤팩터·래머·소형믹서가 건설기계로 섰음.
|
||||
거창 실무 기계경비 「덤프트럭 15Ton 건설기계운전사 · 8Ton·4.5Ton 화물차운전사 · 플레이트 콤팩터 일반기계운전사」 = 원문.
|
||||
원문이 이름으로 안 가르는 기종(콘크리트 펌프차 · 트랙터(타이어) · 콘크리트 믹서트럭 …)은 **잠정 그대로 + 사유**.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from B09_Estimation.B09_Estimation_MachineExpenseSheet import machine_expense_sheets
|
||||
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||||
_CATALOG_SUBPATH,
|
||||
_read_json,
|
||||
load_operating_records,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||||
|
||||
ALIASES = _read_json(*_CATALOG_SUBPATH, "labor_const_2026-01-01.json")["variables"]["aliases"]
|
||||
CONST, TRUCK, GENERAL = (
|
||||
ALIASES[k] for k in ("labor_op_const", "labor_op_truck", "labor_op_general")
|
||||
)
|
||||
|
||||
|
||||
def _records():
|
||||
return {r.machine_code: r for r in load_operating_records().records}
|
||||
|
||||
|
||||
def test_원문이_가르는_직종() -> None:
|
||||
records = _records()
|
||||
expected = {
|
||||
"0602-0150": CONST, # 덤프트럭 15t — 12ton 이상
|
||||
"0602-0320": CONST,
|
||||
"0602-0045": TRUCK, # 덤프트럭 4.5t — 12ton 미만
|
||||
"7204-0055": TRUCK, # 살수차
|
||||
"1730-0015": GENERAL, # 플레이트 콤팩터
|
||||
"1630-0080": GENERAL, # 래머
|
||||
"4205-0045": GENERAL, # 콘크리트 믹서 0.45㎥ — 소형믹서(0.55 미만)
|
||||
"0201-0070": CONST, # 굴착기
|
||||
"5205-0035": CONST, # 공기압축기(이동식) 3.5 — 2.83 이상
|
||||
}
|
||||
for code, occupation in expected.items():
|
||||
record = records[code]
|
||||
assert record.operator_occupation_code == occupation, record
|
||||
assert record.operator_mapping_is_provisional is False, record
|
||||
|
||||
|
||||
def test_원문이_안_가르는_기종은_잠정_그대로() -> None:
|
||||
records = _records()
|
||||
for code in ("2602-0015", "4304-0060"): # 트랙터(타이어) · 콘크리트 믹서트럭
|
||||
assert records[code].operator_mapping_is_provisional is True, records[code]
|
||||
assert records["2602-0015"].operator_occupation_code == CONST # 옛 규칙 그대로
|
||||
assert records["4304-0060"].operator_occupation_code == TRUCK
|
||||
|
||||
|
||||
def test_조립에_선_둘이_원문_직종으로_섬() -> None:
|
||||
build = cached_build(dump_haul_m=("164.23",))
|
||||
dump = {r.ref_code for r in build.book.details["X-0602-0150"]}
|
||||
compactor = {r.ref_code for r in build.book.details["X-1730-0015"]}
|
||||
assert f"{CONST}#시간" in dump, dump
|
||||
assert f"{GENERAL}#시간" in compactor, compactor
|
||||
|
||||
|
||||
def test_잠정_직종은_중기경비_장에_사유() -> None:
|
||||
sheets = {s["machine_code"]: s for s in machine_expense_sheets(cached_build())}
|
||||
assert sheets["0201-0070"]["operator_note"] == "" # 굴착기 — 원문이 가름
|
||||
note = sheets["7205-0125"]["operator_note"] # 이동식 임목파쇄기 — 원문 목록에 없음
|
||||
assert "잠정" in note and "8-1-2" in note, note
|
||||
Reference in New Issue
Block a user