feat(z01·b09): 코드에 박혀 있던 기계 작업량 밑값을 마스터로 꺼냄 — 새 자료 data_machine_productivity(불도저 속도 25 · 삽날 10 · 덤프 운반·적재 밑값 9 · 재료별 3) · B09 는 그 파일을 읽음(값은 한 칸도 안 바꿈 · 원문 pum_const C0426·C0427 대조 시험) · Z01 kind machine_productivity 로 관리자가 고칠 수 있게 · 식은 값이 아니라 로직이라 코드에 남김 · 이름표에 없는 새 자료는 폴더에서 찾음(사용자 원칙 「마스터 = 관리자가 제어할 값 · 코드 안에 있으면 안 됨」 · 브레인)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""B09 — 기계 작업량 **밑값**을 마스터에서 읽는다(2026-09-16 사용자 원칙 · 브레인 지시).
|
||||
|
||||
마스터 = 관리자가 제어할 값 · **코드 안에 있으면 안 됨.**
|
||||
|
||||
그동안 불도저 속도·삽날 용량(건설품셈 8-2-1)과 덤프 운반·적재 식 계수(산림품셈 10-12)가
|
||||
코드 상수로 박혀 있었다. 값은 그대로 두고 자리만 옮긴다 —
|
||||
`resources/data_machine_productivity/machine_productivity_<판>.json`.
|
||||
⚠ **식은 여기 없다.** `Q = n·q·f·E` 같은 식은 값이 아니라 로직이라 계산 모듈에 남는다.
|
||||
⚠ 옮기면서 값을 하나도 안 바꿨다 — 원문(pum_const C0426·C0427)과의 대조는 시험이 지킨다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DATA_DIR = Path(__file__).resolve().parent.parent / "resources" / "data_machine_productivity"
|
||||
PREFIX = "machine_productivity_"
|
||||
|
||||
|
||||
class MachineProductivityDataError(FileNotFoundError):
|
||||
"""밑값 판이 없다 — 지어내지 않고 멈춘다."""
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _read(path: str, _mtime_ns: int) -> dict[str, Any]:
|
||||
return json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def load() -> dict[str, Any]:
|
||||
"""끝 판 한 벌(이름 차례)."""
|
||||
found = sorted(DATA_DIR.glob(PREFIX + "*.json")) if DATA_DIR.is_dir() else []
|
||||
if not found:
|
||||
raise MachineProductivityDataError(f"기계 작업량 밑값 판이 없습니다: {DATA_DIR}")
|
||||
return _read(str(found[-1]), found[-1].stat().st_mtime_ns)
|
||||
|
||||
|
||||
def records(variable: str) -> list[dict[str, Any]]:
|
||||
return load()["variables"][variable]["records"]
|
||||
|
||||
|
||||
def dozer_speeds() -> dict[str, dict[Decimal, dict[int, tuple[Decimal, Decimal]]]]:
|
||||
"""{무한궤도·타이어: {규격(ton): {단: (전진, 후진)}}} — 후진이 원문에 없는 단은 담기지 않는다."""
|
||||
out: dict[str, dict[Decimal, dict[int, tuple[Decimal, Decimal]]]] = {}
|
||||
for row in records("dozer_speed"):
|
||||
track = out.setdefault(row["track"], {})
|
||||
gears = track.setdefault(Decimal(row["tonnage_ton"]), {})
|
||||
gears[int(row["gear"])] = (
|
||||
Decimal(row["forward_m_per_min"]),
|
||||
Decimal(row["reverse_m_per_min"]),
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def dozer_blades() -> dict[tuple[str, Decimal], Decimal]:
|
||||
"""{(무한궤도·타이어, 규격(ton)): 삽날 용량 q˚(㎥)}."""
|
||||
return {
|
||||
(row["track"], Decimal(row["tonnage_ton"])): Decimal(row["blade_m3"])
|
||||
for row in records("dozer_blade")
|
||||
}
|
||||
|
||||
|
||||
def haul_params() -> dict[str, Decimal]:
|
||||
"""덤프 운반·적재 식이 쓰는 밑값(적재 규격 T · 버켓 q · 싸이클 · 주행속도 · 적하·대기·덮개)."""
|
||||
return {row["key"]: Decimal(row["value"]) for row in records("dump_haul")}
|
||||
@@ -23,6 +23,7 @@ from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation import B09_Estimation_MachineProductivity_Data as data
|
||||
from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
|
||||
from B09_Estimation.B09_Estimation_MachineProductivity import (
|
||||
CycleFactors,
|
||||
@@ -38,47 +39,13 @@ _ZERO = Decimal(0)
|
||||
|
||||
|
||||
#: 전진·후진 속도 (m/분) — 8-2-1 2.가·나. **단(gear)마다 다르다.**
|
||||
#: ⭐ 값은 마스터 `resources/data_machine_productivity/` 에 있다 — 여기 박지 않는다(사용자 원칙).
|
||||
#: {규격(ton): {단: (전진, 후진)}}
|
||||
#: ⚠ 무한궤도 4·13 톤과 타이어 전 규격은 **후진 3단이 표에 없다**(「-」) — 그 단은
|
||||
#: 아예 안 담는다. 담으면 없는 속도로 싸이클이 서서 값이 조용히 틀린다.
|
||||
#: ⚠ **단은 작업이 정한다** (8-2-1 2.가 [주]) — 굴착·굴착운반 1단, 흐트러진 토사운반
|
||||
#: 2단, 평탄 정지·전압 3단. 그래서 임도 표가 「55m/분(2단)」처럼 단을 적어 온다.
|
||||
_DOZER_SPEEDS = {
|
||||
"무한궤도": {
|
||||
Decimal("4"): {1: (Decimal(40), Decimal(63)), 2: (Decimal(57), Decimal(85))},
|
||||
Decimal("7"): {
|
||||
1: (Decimal(43), Decimal(53)),
|
||||
2: (Decimal(67), Decimal(78)),
|
||||
3: (Decimal(92), Decimal(107)),
|
||||
},
|
||||
Decimal("10"): {
|
||||
1: (Decimal(42), Decimal(50)),
|
||||
2: (Decimal(64), Decimal(75)),
|
||||
3: (Decimal(88), Decimal(105)),
|
||||
},
|
||||
Decimal("12"): {
|
||||
1: (Decimal(40), Decimal(48)),
|
||||
2: (Decimal(55), Decimal(70)),
|
||||
3: (Decimal(75), Decimal(100)),
|
||||
},
|
||||
Decimal("13"): {1: (Decimal(40), Decimal(48)), 2: (Decimal(55), Decimal(70))},
|
||||
Decimal("19"): {
|
||||
1: (Decimal(40), Decimal(46)),
|
||||
2: (Decimal(55), Decimal(70)),
|
||||
3: (Decimal(75), Decimal(98)),
|
||||
},
|
||||
Decimal("32"): {
|
||||
1: (Decimal(40), Decimal(43)),
|
||||
2: (Decimal(52), Decimal(58)),
|
||||
3: (Decimal(70), Decimal(78)),
|
||||
},
|
||||
},
|
||||
"타이어": {
|
||||
Decimal("15"): {1: (Decimal(83), Decimal(92)), 2: (Decimal(200), Decimal(125))},
|
||||
Decimal("28"): {1: (Decimal(92), Decimal(92)), 2: (Decimal(200), Decimal(200))},
|
||||
Decimal("33"): {1: (Decimal(92), Decimal(110)), 2: (Decimal(210), Decimal(250))},
|
||||
},
|
||||
}
|
||||
_DOZER_SPEEDS = data.dozer_speeds() # 마스터에서 읽음 — 값은 그대로(2026-09-16 꺼냄)
|
||||
#: 기어 변속시간 (분) — 8-2-1 「t: 기어 변속시간(0.25분)」
|
||||
_DOZER_GEAR_SHIFT_MIN = Decimal("0.25")
|
||||
_MINUTES_PER_HOUR = Decimal(60)
|
||||
@@ -144,18 +111,7 @@ def dozer_speeds(
|
||||
|
||||
#: 삽날 용량 q゚(㎥) — 8-2-1 1.가. **규격을 되짚는 열쇠**로도 쓴다.
|
||||
#: ⚠ 무한궤도 10 톤과 13 톤이 둘 다 1.5 ㎥ 라 **용량만으로는 못 가른다** — 속도로 마저 가른다.
|
||||
_DOZER_BLADE_M3 = {
|
||||
("무한궤도", Decimal("4")): Decimal("0.5"), # 초습지
|
||||
("무한궤도", Decimal("7")): Decimal("1.1"),
|
||||
("무한궤도", Decimal("10")): Decimal("1.5"),
|
||||
("무한궤도", Decimal("12")): Decimal("2.0"),
|
||||
("무한궤도", Decimal("13")): Decimal("1.5"), # 습지
|
||||
("무한궤도", Decimal("19")): Decimal("3.2"),
|
||||
("무한궤도", Decimal("32")): Decimal("5.5"),
|
||||
("타이어", Decimal("15")): Decimal("3.1"),
|
||||
("타이어", Decimal("28")): Decimal("4.0"),
|
||||
("타이어", Decimal("33")): Decimal("5.7"),
|
||||
}
|
||||
_DOZER_BLADE_M3 = data.dozer_blades() # 마스터에서 읽음
|
||||
|
||||
#: 습지·초습지 갈래는 카탈로그 이름이 따로다 — 「습지 불도저」.
|
||||
_DOZER_WET_TONS = (Decimal("4"), Decimal("13"))
|
||||
|
||||
@@ -31,19 +31,23 @@ from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation import B09_Estimation_MachineProductivity_Data as data
|
||||
from B09_Estimation.B09_Estimation_MachineProductivity import fix2
|
||||
|
||||
#: 「[주] 장비는 덤프트럭(15ton)을 적용한다」 — 기계 카탈로그 덤프트럭 15.
|
||||
DUMP_TRUCK_CODE = "0602-0150"
|
||||
DUMP_PARENT = "FP-10-12"
|
||||
|
||||
_TRUCK_TON = Decimal(15)
|
||||
_BUCKET_M3 = Decimal("0.7")
|
||||
_LOADER_CYCLE_SEC = Decimal(20)
|
||||
_V_LOADED_KMH = Decimal(5)
|
||||
_V_EMPTY_KMH = Decimal(6)
|
||||
_T3_UNLOAD, _T4_WAIT, _T5_COVER = Decimal("1.1"), Decimal("0.9"), Decimal("0.5")
|
||||
_SIXTY = Decimal(60)
|
||||
#: ⭐ 식이 쓰는 밑값은 **마스터**에 있다 — `resources/data_machine_productivity/`(2026-09-16 꺼냄).
|
||||
#: 식(Q·㎝t·n)은 값이 아니라 로직이라 여기 남는다.
|
||||
_HAUL = data.haul_params()
|
||||
_TRUCK_TON = _HAUL["truck_ton"]
|
||||
_BUCKET_M3 = _HAUL["bucket_m3"]
|
||||
_LOADER_CYCLE_SEC = _HAUL["loader_cycle_sec"]
|
||||
_V_LOADED_KMH = _HAUL["speed_loaded_kmh"]
|
||||
_V_EMPTY_KMH = _HAUL["speed_empty_kmh"]
|
||||
_T3_UNLOAD, _T4_WAIT, _T5_COVER = _HAUL["unload_min"], _HAUL["wait_min"], _HAUL["cover_min"]
|
||||
_SIXTY = Decimal(60) # 분↔시간 환산 — 값이 아니라 단위
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -66,37 +70,33 @@ _COMMON_NOTE = (
|
||||
_ES_NOTE = "Es = 운반 줄 값(적재 식의 E0 와 다른 기호)"
|
||||
|
||||
DUMP_MATERIALS: dict[str, DumpMaterial] = {
|
||||
"FP-10-12-01": DumpMaterial(
|
||||
"FP-10-12-01", "토사", Decimal("1.9"), Decimal("1.3"), Decimal("0.9"), Decimal("0.85"),
|
||||
Decimal("0.9"), (_COMMON_NOTE, _ES_NOTE),
|
||||
),
|
||||
"FP-10-12-02": DumpMaterial(
|
||||
"FP-10-12-02", "암절취", Decimal("2.4"), Decimal("1.35"), Decimal("0.55"), Decimal("0.35"),
|
||||
Decimal("0.9"), (_COMMON_NOTE + " · 원문 10 이면 n 1.18배", _ES_NOTE),
|
||||
),
|
||||
"FP-10-12-03": DumpMaterial(
|
||||
"FP-10-12-03", "발파암", Decimal("2.4"), Decimal("1.625"), Decimal("0.55"), Decimal("0.35"),
|
||||
Decimal("0.9"),
|
||||
(_COMMON_NOTE, _ES_NOTE, "E 원문 누락 — 0.9 적용(같은 절 토사·암절취 · 덤프 작업효율)"),
|
||||
),
|
||||
} # fmt: skip
|
||||
row["work_item_code"]: DumpMaterial(
|
||||
row["work_item_code"],
|
||||
row["label"],
|
||||
Decimal(row["unit_weight_ton_per_m3"]),
|
||||
Decimal(row["loose_factor"]),
|
||||
Decimal(row["bucket_factor"]),
|
||||
Decimal(row["loader_efficiency"]),
|
||||
Decimal(row["truck_efficiency"]),
|
||||
tuple(row.get("notes") or ()),
|
||||
)
|
||||
for row in data.records("dump_material")
|
||||
}
|
||||
|
||||
|
||||
#: 「1. 적재」 — Q1 = 3600 × q0 × K × f × E0 / ㎝ · 굴착기(무한궤도) 0.7㎥ · ㎝ 22초(180°).
|
||||
#: ⚠ E0 는 운반 식의 Es 와 **다른 기호**다(판정 ④⑥). 토사 본문 「E0=0.75」 는 표(10-12-1 [주]⑤
|
||||
#: 「E0 토사 0.60(불량) — 임도」)·10-12-3 [주]③(「토사 0.6」)과 어긋나 **0.60 채택 · 0.75 버림**.
|
||||
LOADER_CODE = "0201-0070"
|
||||
_LOADING_CYCLE_SEC = Decimal(22)
|
||||
_LOADING_CYCLE_SEC = _HAUL["loading_cycle_sec"]
|
||||
_LOADING_E0 = {
|
||||
"FP-10-12-01": Decimal("0.60"),
|
||||
"FP-10-12-02": Decimal("0.35"),
|
||||
"FP-10-12-03": Decimal("0.35"),
|
||||
row["work_item_code"]: Decimal(row["loading_efficiency"])
|
||||
for row in data.records("dump_material")
|
||||
}
|
||||
#: 적재 E0 사유 — 마스터가 줄에 따로 들고 있음(운반 줄 사유와 섞지 않음).
|
||||
_LOADING_NOTES = {
|
||||
"FP-10-12-01": "E0 = 0.60(표 「임도」·10-12-3 [주]③) — 본문 표기 0.75 버림",
|
||||
"FP-10-12-02": "E0 = 0.35(본문·표 「파쇄암」 같음)",
|
||||
"FP-10-12-03": "E0 = 0.35(10-12-3 [주]③ 「파쇄암 0.35」)",
|
||||
} # fmt: skip
|
||||
row["work_item_code"]: row.get("loading_note", "") for row in data.records("dump_material")
|
||||
}
|
||||
|
||||
|
||||
def loading_output(code: str) -> Decimal:
|
||||
|
||||
@@ -45,12 +45,16 @@ _RATE_NOTICE = "법이 정한 값 — 고치면 원본 갱신 때 구간 이름
|
||||
|
||||
|
||||
def _source(file_id: str) -> Path:
|
||||
"""원본 파일 자리 — 이름표가 가리키는 곳이 먼저, 아직 이름표에 없는 새 자료는 폴더에서 찾음."""
|
||||
labels = tables.load_labels()
|
||||
entry = next((f for f in labels["files"] if f["file_id"] == file_id), None)
|
||||
path = tables._file_path(entry) if entry else None
|
||||
if path is None:
|
||||
raise FileNotFoundError(f"이름표에 원본 자리가 없음: {file_id}")
|
||||
return path
|
||||
if path is not None:
|
||||
return path
|
||||
found = sorted(tables.RESOURCES.glob(f"data_*/{file_id}*.json"))
|
||||
if not found:
|
||||
raise FileNotFoundError(f"원본 자리를 못 찾음: {file_id}")
|
||||
return found[-1]
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
|
||||
@@ -281,6 +281,73 @@ def _masonry_class(doc: dict[str, Any], name: str) -> list[_ROW]:
|
||||
return rows
|
||||
|
||||
|
||||
def _machine_productivity(doc: dict[str, Any], name: str) -> list[_ROW]:
|
||||
"""기계 작업량 밑값 — 코드에 박혀 있던 값을 꺼낸 자리(불도저 속도·삽날 · 덤프 운반·적재 계수).
|
||||
|
||||
⚠ 식(`Q = n·q·f·E` 따위)은 값이 아니라 로직이라 계산 모듈에 남는다 — 여기는 계수만.
|
||||
"""
|
||||
rows = []
|
||||
for r in doc["variables"]["dozer_speed"]["records"]:
|
||||
rows.append(
|
||||
_row(
|
||||
f"dozer_speed/{r['track']}/{r['tonnage_ton']}/{r['gear']}",
|
||||
name,
|
||||
("table", "track", "tonnage_ton", "gear"),
|
||||
table="dozer_speed",
|
||||
track=r["track"],
|
||||
tonnage_ton=float(r["tonnage_ton"]),
|
||||
gear=int(r["gear"]),
|
||||
forward_m_per_min=float(r["forward_m_per_min"]),
|
||||
reverse_m_per_min=float(r["reverse_m_per_min"]),
|
||||
)
|
||||
)
|
||||
for r in doc["variables"]["dozer_blade"]["records"]:
|
||||
rows.append(
|
||||
_row(
|
||||
f"dozer_blade/{r['track']}/{r['tonnage_ton']}",
|
||||
name,
|
||||
("table", "track", "tonnage_ton"),
|
||||
table="dozer_blade",
|
||||
track=r["track"],
|
||||
tonnage_ton=float(r["tonnage_ton"]),
|
||||
blade_m3=float(r["blade_m3"]),
|
||||
)
|
||||
)
|
||||
for r in doc["variables"]["dump_haul"]["records"]:
|
||||
rows.append(
|
||||
_row(
|
||||
f"dump_haul/{r['key']}",
|
||||
name,
|
||||
("table", "key"),
|
||||
table="dump_haul",
|
||||
key=r["key"],
|
||||
value=float(r["value"]),
|
||||
unit=r.get("unit"),
|
||||
meaning=r.get("meaning"),
|
||||
)
|
||||
)
|
||||
for r in doc["variables"]["dump_material"]["records"]:
|
||||
rows.append(
|
||||
_row(
|
||||
f"dump_material/{r['work_item_code']}",
|
||||
name,
|
||||
("table", "work_item_code"),
|
||||
table="dump_material",
|
||||
work_item_code=r["work_item_code"],
|
||||
label=r["label"],
|
||||
unit_weight_ton_per_m3=float(r["unit_weight_ton_per_m3"]),
|
||||
loose_factor=float(r["loose_factor"]),
|
||||
bucket_factor=float(r["bucket_factor"]),
|
||||
loader_efficiency=float(r["loader_efficiency"]),
|
||||
truck_efficiency=float(r["truck_efficiency"]),
|
||||
loading_efficiency=float(r["loading_efficiency"]),
|
||||
notes=r.get("notes"),
|
||||
loading_note=r.get("loading_note"),
|
||||
)
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
#: kind → 원본 파일 id · 줄 만드는 길 · 고칠 칸. 이름은 **자료 이름 그대로**(브레인).
|
||||
SPEC: dict[str, dict[str, Any]] = {
|
||||
"coef": {"file": "coef", "build": _coef, "editable": ["min", "max"]},
|
||||
@@ -315,6 +382,22 @@ SPEC: dict[str, dict[str, Any]] = {
|
||||
"editable": ["min_cm", "max_cm"],
|
||||
},
|
||||
"stone_kind": {"file": "stone_kind", "build": _stone_kind, "editable": ["m3_per_m2", "ratio"]},
|
||||
"machine_productivity": {
|
||||
"file": "machine_productivity",
|
||||
"build": _machine_productivity,
|
||||
"editable": [
|
||||
"forward_m_per_min",
|
||||
"reverse_m_per_min",
|
||||
"blade_m3",
|
||||
"value",
|
||||
"unit_weight_ton_per_m3",
|
||||
"loose_factor",
|
||||
"bucket_factor",
|
||||
"loader_efficiency",
|
||||
"truck_efficiency",
|
||||
"loading_efficiency",
|
||||
],
|
||||
},
|
||||
"masonry_class": {
|
||||
"file": "masonry_class",
|
||||
"build": _masonry_class,
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "machine_productivity",
|
||||
"effective_date": "2026-01-01",
|
||||
"note": "기계 작업량 밑값 — 코드에 박혀 있던 값을 그대로 옮긴 벌(2026-09-16 사용자 원칙 「마스터 = 관리자가 제어할 값 · 코드 안에 있으면 안 됨」). 값은 한 칸도 안 바꿨다 — 옮기기 전후 금액이 같다.",
|
||||
"policy": {
|
||||
"no_value_changed": "코드에 있던 값을 그대로 옮겼다. 원문과의 대조는 시험이 지킨다.",
|
||||
"formula_stays_in_code": "식(Q = n·q·f·E 따위)은 값이 아니라 로직이라 코드에 둔다 — 계수만 여기 있다.",
|
||||
"source_tables_mashed": "삽날 용량표는 원문 옮기기에서 절 이름이 뭉개져(`5-3.2-5.5-타 이 어…`) 표로 안 들어왔다. 값은 원문 본문 것이고, 원문 표가 제 모양으로 들어오면 그때 대조한다."
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"doc": "건설공사 표준품셈 8-2-1 불도저 2.가·나 전진·후진 속도",
|
||||
"tables": [
|
||||
"C0426",
|
||||
"C0427"
|
||||
],
|
||||
"role": "primary"
|
||||
},
|
||||
{
|
||||
"doc": "건설공사 표준품셈 8-2-1 1.가 삽날 용량 q˚",
|
||||
"tables": [],
|
||||
"role": "primary",
|
||||
"note": "원문 표가 뭉개져 카탈로그에 표로 없음"
|
||||
},
|
||||
{
|
||||
"doc": "산림사업 표준품셈(고시 제2025-82호) 10-12 「1. 적재」·「2. 운반」 식 서식",
|
||||
"tables": [],
|
||||
"role": "primary",
|
||||
"note": "원문이 표가 아니라 채워 넣는 식이라 공종 마스터에 안 실림"
|
||||
}
|
||||
],
|
||||
"variables": {
|
||||
"dozer_speed": {
|
||||
"unit": "m/분",
|
||||
"key": "track+tonnage_ton+gear",
|
||||
"note": "후진이 「-」인 단은 원문에 값이 없어 담지 않는다(없는 속도로 싸이클이 서면 값이 조용히 틀림).",
|
||||
"records": [
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "4",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "40",
|
||||
"reverse_m_per_min": "63"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "4",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "57",
|
||||
"reverse_m_per_min": "85"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "7",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "43",
|
||||
"reverse_m_per_min": "53"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "7",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "67",
|
||||
"reverse_m_per_min": "78"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "7",
|
||||
"gear": 3,
|
||||
"forward_m_per_min": "92",
|
||||
"reverse_m_per_min": "107"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "10",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "42",
|
||||
"reverse_m_per_min": "50"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "10",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "64",
|
||||
"reverse_m_per_min": "75"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "10",
|
||||
"gear": 3,
|
||||
"forward_m_per_min": "88",
|
||||
"reverse_m_per_min": "105"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "12",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "40",
|
||||
"reverse_m_per_min": "48"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "12",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "55",
|
||||
"reverse_m_per_min": "70"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "12",
|
||||
"gear": 3,
|
||||
"forward_m_per_min": "75",
|
||||
"reverse_m_per_min": "100"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "13",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "40",
|
||||
"reverse_m_per_min": "48"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "13",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "55",
|
||||
"reverse_m_per_min": "70"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "19",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "40",
|
||||
"reverse_m_per_min": "46"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "19",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "55",
|
||||
"reverse_m_per_min": "70"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "19",
|
||||
"gear": 3,
|
||||
"forward_m_per_min": "75",
|
||||
"reverse_m_per_min": "98"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "32",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "40",
|
||||
"reverse_m_per_min": "43"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "32",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "52",
|
||||
"reverse_m_per_min": "58"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "32",
|
||||
"gear": 3,
|
||||
"forward_m_per_min": "70",
|
||||
"reverse_m_per_min": "78"
|
||||
},
|
||||
{
|
||||
"track": "타이어",
|
||||
"tonnage_ton": "15",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "83",
|
||||
"reverse_m_per_min": "92"
|
||||
},
|
||||
{
|
||||
"track": "타이어",
|
||||
"tonnage_ton": "15",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "200",
|
||||
"reverse_m_per_min": "125"
|
||||
},
|
||||
{
|
||||
"track": "타이어",
|
||||
"tonnage_ton": "28",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "92",
|
||||
"reverse_m_per_min": "92"
|
||||
},
|
||||
{
|
||||
"track": "타이어",
|
||||
"tonnage_ton": "28",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "200",
|
||||
"reverse_m_per_min": "200"
|
||||
},
|
||||
{
|
||||
"track": "타이어",
|
||||
"tonnage_ton": "33",
|
||||
"gear": 1,
|
||||
"forward_m_per_min": "92",
|
||||
"reverse_m_per_min": "110"
|
||||
},
|
||||
{
|
||||
"track": "타이어",
|
||||
"tonnage_ton": "33",
|
||||
"gear": 2,
|
||||
"forward_m_per_min": "210",
|
||||
"reverse_m_per_min": "250"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dozer_blade": {
|
||||
"unit": "㎥",
|
||||
"key": "track+tonnage_ton",
|
||||
"note": "무한궤도 10·13 톤이 둘 다 1.5 ㎥ 라 용량만으로는 못 가른다 — 속도로 마저 가른다.",
|
||||
"records": [
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "4",
|
||||
"blade_m3": "0.5"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "7",
|
||||
"blade_m3": "1.1"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "10",
|
||||
"blade_m3": "1.5"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "12",
|
||||
"blade_m3": "2.0"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "13",
|
||||
"blade_m3": "1.5"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "19",
|
||||
"blade_m3": "3.2"
|
||||
},
|
||||
{
|
||||
"track": "무한궤도",
|
||||
"tonnage_ton": "32",
|
||||
"blade_m3": "5.5"
|
||||
},
|
||||
{
|
||||
"track": "타이어",
|
||||
"tonnage_ton": "15",
|
||||
"blade_m3": "3.1"
|
||||
},
|
||||
{
|
||||
"track": "타이어",
|
||||
"tonnage_ton": "28",
|
||||
"blade_m3": "4.0"
|
||||
},
|
||||
{
|
||||
"track": "타이어",
|
||||
"tonnage_ton": "33",
|
||||
"blade_m3": "5.7"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dump_haul": {
|
||||
"key": "key",
|
||||
"note": "덤프 운반·적재 식이 쓰는 밑값. 장비 규격이 바뀌면 여기를 고친다.",
|
||||
"records": [
|
||||
{
|
||||
"key": "truck_ton",
|
||||
"value": "15",
|
||||
"unit": "ton",
|
||||
"meaning": "덤프트럭 적재 규격 T"
|
||||
},
|
||||
{
|
||||
"key": "bucket_m3",
|
||||
"value": "0.7",
|
||||
"unit": "㎥",
|
||||
"meaning": "적재기계 버켓 용량 q"
|
||||
},
|
||||
{
|
||||
"key": "loader_cycle_sec",
|
||||
"value": "20",
|
||||
"unit": "초",
|
||||
"meaning": "적재 대기 싸이클 ㎝s"
|
||||
},
|
||||
{
|
||||
"key": "speed_loaded_kmh",
|
||||
"value": "5",
|
||||
"unit": "㎞/hr",
|
||||
"meaning": "적재 주행속도 V1"
|
||||
},
|
||||
{
|
||||
"key": "speed_empty_kmh",
|
||||
"value": "6",
|
||||
"unit": "㎞/hr",
|
||||
"meaning": "공차 주행속도 V2"
|
||||
},
|
||||
{
|
||||
"key": "unload_min",
|
||||
"value": "1.1",
|
||||
"unit": "분",
|
||||
"meaning": "적하 시간 t3"
|
||||
},
|
||||
{
|
||||
"key": "wait_min",
|
||||
"value": "0.9",
|
||||
"unit": "분",
|
||||
"meaning": "대기 시간 t4"
|
||||
},
|
||||
{
|
||||
"key": "cover_min",
|
||||
"value": "0.5",
|
||||
"unit": "분",
|
||||
"meaning": "덮개 시간 t5"
|
||||
},
|
||||
{
|
||||
"key": "loading_cycle_sec",
|
||||
"value": "22",
|
||||
"unit": "초",
|
||||
"meaning": "적재 식 싸이클 ㎝(180°)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dump_material": {
|
||||
"key": "work_item_code",
|
||||
"note": "10-12 절 재료별 값 — 고른 값·버린 값 사유를 줄에 함께 둔다. 적재 사유는 `loading_note` 로 따로 둔다(운반 줄 사유와 섞지 않음).",
|
||||
"records": [
|
||||
{
|
||||
"work_item_code": "FP-10-12-01",
|
||||
"label": "토사",
|
||||
"unit_weight_ton_per_m3": "1.9",
|
||||
"loose_factor": "1.3",
|
||||
"bucket_factor": "0.9",
|
||||
"loader_efficiency": "0.85",
|
||||
"truck_efficiency": "0.9",
|
||||
"loading_efficiency": "0.60",
|
||||
"notes": [
|
||||
"n 의 Qt = 계산값 T/γt×L(원문 표기 「10/(0.7×K)」의 10 은 토사 어림수 복사로 봄 — 버림)",
|
||||
"Es = 운반 줄 값(적재 식의 E0 와 다른 기호)"
|
||||
],
|
||||
"loading_note": "E0 = 0.60(표 「임도」·10-12-3 [주]③) — 본문 표기 0.75 버림"
|
||||
},
|
||||
{
|
||||
"work_item_code": "FP-10-12-02",
|
||||
"label": "암절취",
|
||||
"unit_weight_ton_per_m3": "2.4",
|
||||
"loose_factor": "1.35",
|
||||
"bucket_factor": "0.55",
|
||||
"loader_efficiency": "0.35",
|
||||
"truck_efficiency": "0.9",
|
||||
"loading_efficiency": "0.35",
|
||||
"notes": [
|
||||
"n 의 Qt = 계산값 T/γt×L(원문 표기 「10/(0.7×K)」의 10 은 토사 어림수 복사로 봄 — 버림) · 원문 10 이면 n 1.18배",
|
||||
"Es = 운반 줄 값(적재 식의 E0 와 다른 기호)"
|
||||
],
|
||||
"loading_note": "E0 = 0.35(본문·표 「파쇄암」 같음)"
|
||||
},
|
||||
{
|
||||
"work_item_code": "FP-10-12-03",
|
||||
"label": "발파암",
|
||||
"unit_weight_ton_per_m3": "2.4",
|
||||
"loose_factor": "1.625",
|
||||
"bucket_factor": "0.55",
|
||||
"loader_efficiency": "0.35",
|
||||
"truck_efficiency": "0.9",
|
||||
"loading_efficiency": "0.35",
|
||||
"notes": [
|
||||
"n 의 Qt = 계산값 T/γt×L(원문 표기 「10/(0.7×K)」의 10 은 토사 어림수 복사로 봄 — 버림)",
|
||||
"Es = 운반 줄 값(적재 식의 E0 와 다른 기호)",
|
||||
"E 원문 누락 — 0.9 적용(같은 절 토사·암절취 · 덤프 작업효율)"
|
||||
],
|
||||
"loading_note": "E0 = 0.35(10-12-3 [주]③ 「파쇄암 0.35」)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -31,6 +33,7 @@ NEW_KINDS = (
|
||||
"masonry_slope",
|
||||
"masonry_back_length",
|
||||
"stone_kind",
|
||||
"machine_productivity", # 2026-09-16 코드에서 꺼낸 기계 작업량 밑값
|
||||
"masonry_class",
|
||||
)
|
||||
|
||||
@@ -185,3 +188,67 @@ def test_고치기도_같은_길(client: TestClient, tmp_path: Path) -> None:
|
||||
json={"values": {"reuse_count": 3}},
|
||||
)
|
||||
assert value_edit.status_code == 200 and value_edit.json()["reuse_count"] == 3
|
||||
|
||||
|
||||
def test_코드에_박혔던_기계_밑값이_마스터로_나옴(client: TestClient) -> None:
|
||||
"""사용자 원칙 — 마스터 = 관리자가 제어할 값 · **코드 안에 있으면 안 됨**(2026-09-16).
|
||||
|
||||
불도저 속도·삽날(건설품셈 8-2-1)과 덤프 운반·적재 계수(산림품셈 10-12 식 서식)를 꺼낸 자리.
|
||||
"""
|
||||
table = _get(client, "machine_productivity", size=500)
|
||||
rows = {r["@id"]: r for r in table["rows"]}
|
||||
assert table["total"] == 25 + 10 + 9 + 3
|
||||
assert rows["dozer_speed/무한궤도/7/2"]["forward_m_per_min"] == 67
|
||||
assert rows["dozer_speed/타이어/33/2"]["reverse_m_per_min"] == 250
|
||||
assert rows["dozer_blade/무한궤도/19"]["blade_m3"] == 3.2
|
||||
assert rows["dump_haul/truck_ton"]["value"] == 15
|
||||
assert rows["dump_material/FP-10-12-01"]["loose_factor"] == 1.3
|
||||
assert "forward_m_per_min" in table["editable"] and "track" in table["locked"]
|
||||
|
||||
|
||||
def test_꺼낸_값은_코드가_쓰던_것과_원문과_같음() -> None:
|
||||
"""옮기며 값이 바뀌면 금액이 조용히 달라짐 — 코드가 읽는 값과 품셈 원문 둘 다에 댐."""
|
||||
import json
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import _DOZER_SPEEDS
|
||||
from B09_Estimation.B09_Estimation_MachineProductivity_Dump import DUMP_MATERIALS
|
||||
|
||||
assert _DOZER_SPEEDS["무한궤도"][Decimal("12")][2] == (Decimal(55), Decimal(70))
|
||||
assert DUMP_MATERIALS["FP-10-12-03"].truck_efficiency == Decimal("0.9")
|
||||
from B09_Estimation import B09_Estimation_MachineProductivity_Dump as dump
|
||||
|
||||
assert (dump._TRUCK_TON, dump._BUCKET_M3, dump._LOADER_CYCLE_SEC) == (
|
||||
Decimal(15),
|
||||
Decimal("0.7"),
|
||||
Decimal(20),
|
||||
)
|
||||
assert (dump._V_LOADED_KMH, dump._V_EMPTY_KMH, dump._LOADING_CYCLE_SEC) == (
|
||||
Decimal(5),
|
||||
Decimal(6),
|
||||
Decimal(22),
|
||||
)
|
||||
pum = json.loads(
|
||||
(ROOT / "resources/data_cost_input_value/pum_const_2026.json").read_text(encoding="utf-8")
|
||||
)["variables"]["pum"]["tables"]
|
||||
by_id = {t["table_id"]: t for t in pum}
|
||||
|
||||
def source(table_id: str, gears: int) -> dict:
|
||||
out = {}
|
||||
for row in by_id[table_id]["rows"][1:]:
|
||||
ton = re.sub(r"\(.*?\)", "", str(row[0])).strip()
|
||||
if not ton.replace(".", "").isdigit():
|
||||
continue
|
||||
cells = [str(v).strip() for v in row[1:]]
|
||||
out[Decimal(ton)] = {
|
||||
i + 1: (f, r)
|
||||
for i, (f, r) in enumerate(zip(cells[:gears], cells[gears : gears * 2]))
|
||||
}
|
||||
return out
|
||||
|
||||
for track, table_id, gears in (("무한궤도", "C0426", 4), ("타이어", "C0427", 3)):
|
||||
original = source(table_id, gears)
|
||||
for ton, speeds in _DOZER_SPEEDS[track].items():
|
||||
for gear, (forward, reverse) in speeds.items():
|
||||
assert (str(forward), str(reverse)) == original[ton][gear], (track, ton, gear)
|
||||
|
||||
Reference in New Issue
Block a user