Merge remote-tracking branch 'origin/dev' into sub_laptop_1
This commit is contained in:
@@ -86,6 +86,7 @@ def chosen_conditions(settings: dict[str, Any] | None) -> list[dict[str, str]]:
|
||||
("transport_distance_km", "기계 수송 거리(편도 ㎞)"),
|
||||
("transport_road", "수송 도로 구분"),
|
||||
("transport_trips", "기계 수송 회수(대수 × 왕복)"),
|
||||
("seedlings_per_ha", "조림목 본수(본/ha · 풀베기 묘목찾기)"),
|
||||
):
|
||||
value = str(picked.get(key) or "").strip()
|
||||
if value:
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""B09 원가계산 — **풀베기 묘목찾기 = 조림목 본수 칸** (2026-09-15 브레인 차례 ② 판정 ㉠ · 지금 칸을 만듦).
|
||||
|
||||
산림 6-2-2 줄베기 · 6-2-3 모두베기 [주]④⑤ 「묘목찾기와 줄베기(모두베기) 품을 **합하여** 적용」 — 묘목찾기는
|
||||
「낫 | 인/100본 | 0.07 | 보통인부」 라 조림목 본수(본/ha · [주]② 둘레베기 참고 · 설계 입력)가 있어야 ha당이 섬.
|
||||
⇒ 본수가 들면 갈래마다 보통인부 0.07 × 본수 ÷ 100 · 비면 「일부만」 으로 막고 사유(반만 선 틀린 값보다 안 선 값).
|
||||
⚠ 제안값 없음 — 부록 예시 2,700본/ha 는 같은 부록이 품도 표와 다르게 적은 자료(묘목찾기 0.10 ↔ 표 0.07).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
#: 공종 → 표 번호(`_JudgedTable` 도구 줄 표 · 인/100본 줄이 있는 둘).
|
||||
SEEDLING_TABLES = {"FP-06-02-02": "F0155", "FP-06-02-03": "F0156"}
|
||||
NAME = "묘목찾기"
|
||||
MISSING = (
|
||||
"조림목 본수(본/ha) 미입력 — 원문 [주]④⑤ 가 묘목찾기({amount}인/100본)와 합하여 적용하라 함"
|
||||
" · 「산출 조건」 에서 넣으면 섬"
|
||||
)
|
||||
|
||||
|
||||
def parse_seedlings_per_ha(text: str | None) -> Decimal | None:
|
||||
"""조림목 본수 칸 — 빈 칸은 `None` · 0 이하·수가 아닌 값은 `ValueError`(조용히 접지 않음)."""
|
||||
cleaned = re.sub(r"[\s,]", "", str(text or ""))
|
||||
if not cleaned:
|
||||
return None
|
||||
try:
|
||||
value = Decimal(cleaned)
|
||||
except InvalidOperation as exc:
|
||||
raise ValueError(f"조림목 본수는 수로 넣어야 합니다: {text}") from exc
|
||||
if value <= 0:
|
||||
raise ValueError(f"조림목 본수는 0 보다 커야 합니다: {text}")
|
||||
return value
|
||||
|
||||
|
||||
def _per_hundred(node: dict[str, Any], table_id: str) -> tuple[Decimal, str] | None:
|
||||
"""(인/100본 값, 인력 이름) — 표에서 읽음. 칸이 달라지면 `None`."""
|
||||
from B09_Estimation.B09_Estimation_ResourceAxis import parse_amount
|
||||
|
||||
table = next((t for t in node.get("tables") or [] if t.get("pum_table_id") == table_id), {})
|
||||
for row in table.get("raw_row") or []:
|
||||
cells = ["".join(str(c).split()) for c in row]
|
||||
if "인/100본" in cells:
|
||||
unit = cells.index("인/100본")
|
||||
amount = parse_amount(cells[unit + 1]) if len(cells) > unit + 2 else None
|
||||
if amount is not None and cells[unit + 2]:
|
||||
return amount, cells[unit + 2]
|
||||
return None
|
||||
|
||||
|
||||
def attach_seedling_finding(
|
||||
build: Any, nodes: dict[str, dict[str, Any]], seedlings_per_ha: Decimal | None
|
||||
) -> None:
|
||||
"""본수가 들면 갈래 제목마다 묘목찾기 인력을 붙이고, 없으면 일부만 + 사유."""
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind
|
||||
|
||||
book = build.book
|
||||
for code, table_id in SEEDLING_TABLES.items():
|
||||
found = _per_hundred(nodes.get(code) or {}, table_id)
|
||||
titles = [t for t in book.titles if t.startswith(f"B-{code}#")]
|
||||
labels = [
|
||||
label for label in build.unattached.get(code) or [] if "".join(label.split()) != NAME
|
||||
]
|
||||
labor = next(
|
||||
(
|
||||
c
|
||||
for c, t in book.titles.items()
|
||||
if found and t.kind is PriceKind.LABOR and "#" not in c and t.name == found[1]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if found is None or labor is None or not titles:
|
||||
build.component_gaps[code] = "묘목찾기 줄 칸이 달라져 못 읽음"
|
||||
build.partial_ratio.setdefault(code, Decimal(0))
|
||||
continue
|
||||
amount, name = found
|
||||
if seedlings_per_ha is None:
|
||||
reason = MISSING.format(amount=amount)
|
||||
build.component_gaps[code] = reason
|
||||
build.partial_ratio.setdefault(code, Decimal(0))
|
||||
labels.append(reason)
|
||||
else:
|
||||
note = f"묘목찾기 {amount}인/100본 × 조림목 {seedlings_per_ha}본/ha ÷ 100 — [주]④⑤ 합하여 적용"
|
||||
for title in titles:
|
||||
book.add_detail(
|
||||
PriceDetail(title, labor, amount * seedlings_per_ha / 100, note=note)
|
||||
)
|
||||
build.unattached[code] = labels
|
||||
@@ -26,11 +26,14 @@ class Equipment:
|
||||
key: str
|
||||
machine: str # AR-X — 1대 1일 호표
|
||||
price: str # AR-M — 구입가(손료 밑수) 칸
|
||||
fuel: tuple[str, str, str] # (2-1 공종, 표, 주연료 줄 이름)
|
||||
#: (2-1 공종, 표, 주연료 줄 이름) · 연료 없는 장비(배부식분무기)는 None
|
||||
fuel: tuple[str, str, str] | None
|
||||
loss: tuple[str, str] # (2-2 공종, 표)
|
||||
choices: tuple[tuple[str, str], ...] = () # 넣은 쪽 하나 — (AR-M, 2-1 표 줄 이름)
|
||||
users: dict[str, str] = field(default_factory=dict) # 공종 → 표가 밝힌 인원 줄 이름
|
||||
basis: str = ""
|
||||
#: 도구 줄 표(6-2·6-4 「사용도구 | … | 인력구분」)의 도구 칸 이름 — 그 줄의 인력이 쓰는 사람.
|
||||
tool: str = ""
|
||||
|
||||
|
||||
EQUIPMENTS: tuple[Equipment, ...] = (
|
||||
@@ -49,6 +52,29 @@ EQUIPMENTS: tuple[Equipment, ...] = (
|
||||
users={"FP-04-02-02": "벌목부", "FP-06-05": "특별인부 (체인톱 사용)"},
|
||||
basis="산림품셈 2-1-1 「체인톱 대수는 산출된 벌목부 또는 특별인부의 100% 적용」 · 2-2-1 손료",
|
||||
),
|
||||
# 도구 줄 표 넷(2026-09-15 브레인 ②) — 2-1-1 2 [주]① 「재료비는 예취기 작업(줄베기, 모두베기, 지상부
|
||||
# 덩굴걷기)에만」 · 「1대당 1인 작업」 · 2-2-2 손료.
|
||||
Equipment(
|
||||
key="예취기",
|
||||
machine="AR-X-da716e54",
|
||||
price="AR-M-148c5888",
|
||||
fuel=("FP-02-01-01", "F0043", "예취기(휘발유)"),
|
||||
loss=("FP-02-02-02", "F0065"),
|
||||
users={"FP-06-02-02": "특별인부", "FP-06-02-03": "특별인부", "FP-06-04-01": "특별인부"},
|
||||
basis="산림품셈 2-1-1 2 예취기 「1대당 1인 작업」 · 2-2-2 손료",
|
||||
tool="예취기",
|
||||
),
|
||||
# 2-2-4 배부식분무기(덩굴 약제처리) 손료만 — 2-1 에 연료 표 없음(사람이 멤).
|
||||
Equipment(
|
||||
key="배부식분무기",
|
||||
machine="AR-X-5d0a0416",
|
||||
price="AR-M-d6394831",
|
||||
fuel=None,
|
||||
loss=("FP-02-02-04", "F0067"),
|
||||
users={"FP-06-04-02": "특별인부"},
|
||||
basis="산림품셈 2-2-4 배부식분무기 「1대당 1인 작업」 손료",
|
||||
tool="배부식분무기",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -143,20 +169,24 @@ def _machine_title(build: Any, nodes: dict, eq: Equipment, fuel_region: str | No
|
||||
|
||||
book = build.book
|
||||
code = f"X-{eq.machine}"
|
||||
fuel = _fuel_values(_table_rows(nodes, eq.fuel[0], eq.fuel[1]), eq.fuel[2])
|
||||
fuel = _fuel_values(_table_rows(nodes, eq.fuel[0], eq.fuel[1]), eq.fuel[2]) if eq.fuel else None
|
||||
loss_rows = _table_rows(nodes, *eq.loss)
|
||||
loss = _row_numbers(loss_rows, loss_rows[0][0]) if loss_rows else []
|
||||
if fuel is None or not loss:
|
||||
if (eq.fuel and fuel is None) or not loss:
|
||||
return [f"{eq.key} — 2장 표 칸이 달라져 장비 몫을 못 읽음"]
|
||||
kind = "휘발유" if "휘발유" in eq.fuel[2] else "경유"
|
||||
liters, misc = fuel
|
||||
if code in book.titles:
|
||||
return [] if eq.price in book.titles else [f"{eq.key}가격 — 손료({loss[0]} × 구입가) 칸"]
|
||||
if not eq.fuel and eq.price not in book.titles:
|
||||
# 연료 없는 장비는 손료가 전부 — 구입가가 없으면 빈 호표가 되어 제목을 안 세움(칸 사유만).
|
||||
return [f"{eq.key}가격 — 손료({loss[0]} × 구입가) 칸 · 「자재 단가」 에 넣으면 붙음"]
|
||||
book.add_title(PriceTitle(code, PriceKind.MACHINE_HOURLY, eq.key, "1대 1일", "대·일"))
|
||||
book.add_detail(
|
||||
PriceDetail(code, _fuel_title(book, kind, fuel_region), liters, note=f"주연료 {kind}")
|
||||
)
|
||||
book.add_detail(_misc_row(code, misc))
|
||||
if eq.fuel and fuel is not None:
|
||||
kind = "휘발유" if "휘발유" in eq.fuel[2] else "경유"
|
||||
liters, misc = fuel
|
||||
book.add_detail(
|
||||
PriceDetail(code, _fuel_title(book, kind, fuel_region), liters, note=f"주연료 {kind}")
|
||||
)
|
||||
book.add_detail(_misc_row(code, misc))
|
||||
reasons = []
|
||||
if eq.price in book.titles:
|
||||
base = f"S-{eq.machine}"
|
||||
@@ -184,7 +214,7 @@ def attach_consumables(
|
||||
uses = build.material_uses.setdefault(material, [])
|
||||
uses.extend(c for c in eq.users if c not in uses)
|
||||
machine_reasons = _machine_title(build, nodes, eq, fuel_region)
|
||||
fuel_rows = _table_rows(nodes, eq.fuel[0], eq.fuel[1])
|
||||
fuel_rows = _table_rows(nodes, eq.fuel[0], eq.fuel[1]) if eq.fuel else []
|
||||
picked = [(c, name) for c, name in eq.choices if c in book.titles]
|
||||
for work_item, user_row in eq.users.items():
|
||||
node_rows = [
|
||||
@@ -193,8 +223,18 @@ def attach_consumables(
|
||||
for r in t.get("raw_row") or []
|
||||
]
|
||||
# 이름 칸이 뭉친 표(「벌목부 보통인부」)도 낱말로 봄 — 그 이름이 없으면 넓히지 않음(③).
|
||||
# 도구 줄 표는 한 줄에 도구와 인력이 함께 — 그 도구 줄이 그 인력을 밝힐 때만(③).
|
||||
if not any(
|
||||
r and (_tight(r[0]) == _tight(user_row) or user_row in str(r[0]).split())
|
||||
r
|
||||
and (
|
||||
_tight(r[0]) == _tight(user_row)
|
||||
or user_row in str(r[0]).split()
|
||||
or (
|
||||
eq.tool
|
||||
and _tight(eq.tool) in {_tight(c) for c in r}
|
||||
and _tight(user_row) in {_tight(c) for c in r}
|
||||
)
|
||||
)
|
||||
for r in node_rows
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -123,6 +123,16 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
|
||||
" 를 따로 세움 — 산림 5-24 · 건설 4-1-3 표 둘 다에 없어 안 넣음 · 물탱크 조종원은 화물차운전사(8-1-2 5호"
|
||||
" 살수차) · [주]⑦ 물주기(인력) 보통인부 0.0005인은 필요시라 안 걺(2026-09-15 브레인).",
|
||||
),
|
||||
"FP-06-02-02": (
|
||||
"부록 예시와 다름",
|
||||
"ⓘ 부록 단가산출서 예시(2025 · 조림목 2,700본/ha)는 묘목찾기 0.10인/100본 · 줄베기 1.40인/ha 로 적어"
|
||||
" 본문 표(묘목찾기 0.07 · 줄베기 1.50~2.50)와 다름 — 본문 표로 셈(기록만 · 2026-09-15 브레인).",
|
||||
),
|
||||
"FP-06-02-03": (
|
||||
"부록 예시와 다름",
|
||||
"ⓘ 부록 단가산출서 예시(2025 · 조림목 2,700본/ha)는 묘목찾기 0.10인/100본 · 모두베기 3.10인/ha 로 적어"
|
||||
" 본문 표(묘목찾기 0.07 · 모두베기 3.50~5.00)와 다름 — 본문 표로 셈(기록만 · 2026-09-15 브레인).",
|
||||
),
|
||||
"FP-12-25": (
|
||||
"운반거리 미정",
|
||||
"⚠ 이 값에는 **운반 몫이 빠져 있습니다** — 품셈 12-25 는 「운반 | 덤프트럭(15ton)」 줄을 "
|
||||
|
||||
@@ -113,7 +113,37 @@ JUDGED_TABLES: dict[str, dict[str, Any]] = {
|
||||
"forms": (("무한궤도", "0201-0080"), ("타이어", "0211-0080")),
|
||||
"why": "원문 L7809 14-2 「0.8㎥ 굴착기 h · 보통인부 h」(10본당)",
|
||||
},
|
||||
# 도구 줄 표 — 「공법 | (갈래) | 사용도구 | 단위 | 소요인력 | 인력구분」 · 이름이 끝 칸(2026-09-15 브레인 ②).
|
||||
# `variants` 면 단위 칸 앞 마지막 칸이 갈래 · 「인/100본」 줄(묘목찾기)은 본수 칸이 들 때 붙음(`_Brushcutting`).
|
||||
"F0155": {
|
||||
"code": "FP-06-02-02",
|
||||
"shape": "tool_rows",
|
||||
"prefix": "줄베기",
|
||||
"variants": True,
|
||||
"why": "원문 6-2-2 「묘목찾기 낫 인/100본 · 줄베기 본수 구간 예취기 인/ha · 인력구분」",
|
||||
},
|
||||
"F0156": {
|
||||
"code": "FP-06-02-03",
|
||||
"shape": "tool_rows",
|
||||
"prefix": "모두베기",
|
||||
"variants": True,
|
||||
"why": "원문 6-2-3 「묘목찾기 낫 인/100본 · 모두베기 조림 경과 예취기 인/ha · 인력구분」",
|
||||
},
|
||||
"F0158": {
|
||||
"code": "FP-06-04-01",
|
||||
"shape": "tool_rows",
|
||||
"prefix": "덩굴걷기",
|
||||
"why": "원문 6-4-1 「기계작업 예취기 인/ha 3.30 특별인부」",
|
||||
},
|
||||
"F0159": {
|
||||
"code": "FP-06-04-02",
|
||||
"shape": "tool_rows",
|
||||
"prefix": "덩굴 약제 살포처리",
|
||||
"why": "원문 6-4-2 「약제살포 배부식분무기 인/ha · 작업보조 인/ha · 인력구분」",
|
||||
},
|
||||
}
|
||||
#: 도구 줄 표의 단위 칸 — 「인/ha」 는 공종 단위 ha 로 곧장 · 「인/100본」 은 본수 칸이 들어야 섬.
|
||||
_RE_TOOL_UNIT = re.compile(r"^인/(ha|100본)$")
|
||||
#: 총칙 L530 「본 품셈에서 제시된 품은 일일 작업시간 8시간을 기준」 — 인력 시간 ÷ 8 = 인.
|
||||
HOURS_PER_DAY = Decimal(8)
|
||||
_RE_SPEC_FIRST_MACHINE = re.compile(r"^\d+(?:\.\d+)?㎥굴착기")
|
||||
@@ -218,7 +248,11 @@ def match_judged_table(
|
||||
return True
|
||||
|
||||
# 비고·Q 가 ㎥당을 적는 모양은 표 머리 밑수를 안 씀(12-12 날개벽은 「개소당」 머리조차 없음).
|
||||
if basis_quantity in (None, 0) and judged["shape"] not in ("remark_labor", "per_m3_rows"):
|
||||
if basis_quantity in (None, 0) and judged["shape"] not in (
|
||||
"remark_labor",
|
||||
"per_m3_rows",
|
||||
"tool_rows", # 줄마다 단위 칸(인/ha)이 밑수
|
||||
):
|
||||
return block("판정 표에 밑수가 없습니다")
|
||||
if judged["shape"] == "header_row":
|
||||
staged = _header_row(code, table, judged, rows, catalog, basis_quantity, unit)
|
||||
@@ -230,6 +264,8 @@ def match_judged_table(
|
||||
staged = _hour_rows(code, table, judged, rows, catalog, basis_quantity)
|
||||
elif judged["shape"] == "per_m3_rows":
|
||||
staged = _per_m3_rows(code, table, judged, rows, catalog)
|
||||
elif judged["shape"] == "tool_rows":
|
||||
staged = _tool_rows(code, table, judged, rows, catalog)
|
||||
else:
|
||||
staged = _merged_first(code, table, judged, rows, catalog, basis_quantity, unit)
|
||||
if isinstance(staged, str):
|
||||
@@ -435,3 +471,39 @@ def _hour_rows(code, table, judged, rows, catalog, basis_quantity) -> list | str
|
||||
for index, entry, amount in labor:
|
||||
staged.append(_row(code, table, entry, amount / basis_quantity, unit, index, form))
|
||||
return staged
|
||||
|
||||
|
||||
#: 「인/100본」 줄 사유 — 본수 칸이 들면 `_Brushcutting` 이 걷고 붙임.
|
||||
PER_HUNDRED_REASON = "조림목 본수(본/ha) 칸이 들어야 붙음 — 단위가 인/100본"
|
||||
|
||||
|
||||
def _tool_rows(code, table, judged, rows, catalog) -> list | str:
|
||||
"""단위 칸(인/ha · 인/100본)을 찾아 뒤 칸 = 값 · 그 뒤 = 인력 · `variants` 면 단위 앞 앞 칸이 갈래.
|
||||
|
||||
⚠ 「인/100본」 줄은 여기서 안 셈 — 본수가 설계 입력이라 자원 축(프로젝트 밖)에선 모름 → 못 붙은 줄로.
|
||||
"""
|
||||
staged: list = []
|
||||
per_ha = 0
|
||||
for index, cells in enumerate(rows):
|
||||
unit = next(
|
||||
(i for i, c in enumerate(cells) if _RE_TOOL_UNIT.match("".join(c.split()))), None
|
||||
)
|
||||
if unit is None or unit < 1 or len(cells) < unit + 3:
|
||||
continue
|
||||
amount = parse_amount(cells[unit + 1])
|
||||
entry = _entry(catalog, cells[unit + 2], code)
|
||||
if amount is None or entry is None:
|
||||
return f"{cells[0]} 줄"
|
||||
if "".join(cells[unit].split()) == "인/100본":
|
||||
staged.append(
|
||||
UnmatchedRow(code, str(table.get("pum_table_id", "")), cells[0], PER_HUNDRED_REASON)
|
||||
)
|
||||
continue
|
||||
variant = ""
|
||||
if judged.get("variants"):
|
||||
if unit < 2:
|
||||
return f"{index}째 줄 갈래 칸"
|
||||
variant = cells[unit - 2]
|
||||
staged.append(_row(code, table, entry, amount, "ha", index, variant))
|
||||
per_ha += 1
|
||||
return staged if per_ha else "인/ha 줄"
|
||||
|
||||
@@ -292,6 +292,8 @@ async def _build_for(project_id: UUID, dump_haul_m: tuple[str, ...] = ()):
|
||||
tuple(sorted(set(dump_haul_m), key=Decimal)),
|
||||
# 자재 수동 단가(PLAN 1장 Ⓐ) — 코드 키만 조립에 얹음. 없으면 종전 벌 그대로.
|
||||
material_prices_key(settings.get(MATERIAL_PRICES_KEY)),
|
||||
# 조림목 본수(본/ha) — 풀베기 묘목찾기 합산(6-2-2·6-2-3 [주]④⑤). 비면 그 둘은 일부만.
|
||||
str(settings.get("seedlings_per_ha") or ""),
|
||||
)
|
||||
# 사용자가 고친 값(PLAN 12장 2차) — 없으면 기본 조립 그 벌 그대로.
|
||||
return edited_build(args, edits_key(settings.get("edits")))
|
||||
|
||||
@@ -202,6 +202,16 @@ async def get_factor_choices(project_id: UUID) -> JSONResponse:
|
||||
"basis": [TRANSPORT_BASIS, TRANSPORT_ASSUMPTION],
|
||||
"notes": transport_notes,
|
||||
},
|
||||
"seedlings": {
|
||||
"per_ha": str(settings.get("seedlings_per_ha") or ""),
|
||||
"basis": [
|
||||
"산림품셈 6-2-2 줄베기 · 6-2-3 모두베기 [주]④⑤ — 「묘목찾기와 줄베기(모두베기) 품을"
|
||||
" 합하여 적용」 · 묘목찾기 0.07인/100본 × 조림목 본수 ÷ 100 · 본수는 [주]② 둘레베기 참고"
|
||||
"(표준지로 조사한 조림목 생육본수)",
|
||||
"⚠ 비워 두면 줄베기·모두베기 단가가 「일부만」 으로 막힙니다 — 묘목찾기가 빠진 값은"
|
||||
" 원문보다 모자라서입니다. 제안값은 두지 않습니다.",
|
||||
],
|
||||
},
|
||||
"labor_surcharge": {
|
||||
"chosen": labor_surcharge_chosen,
|
||||
"total_percent": f"{labor_surcharge_total:g}",
|
||||
@@ -253,6 +263,8 @@ class FactorChoiceBody(BaseModel):
|
||||
transport_trips: str | None = None
|
||||
#: 품 할인·할증(1-4) — 계열코드 → 고른 행. **빈 값이면 그 계열을 끄는 것.**
|
||||
labor_surcharge: dict[str, str] | None = None
|
||||
#: 조림목 본수(본/ha) — 풀베기 묘목찾기 합산. **빈 문자열이면 줄베기·모두베기가 일부만으로 막힘.**
|
||||
seedlings_per_ha: str | None = None
|
||||
|
||||
|
||||
@router.put("/{project_id}/estimation/factors")
|
||||
@@ -335,6 +347,14 @@ async def put_factor_choices(project_id: UUID, body: FactorChoiceBody) -> JSONRe
|
||||
except ValueError as exc:
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
||||
values["transport_trips"] = "" if trips is None else str(trips)
|
||||
if body.seedlings_per_ha is not None:
|
||||
from B09_Estimation.B09_Estimation_Brushcutting import parse_seedlings_per_ha
|
||||
|
||||
try:
|
||||
seedlings = parse_seedlings_per_ha(body.seedlings_per_ha)
|
||||
except ValueError as exc:
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
||||
values["seedlings_per_ha"] = "" if seedlings is None else str(seedlings)
|
||||
if body.labor_surcharge is not None:
|
||||
from B09_Estimation.B09_Estimation_LaborSurcharge import parse_choices
|
||||
|
||||
|
||||
@@ -80,11 +80,18 @@ interface LaborSurchargeRow {
|
||||
basis: string[];
|
||||
}
|
||||
|
||||
/** 조림목 본수(본/ha) — 풀베기 묘목찾기 합산(산림품셈 6-2-2·6-2-3 [주]④⑤). 비면 일부만. */
|
||||
interface SeedlingsRow {
|
||||
per_ha: string;
|
||||
basis: string[];
|
||||
}
|
||||
|
||||
export interface FactorChoicesDto {
|
||||
ranges: RangeFactorRow[];
|
||||
machines: MachineChoiceRow[];
|
||||
misc_material?: MiscMaterialRow;
|
||||
transport?: TransportRow;
|
||||
seedlings?: SeedlingsRow;
|
||||
labor_surcharge?: LaborSurchargeRow;
|
||||
notes: string[];
|
||||
}
|
||||
@@ -109,6 +116,7 @@ export async function saveFactorChoices(
|
||||
transport_road?: string;
|
||||
transport_trips?: string;
|
||||
labor_surcharge?: Record<string, string>;
|
||||
seedlings_per_ha?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const response = await fetch(
|
||||
@@ -138,6 +146,7 @@ function percentBox(
|
||||
value: string,
|
||||
placeholder: string,
|
||||
onApply: (text: string) => void,
|
||||
unit = "%",
|
||||
): HTMLElement {
|
||||
const wrap = el("div", "b09s-hint b09s-inline");
|
||||
const input = el("input");
|
||||
@@ -150,7 +159,7 @@ function percentBox(
|
||||
const apply = el("button", "", "적용");
|
||||
apply.type = "button";
|
||||
apply.addEventListener("click", () => onApply(input.value.trim()));
|
||||
wrap.append(el("span", "b09s-head", label), input, el("span", "", "%"), apply);
|
||||
wrap.append(el("span", "b09s-head", label), input, el("span", "", unit), apply);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@@ -275,6 +284,20 @@ export function drawFactorChoices(
|
||||
body.append(note(transport.trips_note));
|
||||
}
|
||||
|
||||
const seedlings = data.seedlings;
|
||||
if (seedlings) {
|
||||
body.append(
|
||||
percentBox(
|
||||
"조림목 본수 (풀베기 묘목찾기)",
|
||||
seedlings.per_ha,
|
||||
"비움",
|
||||
(text) => save({ seedlings_per_ha: text }),
|
||||
"본/ha",
|
||||
),
|
||||
);
|
||||
for (const line of seedlings.basis) body.append(note(line));
|
||||
}
|
||||
|
||||
const surcharge = data.labor_surcharge;
|
||||
if (surcharge) {
|
||||
body.append(head("품의 할인·할증 (산림품셈 1-4) — 고른 것만 붙습니다"));
|
||||
|
||||
@@ -70,6 +70,7 @@ from B09_Estimation.B09_Estimation_WorkItemUnit import BORROWED_BASIS_PER
|
||||
from B09_Estimation.B09_Estimation_WorkItemUnit import unit_of as work_item_unit
|
||||
from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at
|
||||
from B09_Estimation.B09_Estimation_Transport import parse_distance_km
|
||||
from B09_Estimation.B09_Estimation_Brushcutting import parse_seedlings_per_ha
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
#: 조종원 **시간당 노임** 제목 꼬리 — 일 노임 제목(`L…`)과 갈라 둠. 노무비목록표엔 안 실림(일당만).
|
||||
@@ -645,6 +646,7 @@ def build_unit_prices(
|
||||
operator_wage_digits: int = 0,
|
||||
dump_haul_m: tuple[Decimal, ...] = (),
|
||||
material_prices: tuple[tuple[str, str, str], ...] = (),
|
||||
seedlings_per_ha: Decimal | None = None,
|
||||
) -> UnitPriceBuild:
|
||||
"""자원 축을 일위대가(`B`)로 조립한다.
|
||||
|
||||
@@ -918,7 +920,11 @@ def build_unit_prices(
|
||||
for row in rows:
|
||||
section = missing_basis.get(str(row.pum_table_id))
|
||||
# 비고가 「인/㎥」 로 밑수를 적은 판정표(12-12 날개벽 「개소당」 머리 없음)는 밑수가 ㎥ 로 섬.
|
||||
per_remark = JUDGED_TABLES.get(str(row.pum_table_id), {}).get("shape") == "remark_labor"
|
||||
# 도구 줄 표(6-2·6-4)는 줄마다 단위 칸 「인/ha」 가 밑수(2026-09-15 브레인 ②).
|
||||
per_remark = JUDGED_TABLES.get(str(row.pum_table_id), {}).get("shape") in (
|
||||
"remark_labor",
|
||||
"tool_rows",
|
||||
)
|
||||
if section and not borrowed and not per_remark:
|
||||
build.basis_missing[work_item_code] = section
|
||||
break
|
||||
@@ -1125,6 +1131,10 @@ def build_unit_prices(
|
||||
from B09_Estimation.B09_Estimation_Consumables import attach_consumables
|
||||
|
||||
attach_consumables(build, nodes_by_code, fuel_region)
|
||||
# 풀베기 묘목찾기(6-2-2·6-2-3 [주]④⑤ 합하여 적용) — 조림목 본수 칸 · 비면 일부만(2026-09-15 브레인 ②).
|
||||
from B09_Estimation.B09_Estimation_Brushcutting import attach_seedling_finding
|
||||
|
||||
attach_seedling_finding(build, nodes_by_code, seedlings_per_ha)
|
||||
# ⚠ **마지막에 한 번** — 조합 사용 공종의 본체 기계를 잡재료 16% 층으로 바꿔 단다
|
||||
# (품셈 제8장 [주]⑤). 조립 도중에 바꾸면 어느 공종이 조합인지 아직 모른다.
|
||||
build.combined_swapped = _apply_combined_misc_rate(
|
||||
@@ -1277,6 +1287,7 @@ def cached_build(
|
||||
operator_wage_digits: str = "",
|
||||
dump_haul_m: tuple[str, ...] = (),
|
||||
material_prices: tuple[tuple[str, str, str], ...] = (),
|
||||
seedlings_per_ha: str = "",
|
||||
) -> UnitPriceBuild:
|
||||
"""조립 결과를 한 번만 만든다 — 품셈 3 MB 를 요청마다 다시 읽지 않는다.
|
||||
|
||||
@@ -1306,6 +1317,7 @@ def cached_build(
|
||||
operator_wage_digits=parse_operator_wage_digits(operator_wage_digits),
|
||||
dump_haul_m=tuple(Decimal(value) for value in dump_haul_m),
|
||||
material_prices=material_prices,
|
||||
seedlings_per_ha=parse_seedlings_per_ha(seedlings_per_ha),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# PLAN — 랩탑 메인 (1장 `[랩탑_메인]` 몫)
|
||||
|
||||
> ⚠ 임시 · 2026-09-15 시놀로지 덮어쓰기 때문 · 사용자 확인 전(브레인 지시). `PLAN.md` 는 읽기만 · 나중에 브레인이 창별 파일을 합쳐 `PLAN.md` 한 벌로 되돌림.
|
||||
> git 에 명시 커밋으로 올림(PC 사이를 건너가고 이력이 남게 · 사용자 지침 「PLAN.md 는 git 밖 · 한 파일로만」 과 어긋나는 임시 조치).
|
||||
|
||||
## 1. 축 세우기 — 랩탑 메인 차례
|
||||
|
||||
### 남은 것
|
||||
- [ ] ⚠ 자원 이름이 겹치면 남의 공종 풀이가 바뀜(명세 2장 이름 맞춤의 부작용) — 자원 목록에 「양수기」 를 넣자 12-26 물푸기 「양수기(150m/m)」 줄 풀이가 바뀌어 제목이 사라짐 → 지금은 「유인헬기방제 양수기」 로 피함(임시) · 언젠가 공종 범위·코드로 맞출 자리
|
||||
- [ ] ㉯ 나머지 — 제목 없음(지금 셈 34 · 같은 자) 가르기: ⓐ 참고 표 19(1-2 단위표준·운반 · 2장 소요재료·기계손료 17 — 다른 공종이 읽는 표라 제목이 안 서는 게 맞음) ⓑ choose_one 부모 2(4-1·4-2 — 자식이 섬 · 단어는 부록 설계예시표 F0475·F0476 에서 걸림) ⓒ 단어는 설계예시표에서만 · 자기 표는 인력뿐 3(3-1 경계표시 **사유 없음** · 3-2 작업로 선정 초급기술자 못 풂 · 3-3 작업로 설치 비율 인력 못 풂) ⓓ **예취기·분무기 인력 표 한 모양 4**(6-2-2 줄베기 · 6-2-3 모두베기 · 6-4-1 덩굴걷기 **사유 없음** · 6-4-2 덩굴 약제 — 「이름 | 도구 | 인/ha | 값 | 직종」 줄을 못 읽음 · 풀리면 2장 예취기·배부식분무기 1일 몫을 체인톱처럼) ⓔ 이름에만 드론·윈치 3(3-8 드론 영상 촬영 인력만 **사유 없음** · 7-3 아키야윈치 **사유 없음** · 8-6-2 드론방제 인력 줄 못 풂) ⓕ 거리 표 2(10-13-1 드론운반 · 10-13-2 인력운반 — 형식 미정으로 건너뜀 · **사유 없음**) ⓖ 13-10-2 나무 말뚝박기 1(뭉친 칸) · 사유 없이 버려지던 것은 전체 49 로 넓혀 사유 붙음(490cab5e) · 남은 차례: 예취기 한 모양 4 → 나머지(3-2 · 3-3 · 8-6-2 · 13-10-2)
|
||||
- [ ] 막힌 셋 풀기(원문에 값이 없음 · 입력·견적 칸이 서야 함): 12-26 물푸기 가동시간 · 10-7-4 모노레일 ps·임대료·운반거리 · 10-8-3 케이블크레인 ℓ/kWh · 8-6-3 지상방제 1일 작업량·1톤 트럭
|
||||
- [ ] 손료계수 없는 기계 222종 되살림(미룸 · 브레인 승인 — 지금 막는 공종 0) — 가르기 끝: 원문 없음 0 · 원천 한 줄 누락 1(3201-0003 「-0003」 표기) · 뭉친 칸이지만 열마다 개수 맞고 상각+정비+관리=계 검증 통과 196(크레인(무한궤도) 15 · 골재생산(컨베이어·크러셔·스크린·빈) 55 · 해상(준설선·예선·대선 등) 66 · 그 밖 60) · 글줄로 붙음 25(3601 콘크리트 피니셔 5 · 9020 준설선 7 · 9060 토운선 6 · 7) · 원문 md 한 줄 읽기가 기존 계수 383/383 일치 · 8-4 운전경비 버려진 줄 3 = 81기종(포장 27 · 기타 36 · 기타 18) 중 공종이 쓰는 것은 물탱크 하나(되살림 끝)
|
||||
- [ ] 10-7-4 모노레일 제목의 보통인부 두 번 셈(4인) — 막힌 채 사유만 적음 · 풀리는 날 운전 단가표 2인을 빼고 셀 것
|
||||
- [ ] 소형 장비 1일 소모 표(㉯) 가운데 2-1-2 경유 표(집재장비) · 천공기 · 예취기 — 쓰는 공종이 풀릴 때 같은 한 벌로
|
||||
|
||||
### 검증완료(2026-09-14 밤 ~ 09-15 · 커밋 해시)
|
||||
- [X] 사유 없이 버려지던 잎 공종 49 에 사유(브레인 차례 ① · ㉯ 자로 본 6 을 전체로 넓혀 셈) — `B09_Estimation_UnreadTables`: 일할 표(작업량·소요량·형식 미정)가 있는데 제목도 사유(못 붙은 줄·성분 빠짐·밑수·[주])도 없는 잎 공종에 「표를 읽는 길이 없어 한 줄도 안 섬 — 표 번호 모양」 을 성분 미확보로 · 읽는 길은 안 만듦(그 표가 풀리면 저절로 안 붙음) · 전후: 사유 +49 · 제목·금액 변화 0 · 프로젝트 내역에 뜨는 것 0(인계 24 공종과 안 겹침 — 화면 묶어 보이기는 아직 필요 없음) · 49 = 2-1-12 · 3장 3 · 4장 4 · 5장 6(5-26 흙갈이 셋은 형식 미정) · 6장 3 · 7장 14 · 8장 9 · 9장 4(콘크리트 깨기) · 10장 4 · 12-29 1 = 이 목록이 곧 「읽는 길」 차례표. 검증완료(490cab5e)
|
||||
- [X] 조종원 직종 8-1-2 5호 원문대로(브레인 「지금 끼울 것」) — 원문이 이름·규격으로 가르는 기종만(덤프트럭 12t · 콘크리트 믹서 0.55㎥ · 공기압축기(이동식) 2.83㎥/min · 괄호 앞 이름 끝말로 · 살수차는 괄호 속) · 바뀐 레코드 정확히 12(덤프 15·20·24·32t 화물차→건설기계 · 콘크리트 믹서 0.10~0.45 · 래머 · 플레이트 콤팩터 → 일반기계) · 원문으로 가름 161 · 잠정 56(콘크리트 펌프차 · 파일천공전용장비 · 타워크레인 · 트랙터(타이어) · 크러셔 · 크롤러드릴 …) 은 옛 규칙 + 중기경비 장 사유 · 곁: 파쇄기 중기경비 장이 연료·조종원 없다고 거짓 공백을 띄우던 것 걷음 · 전후(본당): 9-14-2 14,523 → 9,177 · 10-12 L164.23m 3,249 → 3,519 · 9,172 → 9,912 · 10,520 → 11,369 · 프로젝트 본체 109,671,600 → 109,682,519(+10,919 · 덤프 운반 토사) · 거창 실무 「덤프 15Ton 건설기계 · 8·4.5Ton 화물차 · 플레이트 콤팩터 일반기계운전사」 = 원문. 검증완료(22692669)
|
||||
- [X] 초류종자살포 5-24-1·2 둘이 섬(브레인 판정 ①②′④) — ① 트럭 4.5ton → 덤프트럭 4.5(범위 별칭 · 옆 칸 규격이 대상 규격과 같으면 덮는 것 아님) ②′ 종자살포기 → 취부기 11.94㎾ 손료만(`LOSS_ONLY_MACHINES` · 8-4 에 줄 없음 · 거창도 손료 20,295 만 · 디젤엔진은 원문 둘 다에 없어 안 넣음) ④ 종자 후보 넷 칸 · 넣은 하나만 · 둘이면 사유(`B09_Estimation_SeedSpray`) · 어긋남 사유 나란히 · 233 → 527(③) → 744원/㎡(재료 단가 전) · 다른 공종·프로젝트 내역 변화 0. 검증완료(3d896dec)
|
||||
- [X] ③ 물탱크(살수차) 운전경비 되살림 — 8-4-8 셀 병합으로 줄째 버려지던 7204 다섯 원문 값(8.2·8.6·9.3·9.4·12.9ℓ · 잡재료 30 · 1인) · 조종원 8-1-2 5호 살수차 = 화물차운전사 · 거창 5,500ℓ 일치. 검증완료(078af37d)
|
||||
- [X] ㉡-3 ㉠ 으로 막은 다섯 표 읽기(브레인 판정 ①②③) — 판정표 「시간 줄」(`hour_rows` · F0450·F0451): 장비 h 그대로 · 인력 h ÷ 8(총칙 L530) · ÷10본 · 굴착기 0.8㎥ 무한궤도·타이어 두 갈래(제안 없음) · 14-1 은 부착용 집게 7206-0070 과 조합(본체 `#조합`) · 사유(이름 다름 · 시간 단위 · 옛 값 8배) · 막힌 셋은 옛 값 까닭 사유(12-26 보통인부 1인뿐 · 10-7-4 같은 사람 두 번 셈 · 10-8-3 특별인부 1인뿐). 옛↔새(본당): 14-2 11,356 → 무한궤도 4,431 · 타이어 4,631 · 14-1 8,603 → 3,192 · 3,316 · 12-26 172,068 · 10-7-4 688,272 · 10-8-3 226,122 은 막힌 채 · 마스터 14-1·14-2 갈래 키만 · 전체 1721 + 392. 검증완료(42e8b4f0)
|
||||
- [X] 갈래마다 밑수가 다른 표 — `_match_two_row_table` 이 밑수로 안 나눠 8-6-1 유인헬기 160배·400배 부풂 → 갈래 「(Nha당)」 밑수 · 그 길 표 다섯 중 밑수 1 아닌 것 8-6-1 하나 · 전체에서 갈래마다 밑수 다른 표도 8-6-1 하나 · 소형 1,591,855 → 9,949원/ha · 대형 2,780,567 → 6,951. 검증완료(e15e9fc2)
|
||||
- [X] ㉡-2 유인헬기방제 8-6-1 — `AreaSet`: 양수기 1대1일 호표(휘발유 10ℓ + 잡품 95% + 손료 칸) ÷ 1일 면적 · 깃발 20ha당 1개(칸) · 헬기 임차료(견적 칸) · 소방관서 급수 단서 · 자원 이름 「유인헬기방제 양수기·깃발」 · 소형 +227원/ha · 대형 +90 · 8-6-3 은 막힌 채 사유 둘(1일 작업량 없음 · 15ha 는 연료 설명 / 1톤 트럭 카탈로그 없음). 검증완료(9563d060)
|
||||
- [X] ㉡-1 체인톱 — `B09_Estimation_Consumables`(2장 표 읽는 한 벌): 체인톱 1대1일 호표 = 휘발유 5.6ℓ + 잡품 40% + 손료 0.0084 × 구입가(칸) · 4-2-2 「벌목부」·6-5 「특별인부 (체인톱 사용)」 인원만큼 · 체인오일 넣은 쪽 · 4-2-2 +31·+41·+53원/㎡ · 6-5 +43,821원/ha · 프로젝트 지장목제거 +548,652(본체 109,122,948 → 109,671,600). 검증완료(852ad6c7)
|
||||
- [X] ㉠ 말없이 버려지던 기계·연료 줄 — 자원 축 줄 읽기 세 자리(규격 앞 장비 이름 · 값·이름 둘 다 못 읽음 · 수량 빈 연료 줄) → 「못 맞춤 + 일부만」 · 새로 사유 붙은 줄 8 · 새로 막힌 공종 5 · 처음 승인받은 `_names_a_machine` 넓히기는 재어 보니 0 을 막아 정정. 검증완료(2f9d42ac)
|
||||
- [X] ㉯ 셈 — 소형 장비·연료 이름 나오는 공종 41 · 제목 선 9 모두 기계·연료 줄 0 · 금액까지 서던 7 · 4-2-2 는 부모 표에만 체인톱(제목 선 자식은 그 하나)
|
||||
- [X] ㉰ 이동식 임목 파쇄 8-11 — 8-4 운전경비 「-」 라 없던 파쇄기 기계 층을 8-11 [주]⑤ 13.5ℓ·잡재료 16%·운전자 1인으로 · ㎥당 = 1/3.5 + 보통인부 2인÷8h÷Q · 파쇄기날 칸 · 51,029원/㎥. 검증완료(0fc64fa7)
|
||||
- [X] ㉱ 조립 줄이 조각 「일부만」·「밑수 없음」 올림 · `_material_row` 순수 이동(710→658). 검증완료(16d34615)
|
||||
- [X] ㉰ 표토제거 답외구간 9-15-2 — 불도저 식 Q1 47.92 ÷ T 0.2 = 239.60 ㎡/hr · 618원/㎡ · 영월 호표 E 자리에 e(0.96) 기록(원문 또렷하면 원문) · 프로젝트 +6,151,768(102,971,180 → 109,122,948). 검증완료(ac66a25c)
|
||||
- [X] 판정표 안 읽은 줄 자동 목록(`_unread_rows`) — 9표 늘어난 줄 0·+1·+4·+8·+6·+7·+1. 검증완료(9deaf479)
|
||||
- [X] 봉상후렉시블 셋 + 12-34-1 — 판정표 F0350·F0351·F0354·F0385 · 별칭 넷 → 엔진식 4611-0350 · 실무 영월 「콘크리트타설(5-5)」 같은 모양 · 새 제목 다섯. 검증완료(15d91be7)
|
||||
- [X] 672 품 할증 겹침 — 합산 근거 건설 공통 1-4-2(교차 참조) · 동일성격 단서 화면. 검증완료(8dddc768)
|
||||
- [X] 진동기 + 연료 종류 — (4611) 원문값 되살림 · 12-15 → 엔진식 · `fuel_kind` 대로 휘발유 · 중기경비 장 `#암석` 뒤처리 · 휘발유 기계 10종 전수 대조. 검증완료(7ea25982)
|
||||
- [X] 661 암석 손료보정(8-1-7 1) — `B09_Estimation_RockLoss` · 봉화 암석 계수 셋 재현 · 바뀐 제목 17 · E1~E3 원문에 없음 · 착암기 8-3 에도 손료 줄 없음. 검증완료(a8427666)
|
||||
- [X] 301 유로폼 12-38-2 — `B09_Estimation_Euroform` · 손료 표 수량 곧장 · 임대료 한 줄 · 가드 ③ 에서 12-38-02 뺌 · 봉화 재료비 4,500·3,697 일치. 검증완료(efaf2403)
|
||||
- [X] 300 발파 9-5-1 — 화약류 자재 단가 칸 · 착암기 부수물 손료 고시 없음 사유. 검증완료(09247fd1)
|
||||
- [X] ⑭ `form_basis` 화면 — 판정표 까닭 회색 한 줄. 검증완료(c7f34b8c)
|
||||
|
||||
### 규칙(오늘 세운 것)
|
||||
- 원문이 또렷하면 원문 · 애매하면 실무 역산 · 어긋남은 늘 기록
|
||||
- 버릴 때는 사유를 남길 것(읽는 자리에서 조용히 버리지 않음)
|
||||
- 빨간 시험은 구현까지 한 호흡에 · autopush 전 git status
|
||||
@@ -409,6 +409,59 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "AR-X-da716e54",
|
||||
"kind": "machine",
|
||||
"name": "예취기",
|
||||
"spec": "배기량 35cc",
|
||||
"unit": "대·일",
|
||||
"source": {
|
||||
"pum_edition": "2026-01-01",
|
||||
"pum_table_ids": [
|
||||
"F0043",
|
||||
"F0065"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "AR-M-148c5888",
|
||||
"kind": "material",
|
||||
"name": "예취기 구입가",
|
||||
"spec": "배기량 35cc · 손료 밑수",
|
||||
"unit": "대",
|
||||
"source": {
|
||||
"pum_edition": "2026-01-01",
|
||||
"pum_table_ids": [
|
||||
"F0065"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "AR-X-5d0a0416",
|
||||
"kind": "machine",
|
||||
"name": "배부식분무기",
|
||||
"spec": "덩굴 약제처리",
|
||||
"unit": "대·일",
|
||||
"source": {
|
||||
"pum_edition": "2026-01-01",
|
||||
"pum_table_ids": [
|
||||
"F0067"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "AR-M-d6394831",
|
||||
"kind": "material",
|
||||
"name": "배부식분무기 구입가",
|
||||
"spec": "덩굴 약제처리 · 손료 밑수",
|
||||
"unit": "대",
|
||||
"source": {
|
||||
"pum_edition": "2026-01-01",
|
||||
"pum_table_ids": [
|
||||
"F0067"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "AR-M-5649cf3f",
|
||||
"kind": "material",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "data_work_item_master_manifest",
|
||||
"generated_at": "2026-09-15T03:10:55+09:00",
|
||||
"generated_at": "2026-09-15T05:49:19+09:00",
|
||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||
"source": {
|
||||
"dataset_id": "pum_forest",
|
||||
@@ -12,8 +12,8 @@
|
||||
"files": [
|
||||
{
|
||||
"file": "work_item_master_2026-01-01.json",
|
||||
"sha256": "480f9510cdd2fe223b54818fb7180f8c6114d6e33e45cd3c6aa829c64de27eb3",
|
||||
"size_bytes": 839153
|
||||
"sha256": "2a0049e73fe48e44c79dda354ae13cf850a61227acdc22645cb94d52e89bbb2b",
|
||||
"size_bytes": 839589
|
||||
},
|
||||
{
|
||||
"file": "form_undetermined_2026-01-01.json",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"dataset_id": "work_item_master_forest",
|
||||
"effective_date": "2026-01-01",
|
||||
"pum_edition": "2026-01-01",
|
||||
"generated_at": "2026-09-15T03:10:55+09:00",
|
||||
"generated_at": "2026-09-15T05:49:19+09:00",
|
||||
"dataset_version": {
|
||||
"dataset_id": "pum_forest",
|
||||
"effective_date": "2026-01-01",
|
||||
@@ -16907,7 +16907,11 @@
|
||||
"formula_rows": [],
|
||||
"special_glyphs": [],
|
||||
"capacity_formula_here": false,
|
||||
"variant_key": [],
|
||||
"variant_key": [
|
||||
"1,500본 미만",
|
||||
"1,500본 이상 3,000본 미만",
|
||||
"3,000본 이상"
|
||||
],
|
||||
"condition_note": [
|
||||
"공 법",
|
||||
"사용도구",
|
||||
@@ -16951,7 +16955,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"variant_keys": []
|
||||
"variant_keys": [
|
||||
"1,500본 미만",
|
||||
"1,500본 이상 3,000본 미만",
|
||||
"3,000본 이상"
|
||||
]
|
||||
},
|
||||
{
|
||||
"work_item_code": "FP-06-02-03",
|
||||
@@ -16978,7 +16986,11 @@
|
||||
"formula_rows": [],
|
||||
"special_glyphs": [],
|
||||
"capacity_formula_here": false,
|
||||
"variant_key": [],
|
||||
"variant_key": [
|
||||
"조림 당해 연도 (전년도 추기조림 포함)",
|
||||
"조림 2년차",
|
||||
"조림 3년차 이상"
|
||||
],
|
||||
"condition_note": [
|
||||
"공 법",
|
||||
"사용도구",
|
||||
@@ -17022,7 +17034,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"variant_keys": []
|
||||
"variant_keys": [
|
||||
"조림 당해 연도 (전년도 추기조림 포함)",
|
||||
"조림 2년차",
|
||||
"조림 3년차 이상"
|
||||
]
|
||||
},
|
||||
{
|
||||
"work_item_code": "FP-06-03",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""예취기·분무기 인력 표 한 모양 넷 — 2026-09-15 브레인 차례 ② (사유 없던 49 다음).
|
||||
|
||||
산림 6-2-2 줄베기 · 6-2-3 모두베기 · 6-4-1 덩굴걷기 · 6-4-2 덩굴 약제 표는 「공법 | (갈래) | 사용도구 | 단위 | 소요인력 |
|
||||
인력구분」 줄 — 이름이 끝 칸이라 줄 읽기가 한 줄도 못 읽었음(6-4-1 은 사유조차 없었음).
|
||||
6-2-2 묘목찾기 낫 인/100본 0.07 보통인부 · 줄베기 1,500본 미만 1.50 · 1,500~3,000 2.00 · 3,000 이상 2.50 특별인부(인/ha)
|
||||
6-2-3 묘목찾기 0.07 · 모두베기 조림 당해 연도 3.50 · 2년차 4.00 · 3년차 이상 5.00
|
||||
6-4-1 기계작업 예취기 인/ha 3.30 특별인부 · 6-4-2 약제살포 배부식분무기 1.00 특별인부 + 작업보조 1.50 보통인부
|
||||
⭐ 6-2-2·6-2-3 [주]④⑤ 「묘목찾기와 줄베기(모두베기) 품을 **합하여** 적용」 — 조림목 본수(본/ha)는 설계 입력 칸 ·
|
||||
비면 「일부만」 으로 막음(반만 선 틀린 값보다 안 선 값) · 제안값 없음(부록 예시 2,700본/ha 는 품도 표와 다른 자료)
|
||||
2장: 예취기 휘발유 5.0ℓ/대/일 · 잡품 10%(2-1-1 2 [주]① 줄베기·모두베기·지상부 덩굴걷기에만) · 손료 0.0084(2-2-2) ·
|
||||
배부식분무기 손료 0.0084(2-2-4) — 체인톱 한 벌 길 · 1대당 1인
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||||
|
||||
CUTTER = "X-AR-X-da716e54"
|
||||
SPRAYER = "X-AR-X-5d0a0416"
|
||||
|
||||
|
||||
def _rows(book, title: str) -> dict[str, Decimal]:
|
||||
return {r.ref_code: r.quantity for r in book.details[title]}
|
||||
|
||||
|
||||
def test_덩굴걷기는_특별인부와_예취기_1대_1일() -> None:
|
||||
build = cached_build()
|
||||
rows = _rows(build.book, "B-FP-06-04-01")
|
||||
assert rows["1003"] == Decimal("3.30") and rows[CUTTER] == Decimal("3.30"), rows
|
||||
assert "FP-06-04-01" not in build.partial_ratio and "FP-06-04-01" not in build.basis_missing
|
||||
assert build.book.titles["B-FP-06-04-01"].unit == "ha"
|
||||
cutter = _rows(build.book, CUTTER)
|
||||
assert cutter["M-FUEL-휘발유"] == Decimal("5.0"), cutter
|
||||
misc = next(r for r in build.book.details[CUTTER] if r.percent_of_material is not None)
|
||||
assert misc.percent_of_material == Decimal(10)
|
||||
assert "예취기가격" in " ".join(build.unattached["FP-06-04-01"])
|
||||
|
||||
|
||||
def test_덩굴_약제는_두_인력을_더하고_분무기_손료_칸() -> None:
|
||||
build = cached_build()
|
||||
rows = _rows(build.book, "B-FP-06-04-02")
|
||||
assert rows["1003"] == Decimal("1.00") and rows["1002"] == Decimal("1.50"), rows
|
||||
assert SPRAYER not in rows # 연료 없는 장비 — 구입가가 없으면 빈 호표를 안 세움
|
||||
assert "배부식분무기가격" in " ".join(build.unattached["FP-06-04-02"])
|
||||
assert "FP-06-04-02" not in build.basis_missing # 단위 칸 인/ha 가 밑수
|
||||
priced = cached_build(material_prices=(("AR-M-d6394831", "150000", "견적"),))
|
||||
assert _rows(priced.book, "B-FP-06-04-02")[SPRAYER] == Decimal("1.00")
|
||||
assert priced.book.resolve(SPRAYER).expense == Decimal("1260.0000") # 150,000 × 0.0084
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "variants"),
|
||||
[
|
||||
(
|
||||
"FP-06-02-02",
|
||||
{"1,500본미만": "1.50", "1,500본이상3,000본미만": "2.00", "3,000본이상": "2.50"},
|
||||
),
|
||||
(
|
||||
"FP-06-02-03",
|
||||
{
|
||||
"조림당해연도(전년도추기조림포함)": "3.50",
|
||||
"조림2년차": "4.00",
|
||||
"조림3년차이상": "5.00",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_풀베기는_갈래마다_특별인부_예취기_묘목찾기는_본수_칸(code, variants) -> None:
|
||||
empty = cached_build()
|
||||
for key, people in variants.items():
|
||||
rows = _rows(empty.book, f"B-{code}#{key}")
|
||||
assert rows["1003"] == Decimal(people) and rows[CUTTER] == Decimal(people), rows
|
||||
assert "1002" not in rows # 본수가 없으면 묘목찾기를 안 셈
|
||||
assert code in empty.partial_ratio # 반만 선 값으로 안 붙게 막음
|
||||
assert "조림목 본수" in empty.component_gaps[code], empty.component_gaps.get(code)
|
||||
filled = cached_build(seedlings_per_ha="2700")
|
||||
for key in variants:
|
||||
rows = _rows(filled.book, f"B-{code}#{key}")
|
||||
assert rows["1002"] == Decimal("0.07") * Decimal(27), rows # 0.07인/100본 × 2,700본/ha
|
||||
assert code not in filled.partial_ratio
|
||||
assert "조림목 본수" not in (filled.component_gaps.get(code) or "")
|
||||
|
||||
|
||||
def test_본수_칸은_양수만_받음() -> None:
|
||||
from B09_Estimation.B09_Estimation_Brushcutting import parse_seedlings_per_ha
|
||||
|
||||
assert parse_seedlings_per_ha("") is None
|
||||
assert parse_seedlings_per_ha("2,700") == Decimal(2700)
|
||||
for bad in ("0", "-5", "많이"):
|
||||
with pytest.raises(ValueError):
|
||||
parse_seedlings_per_ha(bad)
|
||||
|
||||
|
||||
def test_부록_예시와_표의_어긋남은_기록만() -> None:
|
||||
note = known_gap_note("FP-06-02-02")
|
||||
assert "부록" in note and "0.10" in note and "0.07" in note, note
|
||||
assert "2,700" not in (cached_build().component_gaps.get("FP-06-02-02") or "") # 제안값 없음
|
||||
@@ -39,9 +39,10 @@ def test_제목_없는_잎_공종은_모두_사유가_있음() -> None:
|
||||
|
||||
def test_사유는_표_번호와_모양을_가리킴() -> None:
|
||||
gaps = cached_build().component_gaps
|
||||
# 6-4-1 덩굴걷기 F0158 은 예취기 도구 줄 표로 풀려 사유가 저절로 빠짐 → 6-2-1 둘레베기로 갈음
|
||||
for code, table in (
|
||||
("FP-03-01", "F0075"),
|
||||
("FP-06-04-01", "F0158"),
|
||||
("FP-06-02-01", "F0154"),
|
||||
("FP-07-03", "F0174"),
|
||||
("FP-03-08", "F0082"),
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user