From 6c697931a0ecc040f4ef25e3c1ec4d313531f3c2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 06:00:07 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(b09):=20=EC=98=88=EC=B7=A8=EA=B8=B0?= =?UTF-8?q?=C2=B7=EB=B6=84=EB=AC=B4=EA=B8=B0=20=EB=8F=84=EA=B5=AC=20?= =?UTF-8?q?=EC=A4=84=20=ED=91=9C=20=EB=84=B7=EC=9D=B4=20=EC=84=AC=20?= =?UTF-8?q?=E2=80=94=206-2-2=20=EC=A4=84=EB=B2=A0=EA=B8=B0=C2=B76-2-3=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=EB=B2=A0=EA=B8=B0=20=EA=B0=88=EB=9E=98=20?= =?UTF-8?q?=C2=B7=206-4-1=20=EB=8D=A9=EA=B5=B4=EA=B1=B7=EA=B8=B0=20=C2=B7?= =?UTF-8?q?=206-4-2=20=EB=8D=A9=EA=B5=B4=20=EC=95=BD=EC=A0=9C=20=C2=B7=20?= =?UTF-8?q?=EC=98=88=EC=B7=A8=EA=B8=B0=20=ED=9C=98=EB=B0=9C=EC=9C=A0=205.0?= =?UTF-8?q?=E2=84=93=C2=B7=EC=9E=A1=ED=92=88=2010%=C2=B7=EC=86=90=EB=A3=8C?= =?UTF-8?q?=20=EC=B9=B8=20=C2=B7=20=EB=B6=84=EB=AC=B4=EA=B8=B0=20=EC=86=90?= =?UTF-8?q?=EB=A3=8C=20=EC=B9=B8=20=C2=B7=20=EC=A1=B0=EB=A6=BC=EB=AA=A9=20?= =?UTF-8?q?=EB=B3=B8=EC=88=98=20=EC=B9=B8(=EB=B9=84=EB=A9=B4=20=EB=AC=98?= =?UTF-8?q?=EB=AA=A9=EC=B0=BE=EA=B8=B0=20=ED=95=A9=EC=82=B0=20=EB=AA=BB=20?= =?UTF-8?q?=ED=95=B4=20=EC=9D=BC=EB=B6=80=EB=A7=8C=20=C2=B7=20=EC=A0=9C?= =?UTF-8?q?=EC=95=88=EA=B0=92=20=EC=97=86=EC=9D=8C)=20=C2=B7=20=EB=A7=88?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=20=EA=B0=88=EB=9E=98=20=EC=9E=AC=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- B09_Estimation/B09_Estimation_BasisSheet.py | 1 + B09_Estimation/B09_Estimation_Brushcutting.py | 90 ++++++++++++++++ B09_Estimation/B09_Estimation_Consumables.py | 62 +++++++++-- B09_Estimation/B09_Estimation_KnownGaps.py | 10 ++ ...B09_Estimation_ResourceAxis_JudgedTable.py | 74 ++++++++++++- B09_Estimation/B09_Estimation_Router.py | 2 + .../B09_Estimation_Router_Factors.py | 20 ++++ B09_Estimation/B09_Estimation_UI_Factors.ts | 25 ++++- B09_Estimation/B09_Estimation_UnitPrice.py | 14 ++- .../resource_catalog_ext_2026-01-01.json | 53 +++++++++ .../data_work_item_master/_manifest.json | 6 +- .../work_item_master_2026-01-01.json | 26 ++++- resources/tester/test_b09_brushcutter.py | 102 ++++++++++++++++++ resources/tester/test_b09_unread_tables.py | 3 +- 14 files changed, 465 insertions(+), 23 deletions(-) create mode 100644 B09_Estimation/B09_Estimation_Brushcutting.py create mode 100644 resources/tester/test_b09_brushcutter.py diff --git a/B09_Estimation/B09_Estimation_BasisSheet.py b/B09_Estimation/B09_Estimation_BasisSheet.py index 006bfb3a..ee948bf4 100644 --- a/B09_Estimation/B09_Estimation_BasisSheet.py +++ b/B09_Estimation/B09_Estimation_BasisSheet.py @@ -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: diff --git a/B09_Estimation/B09_Estimation_Brushcutting.py b/B09_Estimation/B09_Estimation_Brushcutting.py new file mode 100644 index 00000000..8194eca3 --- /dev/null +++ b/B09_Estimation/B09_Estimation_Brushcutting.py @@ -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 diff --git a/B09_Estimation/B09_Estimation_Consumables.py b/B09_Estimation/B09_Estimation_Consumables.py index 01a90a12..51429be2 100644 --- a/B09_Estimation/B09_Estimation_Consumables.py +++ b/B09_Estimation/B09_Estimation_Consumables.py @@ -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 diff --git a/B09_Estimation/B09_Estimation_KnownGaps.py b/B09_Estimation/B09_Estimation_KnownGaps.py index 550c4258..e091ff58 100644 --- a/B09_Estimation/B09_Estimation_KnownGaps.py +++ b/B09_Estimation/B09_Estimation_KnownGaps.py @@ -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)」 줄을 " diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py b/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py index 8d1226f4..04c0da1a 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py @@ -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 줄" diff --git a/B09_Estimation/B09_Estimation_Router.py b/B09_Estimation/B09_Estimation_Router.py index e97a7adb..7aa278a3 100644 --- a/B09_Estimation/B09_Estimation_Router.py +++ b/B09_Estimation/B09_Estimation_Router.py @@ -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"))) diff --git a/B09_Estimation/B09_Estimation_Router_Factors.py b/B09_Estimation/B09_Estimation_Router_Factors.py index f2bbc9c6..026a41f3 100644 --- a/B09_Estimation/B09_Estimation_Router_Factors.py +++ b/B09_Estimation/B09_Estimation_Router_Factors.py @@ -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 diff --git a/B09_Estimation/B09_Estimation_UI_Factors.ts b/B09_Estimation/B09_Estimation_UI_Factors.ts index fba13b20..e83d409f 100644 --- a/B09_Estimation/B09_Estimation_UI_Factors.ts +++ b/B09_Estimation/B09_Estimation_UI_Factors.ts @@ -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; + seedlings_per_ha?: string; }, ): Promise { 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) — 고른 것만 붙습니다")); diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 496693b3..5005265c 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -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), ) diff --git a/resources/data_resource_catalog/resource_catalog_ext_2026-01-01.json b/resources/data_resource_catalog/resource_catalog_ext_2026-01-01.json index 2df1d6be..6c5990b4 100644 --- a/resources/data_resource_catalog/resource_catalog_ext_2026-01-01.json +++ b/resources/data_resource_catalog/resource_catalog_ext_2026-01-01.json @@ -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", diff --git a/resources/data_work_item_master/_manifest.json b/resources/data_work_item_master/_manifest.json index 45c5cf4a..2bef77a0 100644 --- a/resources/data_work_item_master/_manifest.json +++ b/resources/data_work_item_master/_manifest.json @@ -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", diff --git a/resources/data_work_item_master/work_item_master_2026-01-01.json b/resources/data_work_item_master/work_item_master_2026-01-01.json index d8ef761d..f2fe5443 100644 --- a/resources/data_work_item_master/work_item_master_2026-01-01.json +++ b/resources/data_work_item_master/work_item_master_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", diff --git a/resources/tester/test_b09_brushcutter.py b/resources/tester/test_b09_brushcutter.py new file mode 100644 index 00000000..5e65e110 --- /dev/null +++ b/resources/tester/test_b09_brushcutter.py @@ -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 "") # 제안값 없음 diff --git a/resources/tester/test_b09_unread_tables.py b/resources/tester/test_b09_unread_tables.py index f4f246bc..4469a8e7 100644 --- a/resources/tester/test_b09_unread_tables.py +++ b/resources/tester/test_b09_unread_tables.py @@ -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"), ): From 3235810953668dcd3737c7c4b4ad51f020d63b61 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 06:16:43 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore(plan):=20=EB=9E=A9=ED=83=91=20?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=E2=80=94=20=EC=98=88=EC=B7=A8=EA=B8=B0?= =?UTF-8?q?=C2=B7=EB=B6=84=EB=AC=B4=EA=B8=B0=20=EB=8F=84=EA=B5=AC=20?= =?UTF-8?q?=EC=A4=84=20=ED=91=9C=20=EB=84=B7=20=EA=B2=80=EC=A6=9D=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=C2=B7=20=ED=99=94=EB=A9=B4=20=EB=AA=A8=EC=96=91=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=EC=A7=80=EC=8B=9C=20=ED=99=95=EC=9D=B8(?= =?UTF-8?q?=ED=95=B4=EB=8B=B9=20=EC=A4=84=20=EC=97=86=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- PLAN_랩탑메인.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/PLAN_랩탑메인.md b/PLAN_랩탑메인.md index 58247073..9f2b4fcf 100644 --- a/PLAN_랩탑메인.md +++ b/PLAN_랩탑메인.md @@ -7,13 +7,14 @@ ### 남은 것 - [ ] ⚠ 자원 이름이 겹치면 남의 공종 풀이가 바뀜(명세 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) +- [ ] ㉯ 나머지 — 제목 없음(지금 셈 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 끝(6c697931) · 남은 차례: 나머지(3-2 작업로 선정 · 3-3 작업로 설치 · 8-6-2 드론방제 · 13-10-2 나무 말뚝박기) · ⓘ 이 창 몫엔 화면 모양(STmate 본뜨기) 때문에 잡아 둔 줄 없음(2026-09-15 사용자 취소 지시 확인) - [ ] 막힌 셋 풀기(원문에 값이 없음 · 입력·견적 칸이 서야 함): 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 경유 표(집재장비) · 천공기 · 예취기 — 쓰는 공종이 풀릴 때 같은 한 벌로 +- [ ] 소형 장비 1일 소모 표(㉯) 가운데 2-1-2 경유 표(집재장비) · 천공기 — 쓰는 공종이 풀릴 때 같은 한 벌로(예취기·배부식분무기는 6c697931 에서 섬) ### 검증완료(2026-09-14 밤 ~ 09-15 · 커밋 해시) +- [X] 예취기·분무기 도구 줄 표 넷(브레인 차례 ② · 판정 ㉠ 지금 칸) — 판정표 「도구 줄」(`tool_rows` · F0155·F0156·F0158·F0159: 단위 칸 인/ha 뒤 값·인력 · 갈래는 단위 앞 앞 칸 · 인/100본 줄은 못 붙은 줄로) · 2장 한 벌에 예취기(휘발유 5.0ℓ/대/일 · 잡품 10% · 손료 0.0084 × 구입가 칸)·배부식분무기(손료만 · 구입가 없으면 호표 안 세움) · 조림목 본수 칸(`B09_Estimation_Brushcutting` · 산출 조건 탭 · 비면 6-2-2·6-2-3 일부만 + 사유 · 제안값 없음) · 부록 예시(2025) 어긋남 기록만 · 마스터 갈래 둘 재생성(6-2-2 본수 구간 셋 · 6-2-3 조림 경과 셋) · 전후(원/ha · 구입가 전): 6-4-1 780,017 · 6-4-2 484,224 · 6-2-2 354,553~590,922 · 6-2-3 827,291~1,181,845(본수 비어 일부만) · 사유 없던 6-4-1 이 풀려 그 사유가 저절로 빠짐 · 프로젝트 내역 변화 0 · ORCA 내 프로젝트 산출 조건에 2,700 넣어 저장·줄베기 1,500본 미만 호표 679,761(묘목찾기 1.89인 붙음) 확인 뒤 비움으로 되돌림. 검증완료(6c697931) - [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) From 5696bd8a97b1c55e19f5c45301806b9bebb0e62c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 06:23:43 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore(plan):=20=EC=B0=BD=EB=B3=84=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=EC=84=9C=20git=20=EB=B0=96=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 답 「계획서 공유는 불가」 — 파일은 로컬에 그대로 두고 git 에만 안 올림(브레인 지시). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- PLAN_랩탑메인.md | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 PLAN_랩탑메인.md diff --git a/PLAN_랩탑메인.md b/PLAN_랩탑메인.md deleted file mode 100644 index 9f2b4fcf..00000000 --- a/PLAN_랩탑메인.md +++ /dev/null @@ -1,43 +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 끝(6c697931) · 남은 차례: 나머지(3-2 작업로 선정 · 3-3 작업로 설치 · 8-6-2 드론방제 · 13-10-2 나무 말뚝박기) · ⓘ 이 창 몫엔 화면 모양(STmate 본뜨기) 때문에 잡아 둔 줄 없음(2026-09-15 사용자 취소 지시 확인) -- [ ] 막힌 셋 풀기(원문에 값이 없음 · 입력·견적 칸이 서야 함): 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 경유 표(집재장비) · 천공기 — 쓰는 공종이 풀릴 때 같은 한 벌로(예취기·배부식분무기는 6c697931 에서 섬) - -### 검증완료(2026-09-14 밤 ~ 09-15 · 커밋 해시) -- [X] 예취기·분무기 도구 줄 표 넷(브레인 차례 ② · 판정 ㉠ 지금 칸) — 판정표 「도구 줄」(`tool_rows` · F0155·F0156·F0158·F0159: 단위 칸 인/ha 뒤 값·인력 · 갈래는 단위 앞 앞 칸 · 인/100본 줄은 못 붙은 줄로) · 2장 한 벌에 예취기(휘발유 5.0ℓ/대/일 · 잡품 10% · 손료 0.0084 × 구입가 칸)·배부식분무기(손료만 · 구입가 없으면 호표 안 세움) · 조림목 본수 칸(`B09_Estimation_Brushcutting` · 산출 조건 탭 · 비면 6-2-2·6-2-3 일부만 + 사유 · 제안값 없음) · 부록 예시(2025) 어긋남 기록만 · 마스터 갈래 둘 재생성(6-2-2 본수 구간 셋 · 6-2-3 조림 경과 셋) · 전후(원/ha · 구입가 전): 6-4-1 780,017 · 6-4-2 484,224 · 6-2-2 354,553~590,922 · 6-2-3 827,291~1,181,845(본수 비어 일부만) · 사유 없던 6-4-1 이 풀려 그 사유가 저절로 빠짐 · 프로젝트 내역 변화 0 · ORCA 내 프로젝트 산출 조건에 2,700 넣어 저장·줄베기 1,500본 미만 호표 679,761(묘목찾기 1.89인 붙음) 확인 뒤 비움으로 되돌림. 검증완료(6c697931) -- [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