Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
364c28a700 |
@@ -21,6 +21,9 @@ from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import (
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_Status import (
|
||||
STATUS_COUNTED_ELSEWHERE as PREP_COUNTED_ELSEWHERE,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_Status import (
|
||||
STATUS_NEEDS_INPUT as PREP_NEEDS_INPUT,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_Status import (
|
||||
STATUS_NOT_APPLICABLE as PREP_NOT_APPLICABLE,
|
||||
)
|
||||
@@ -79,7 +82,9 @@ def _preparation_rows(preparation_table: dict[str, Any]) -> list[dict[str, Any]]
|
||||
"composite_parts": None,
|
||||
"structure_kind": None,
|
||||
# ⚠ 못 서는 까닭을 그대로 넘긴다 — 받는 쪽이 「만들어야 할 것」 목록에 얹는다.
|
||||
"blocked_kind": None if ready else _prep_blocked_kind(status),
|
||||
"blocked_kind": None
|
||||
if ready
|
||||
else row.get("blocked_kind") or _prep_blocked_kind(status),
|
||||
"blocked_reason": "" if ready else str(row.get("reason") or status),
|
||||
# ⚠ 준비공 줄도 갈래를 실어 보낸다(2026-09-09) — 종전에는 늘 `None` 이라
|
||||
# 표토 운반처럼 **부모 공종코드**로 가는 줄이 B09 에서 「후보 N건」에 머물렀다.
|
||||
@@ -105,9 +110,15 @@ def _preparation_rows(preparation_table: dict[str, Any]) -> list[dict[str, Any]]
|
||||
|
||||
|
||||
def _prep_blocked_kind(status: str) -> str | None:
|
||||
"""준비공 줄의 상태를 **막힌 갈래 셋** 중 하나로 옮긴다. 모르는 상태는 `None`."""
|
||||
if status == PREP_PENDING:
|
||||
"""준비공 줄의 상태를 **막힌 갈래 셋** 중 하나로 옮긴다. 줄이 갈래를 적어 오면 그것이 이긴다.
|
||||
|
||||
⚠ 「근거 없음」을 `input_missing` 으로 보내면 사방 원단위처럼 **우리가 만들 줄**이 B09 에
|
||||
「입력이 필요합니다」로 뜬다 — 사용자가 넣을 칸을 찾아 헤맨다(2026-09-14 브레인 ㉴).
|
||||
"""
|
||||
if status == PREP_NEEDS_INPUT:
|
||||
return BLOCKED_INPUT_MISSING
|
||||
if status == PREP_PENDING:
|
||||
return BLOCKED_UNIT_DATA_MISSING
|
||||
if status == PREP_COUNTED_ELSEWHERE:
|
||||
# 다른 표에서 이미 선 줄 — 막힌 것이 아니라 **여기서 세면 안 되는** 줄이다.
|
||||
return None
|
||||
|
||||
@@ -50,15 +50,21 @@ from B08_Quantity.B08_Quantity_Engine_Preparation_Ancillary import ( # noqa: E4
|
||||
ANCILLARY_ITEMS,
|
||||
ancillary_rows,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_FrameMaterial import ( # noqa: E402
|
||||
FRAME_MATERIAL_SUGGESTED,
|
||||
frame_material_rows,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_Status import ( # noqa: E402
|
||||
STATUS_COUNTED_ELSEWHERE,
|
||||
STATUS_NEEDS_INPUT,
|
||||
STATUS_NOT_APPLICABLE,
|
||||
STATUS_PENDING,
|
||||
STATUS_READY,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_TreeWaste import tree_waste_rows # noqa: E402
|
||||
|
||||
__all__ = ["ANCILLARY_ITEMS", "ancillary_rows"] # 갈라 나간 뒤에도 여기서 읽을 수 있게
|
||||
# 갈라 나간 뒤에도 여기서 읽을 수 있게(부대시설 2026-09-09 · 규준틀 재료 2026-09-14).
|
||||
__all__ = ["ANCILLARY_ITEMS", "FRAME_MATERIAL_SUGGESTED", "ancillary_rows", "frame_material_rows"]
|
||||
|
||||
|
||||
def batter_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[str]]:
|
||||
@@ -170,7 +176,7 @@ def _topsoil_row(
|
||||
return {
|
||||
**base,
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"status": STATUS_NEEDS_INPUT,
|
||||
"reason": (
|
||||
"노면 면적을 못 셉니다 — 횡단 설계에 노체 끝(노면 폭)이 없는 측점이 있음. "
|
||||
f"대상은 노면 + 절토대상지(별표2) · 절토 사면 {cut:,.1f}㎡ 만으로는 세우지 않음"
|
||||
@@ -184,7 +190,7 @@ def _topsoil_row(
|
||||
return {
|
||||
**base,
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"status": STATUS_NEEDS_INPUT,
|
||||
"reason": (
|
||||
"대상 면적이 0 ㎡ 입니다 — 횡단·사면표가 아직 서지 않았습니다. "
|
||||
"0 ㎡ 로 내면 「표토가 없는 노선」으로 읽히므로 값을 세우지 않습니다"
|
||||
@@ -309,7 +315,7 @@ def _root_removal_row(slope: dict[str, float], stand_volume_class: str | None) -
|
||||
"amount": area if area > 0 else None,
|
||||
# ⭐ 2026-09-13 판정 Ⓑ — 셈은 토공집계 「지장목제거 · 뿌리뽑기」(FP-09-21)가 한다.
|
||||
# 여기는 **보이되 안 실린다**(같은 면적 · 같은 작업 — 또 세면 이중계상).
|
||||
"status": STATUS_COUNTED_ELSEWHERE if area > 0 else STATUS_PENDING,
|
||||
"status": STATUS_COUNTED_ELSEWHERE if area > 0 else STATUS_NEEDS_INPUT,
|
||||
"reason": " · ".join(reasons),
|
||||
"reference_amount": area,
|
||||
"work_item_code": None,
|
||||
@@ -364,7 +370,7 @@ def chipping_rows(enabled: Any, volume_m3: Any) -> list[dict[str, Any]]:
|
||||
"item": CHIPPING_ITEM,
|
||||
"unit": "㎥",
|
||||
"amount": amount,
|
||||
"status": STATUS_READY if amount and amount > 0 else STATUS_PENDING,
|
||||
"status": STATUS_READY if amount and amount > 0 else STATUS_NEEDS_INPUT,
|
||||
"reason": CHIPPING_ON_NOTE if not amount else "산출 조건에서 넣은 부피 (확정 5차 5번)",
|
||||
"work_item_code": CHIPPING_CODE,
|
||||
}
|
||||
@@ -382,7 +388,7 @@ def _root_steps_rows(
|
||||
"item": "뿌리 적재",
|
||||
"unit": "㎡",
|
||||
"amount": area if area > 0 else None,
|
||||
"status": STATUS_READY if area > 0 else STATUS_PENDING,
|
||||
"status": STATUS_READY if area > 0 else STATUS_NEEDS_INPUT,
|
||||
"reason": (
|
||||
f"{ROOT_STEPS_NOTE} · {ROOT_REMOVAL_BASIS}"
|
||||
" · ⚠ **품셈 9-20-2 는 「10주당」이라 밑수 축이 다름** — 면적 축으로 내고"
|
||||
@@ -434,7 +440,7 @@ def _topsoil_haul_row(
|
||||
"item": "표토 운반·적치",
|
||||
"unit": "㎥",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"status": STATUS_NEEDS_INPUT,
|
||||
"reason": f"{TOPSOIL_HAUL_LAW} · 제거 면적이 아직 안 서서 운반도 못 셈",
|
||||
"work_item_code": None,
|
||||
}
|
||||
@@ -444,7 +450,7 @@ def _topsoil_haul_row(
|
||||
"item": "표토 운반·적치",
|
||||
"unit": "㎥",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"status": STATUS_NEEDS_INPUT,
|
||||
"reason": (
|
||||
f"{TOPSOIL_HAUL_LAW} · 운반 부피 = 제거 면적 × 표토 두께 — 두께가 아직 입력되지"
|
||||
f" 않았습니다. {TOPSOIL_ORIGINAL_APPLIED} (제거 면적 {float(area):,.1f}㎡)"
|
||||
@@ -472,7 +478,7 @@ def _topsoil_haul_row(
|
||||
"item": "표토 운반·적치",
|
||||
"unit": "㎥",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"status": STATUS_NEEDS_INPUT,
|
||||
"reason": (
|
||||
f"{TOPSOIL_HAUL_LAW} · 운반거리가 아직 입력되지 않았습니다 — 「최고 홍수위보다"
|
||||
f" 높은 장소」는 현장에서 정하는 자리라 품셈이 거리를 주지 않습니다"
|
||||
@@ -505,88 +511,17 @@ def _topsoil_haul_row(
|
||||
}
|
||||
|
||||
|
||||
#: 규준틀 재료 — **세는 것은 확정이고 수량만 몰랐던 자리**(2026-09-09).
|
||||
#: 품셈 11-2·11-3 [주]④ 「재료량은 **설계수량에 따른다**」 ⇒ 품셈이 값을 안 주는 것이지
|
||||
#: 「안 센다」가 아니다. 그래서 **제안값을 보이고 사용자가 고치는** 모양으로 둔다
|
||||
#: (확정 ⑨·⑩ 과 같은 틀 — 「가는 기본값이고 나 선택처럼 동작할 수 있어야 함」).
|
||||
#: ⚠ **제안값은 실무 관측값이지 법정 기준이 아니다** — 울진 소광 원단위 라이브러리 §8
|
||||
#: 「규준틀 수평 | 개소 | 각재 50×50 0.0044㎥ · 판재 T12 0.0029㎥ · 못 0.03㎏」.
|
||||
#: ⚠ **비탈 규준틀 값은 그 시트에 없다** — 수평 값을 준용하고 그 사실을 사유에 적는다.
|
||||
#: ⚠ **손율은 원문에 있다** — 품셈 11-2 [주]③ 비탈 **50%** · 11-3 [주]③ 수평 **80%**.
|
||||
FRAME_MATERIAL_SUGGESTED = {
|
||||
"각재 50×50": (0.0044, "㎥"),
|
||||
"판재 T12": (0.0029, "㎥"),
|
||||
"못": (0.03, "㎏"),
|
||||
}
|
||||
FRAME_MATERIAL_SOURCE = (
|
||||
"⚠ 실무 관측값(울진 소광 원단위 라이브러리 §8 규준틀 수평) — **법정 기준 아님**."
|
||||
" 품셈 11-2·11-3 [주]④ 는 「재료량은 설계수량에 따른다」로만 둠. 산출 조건에서 고칠 수 있음"
|
||||
)
|
||||
FRAME_LOSS_RATE = {"비탈 규준틀": 50, "수평 규준틀": 80}
|
||||
#: 자재 줄의 **이름 · 규격** — 이름 칸에 규격을 섞으면 할증률표(「각재」·「판재」)와 안 맞음
|
||||
#: (2026-09-14 · 돌 줄 8210c2b7 과 같은 병). 산출 조건 키·관급구분 키(이름+규격)는 글자 그대로.
|
||||
FRAME_MATERIAL_NAME_SPEC = {"각재 50×50": ("각재", "50×50"), "판재 T12": ("판재", "T12")}
|
||||
|
||||
|
||||
def frame_material_rows(
|
||||
frame_rows: list[dict[str, Any]], overrides: dict[str, Any] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""규준틀 재료 — 개소 × 개소당 수량. **자재 축으로 보낸다.**
|
||||
|
||||
⚠ 개소가 안 서면 재료도 안 선다(밑수가 그 줄이다).
|
||||
⚠ 값은 **제안값**이고 산출 조건에서 덮어쓸 수 있다 — 그 사실이 사유에 적힌다.
|
||||
"""
|
||||
given = overrides or {}
|
||||
rows: list[dict[str, Any]] = []
|
||||
for frame in frame_rows:
|
||||
count = frame.get("amount")
|
||||
if not count:
|
||||
continue
|
||||
loss = FRAME_LOSS_RATE.get(str(frame.get("item")), None)
|
||||
for name, (default, unit) in FRAME_MATERIAL_SUGGESTED.items():
|
||||
raw = given.get(name)
|
||||
try:
|
||||
per_ea = (
|
||||
float(raw) if raw is not None and str(raw).strip() != "" else float(default)
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
per_ea = float(default)
|
||||
picked = "산출 조건에서 고른 값" if raw not in (None, "") else "제안값(기본)"
|
||||
material, spec = FRAME_MATERIAL_NAME_SPEC.get(name, (name, ""))
|
||||
rows.append(
|
||||
{
|
||||
"name": material,
|
||||
"spec": spec,
|
||||
"unit": unit,
|
||||
"amount": float(count) * per_ea,
|
||||
"destination": "material",
|
||||
"source": str(frame.get("item") or "규준틀"),
|
||||
"basis": (
|
||||
f"{frame.get('item')} {float(count):g}개소 × {per_ea:g}{unit}/개소"
|
||||
f" ({picked}) · {FRAME_MATERIAL_SOURCE}"
|
||||
# ⚠ 준용이라는 사실이 상수 주석에만 있고 **화면 근거에는 없던**
|
||||
# 자리다 — 값이 서면 어디서 온 값인지 안 보인다(2026-09-09 감사).
|
||||
+ (
|
||||
" · ⚠ 비탈 규준틀 재료량은 그 시트에 없어 **수평 값을 준용**함"
|
||||
if str(frame.get("item")) == "비탈 규준틀"
|
||||
else ""
|
||||
)
|
||||
+ (f" · 손율 {loss}%(품셈 [주]③)" if loss else "")
|
||||
),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _batter_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""비탈 규준틀 한 줄. **개소는 원문 기준으로 서고 재료는 미확보**다."""
|
||||
count, notes = batter_frame_count(slope_rows)
|
||||
# 사면표가 있는데 0 개소면 **이 노선엔 필요 없는 것**이다 — 「근거 없음」이 아니다(㉴).
|
||||
idle = STATUS_NOT_APPLICABLE if slope_rows else STATUS_NEEDS_INPUT
|
||||
return {
|
||||
"group": "준비공",
|
||||
"item": "비탈 규준틀",
|
||||
"unit": "개소",
|
||||
"amount": float(count) if count else None,
|
||||
"status": STATUS_READY if count else STATUS_PENDING,
|
||||
"status": STATUS_READY if count else idle,
|
||||
"reason": (
|
||||
"; ".join(notes)
|
||||
+ " · 재료량은 품셈 11-2 [주]④ 「설계수량에 따른다」 —"
|
||||
@@ -604,7 +539,12 @@ def _level_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"item": "수평 규준틀",
|
||||
"unit": "개소",
|
||||
"amount": float(count) if count is not None else None,
|
||||
"status": STATUS_READY if count is not None else STATUS_PENDING,
|
||||
# 사면표가 안 섰으면 앞 단계 몫(입력) · 섰는데 성토고 칸이 없으면 우리 자료가 없는 것.
|
||||
"status": STATUS_READY
|
||||
if count is not None
|
||||
else STATUS_PENDING
|
||||
if slope_rows
|
||||
else STATUS_NEEDS_INPUT,
|
||||
"reason": "; ".join(notes)
|
||||
+ (
|
||||
" · 재료량은 품셈 11-3 [주]④ 「설계수량에 따른다」 —"
|
||||
@@ -705,6 +645,7 @@ def build_table(
|
||||
"columns": ["구분", "공종", "단위", "수량", "상태", "사유"],
|
||||
"rows": rows,
|
||||
"ready_count": sum(1 for row in rows if row["status"] == STATUS_READY),
|
||||
"input_count": sum(1 for row in rows if row["status"] == STATUS_NEEDS_INPUT),
|
||||
"pending_count": sum(1 for row in rows if row["status"] == STATUS_PENDING),
|
||||
"row_count": len(rows),
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Any
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_Status import (
|
||||
REASON_NO_WORK_ITEM,
|
||||
STATUS_NEEDS_INPUT,
|
||||
STATUS_PENDING,
|
||||
STATUS_READY,
|
||||
)
|
||||
@@ -94,7 +95,7 @@ def ancillary_rows(counts: dict[str, Any] | None = None) -> list[dict[str, Any]]
|
||||
reasons.append(REASON_NO_WORK_ITEM)
|
||||
if amount is None:
|
||||
reasons.append("개소가 아직 입력되지 않았습니다 — 넣으면 물량이 섭니다")
|
||||
status = STATUS_PENDING
|
||||
status = STATUS_NEEDS_INPUT
|
||||
else:
|
||||
status = STATUS_READY if spec["code"] else STATUS_PENDING
|
||||
rows.append(
|
||||
@@ -106,6 +107,8 @@ def ancillary_rows(counts: dict[str, Any] | None = None) -> list[dict[str, Any]]
|
||||
"status": status,
|
||||
"work_item_code": spec["code"],
|
||||
"legal_required": bool(spec["legal"]),
|
||||
# 품셈에 공종이 없는 줄 — 금액은 별도 단가(사람 입력)로만 서서 인계는 늘 입력 갈래.
|
||||
**({} if spec["code"] else {"blocked_kind": "input_missing"}),
|
||||
"reason": " · ".join(reasons),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""준비공 — 규준틀 재료 줄 (`B08_Quantity_Engine_Preparation` 에서 갈라냄 · 2026-09-14 700줄 제한).
|
||||
|
||||
내용·규칙은 그대로 옮겼다 — 개소 × 개소당 수량을 자재 축으로 보내고,
|
||||
값은 제안값이며 산출 조건이 이긴다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
#: 규준틀 재료 — **세는 것은 확정이고 수량만 몰랐던 자리**(2026-09-09).
|
||||
#: 품셈 11-2·11-3 [주]④ 「재료량은 **설계수량에 따른다**」 ⇒ 품셈이 값을 안 주는 것이지
|
||||
#: 「안 센다」가 아니다. 그래서 **제안값을 보이고 사용자가 고치는** 모양으로 둔다
|
||||
#: (확정 ⑨·⑩ 과 같은 틀 — 「가는 기본값이고 나 선택처럼 동작할 수 있어야 함」).
|
||||
#: ⚠ **제안값은 실무 관측값이지 법정 기준이 아니다** — 울진 소광 원단위 라이브러리 §8
|
||||
#: 「규준틀 수평 | 개소 | 각재 50×50 0.0044㎥ · 판재 T12 0.0029㎥ · 못 0.03㎏」.
|
||||
#: ⚠ **비탈 규준틀 값은 그 시트에 없다** — 수평 값을 준용하고 그 사실을 사유에 적는다.
|
||||
#: ⚠ **손율은 원문에 있다** — 품셈 11-2 [주]③ 비탈 **50%** · 11-3 [주]③ 수평 **80%**.
|
||||
FRAME_MATERIAL_SUGGESTED = {
|
||||
"각재 50×50": (0.0044, "㎥"),
|
||||
"판재 T12": (0.0029, "㎥"),
|
||||
"못": (0.03, "㎏"),
|
||||
}
|
||||
FRAME_MATERIAL_SOURCE = (
|
||||
"⚠ 실무 관측값(울진 소광 원단위 라이브러리 §8 규준틀 수평) — **법정 기준 아님**."
|
||||
" 품셈 11-2·11-3 [주]④ 는 「재료량은 설계수량에 따른다」로만 둠. 산출 조건에서 고칠 수 있음"
|
||||
)
|
||||
FRAME_LOSS_RATE = {"비탈 규준틀": 50, "수평 규준틀": 80}
|
||||
#: 자재 줄의 **이름 · 규격** — 이름 칸에 규격을 섞으면 할증률표(「각재」·「판재」)와 안 맞음
|
||||
#: (2026-09-14 · 돌 줄 8210c2b7 과 같은 병). 산출 조건 키·관급구분 키(이름+규격)는 글자 그대로.
|
||||
FRAME_MATERIAL_NAME_SPEC = {"각재 50×50": ("각재", "50×50"), "판재 T12": ("판재", "T12")}
|
||||
|
||||
|
||||
def frame_material_rows(
|
||||
frame_rows: list[dict[str, Any]], overrides: dict[str, Any] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""규준틀 재료 — 개소 × 개소당 수량. **자재 축으로 보낸다.**
|
||||
|
||||
⚠ 개소가 안 서면 재료도 안 선다(밑수가 그 줄이다).
|
||||
⚠ 값은 **제안값**이고 산출 조건에서 덮어쓸 수 있다 — 그 사실이 사유에 적힌다.
|
||||
"""
|
||||
given = overrides or {}
|
||||
rows: list[dict[str, Any]] = []
|
||||
for frame in frame_rows:
|
||||
count = frame.get("amount")
|
||||
if not count:
|
||||
continue
|
||||
loss = FRAME_LOSS_RATE.get(str(frame.get("item")), None)
|
||||
for name, (default, unit) in FRAME_MATERIAL_SUGGESTED.items():
|
||||
raw = given.get(name)
|
||||
try:
|
||||
per_ea = (
|
||||
float(raw) if raw is not None and str(raw).strip() != "" else float(default)
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
per_ea = float(default)
|
||||
picked = "산출 조건에서 고른 값" if raw not in (None, "") else "제안값(기본)"
|
||||
material, spec = FRAME_MATERIAL_NAME_SPEC.get(name, (name, ""))
|
||||
rows.append(
|
||||
{
|
||||
"name": material,
|
||||
"spec": spec,
|
||||
"unit": unit,
|
||||
"amount": float(count) * per_ea,
|
||||
"destination": "material",
|
||||
"source": str(frame.get("item") or "규준틀"),
|
||||
"basis": (
|
||||
f"{frame.get('item')} {float(count):g}개소 × {per_ea:g}{unit}/개소"
|
||||
f" ({picked}) · {FRAME_MATERIAL_SOURCE}"
|
||||
# ⚠ 준용이라는 사실이 상수 주석에만 있고 **화면 근거에는 없던**
|
||||
# 자리다 — 값이 서면 어디서 온 값인지 안 보인다(2026-09-09 감사).
|
||||
+ (
|
||||
" · ⚠ 비탈 규준틀 재료량은 그 시트에 없어 **수평 값을 준용**함"
|
||||
if str(frame.get("item")) == "비탈 규준틀"
|
||||
else ""
|
||||
)
|
||||
+ (f" · 손율 {loss}%(품셈 [주]③)" if loss else "")
|
||||
),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
@@ -8,6 +8,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
STATUS_READY = "값 있음"
|
||||
#: 사람이 넣으면 서는 줄 — 산출 조건 칸, 또는 앞 단계(횡단·사면표)를 마치면 섬.
|
||||
#: 인계 `input_missing`.
|
||||
#: ⚠ 「근거 없음」과 가른다 — 한 낱말로 덮으면 입력만 넣으면 서는 줄이 「못 세움」으로 읽힌다
|
||||
#: (2026-09-14 브레인 ㉴ · 인계는 반대로 사방 원단위까지 「입력이 필요합니다」로 보냈다).
|
||||
STATUS_NEEDS_INPUT = "입력이 필요함"
|
||||
#: 자료·산식이 우리에게 없는 줄 — 입력으로는 안 풀림. 인계 `unit_data_missing`.
|
||||
STATUS_PENDING = "값을 낼 근거가 없음"
|
||||
STATUS_COUNTED_ELSEWHERE = "다른 표에서 이미 섬"
|
||||
STATUS_NOT_APPLICABLE = "해당 없음"
|
||||
|
||||
@@ -27,7 +27,7 @@ from __future__ import annotations
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_Status import STATUS_PENDING, STATUS_READY
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_Status import STATUS_NEEDS_INPUT, STATUS_READY
|
||||
|
||||
TREE_WASTE_ITEM = "임목폐기물 처리"
|
||||
STEM_FORM_FACTOR = 0.5 # k
|
||||
@@ -139,7 +139,7 @@ def tree_waste_rows(
|
||||
{
|
||||
**base,
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"status": STATUS_NEEDS_INPUT, # 조사값 칸 · 앞 단계 사면표 — 둘 다 사람 몫
|
||||
"reason": f"{why}. 산식: {TREE_WASTE_BASIS} · {root_basis}",
|
||||
"reference_amount": area,
|
||||
}
|
||||
|
||||
@@ -243,6 +243,8 @@ export interface PreparationRow {
|
||||
export interface PreparationTable {
|
||||
columns: string[];
|
||||
rows: PreparationRow[];
|
||||
/** 입력하면 서는 줄 수 — 「근거 없음」(`pending_count`)과 갈라 셈(2026-09-14). */
|
||||
input_count?: number;
|
||||
pending_count: number;
|
||||
row_count: number;
|
||||
}
|
||||
@@ -261,7 +263,7 @@ export function renderPreparationGrid(
|
||||
|
||||
const caption = document.createElement("p");
|
||||
caption.className = "b08-grid__caption";
|
||||
caption.textContent = `${table.row_count}줄 · 값을 낼 근거가 아직 없는 줄 ${table.pending_count}개`;
|
||||
caption.textContent = `${table.row_count}줄 · 입력이 필요한 줄 ${table.input_count ?? 0}개 · 값을 낼 근거가 없는 줄 ${table.pending_count}개`;
|
||||
const unconfirmed = table.rows.reduce((sum, row) => sum + (row.unconfirmed ?? 0), 0);
|
||||
if (unconfirmed) {
|
||||
const badge = document.createElement("span");
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""준비공 상태 칸 — 「입력이 필요함」과 「값을 낼 근거가 없음」을 가른다 (2026-09-14 브레인 ㉴).
|
||||
|
||||
훑기에서 잡은 거짓 사유: 상태 칸이 **입력만 넣으면 서는 줄**(표토 두께·거리 · 임목폐기물 조사값 ·
|
||||
부대시설 개소 · 임목파쇄 부피)까지 「값을 낼 근거가 없음」으로 덮었다. 반대로 인계는
|
||||
**우리가 만들어야 하는 줄**(사방 원단위 · 뿌리 부피)까지 「입력이 필요합니다」로 보냈다.
|
||||
|
||||
① 입력이면 풀리는 줄 → 상태 「입력이 필요함」 · 인계 `input_missing`
|
||||
② 자료·산식이 없는 줄 → 상태 「값을 낼 근거가 없음」 · 인계 `unit_data_missing`
|
||||
③ 앞 단계(횡단·사면표)가 안 선 줄도 사람이 할 일이라 ①
|
||||
④ 규준틀이 필요 없는 노선(비탈길이 10m 이상 구간 없음)은 「해당 없음」
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation import build_table
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation_Status import (
|
||||
STATUS_NEEDS_INPUT,
|
||||
STATUS_NOT_APPLICABLE,
|
||||
STATUS_PENDING,
|
||||
)
|
||||
|
||||
SLOPE = {"tree_removal_fill": 600.0, "tree_removal_cut": 400.0, "face_dressing_cut": 300.0}
|
||||
SHORT_SLOPES = [{"lengths": {"fill": 4.0}, "distance_m": 20.0, "fill_height_m": 2.0}]
|
||||
|
||||
|
||||
def _table(**overrides: object) -> dict:
|
||||
args: dict = {
|
||||
"slope_totals": SLOPE,
|
||||
"structures": [{"type_id": "erosion_check"}],
|
||||
"slope_rows": SHORT_SLOPES,
|
||||
"road_surface_area_m2": 1000.0,
|
||||
"chipping_enabled": True,
|
||||
"tree_waste": {},
|
||||
"ancillary_counts": {},
|
||||
}
|
||||
args.update(overrides)
|
||||
return build_table(**args)
|
||||
|
||||
|
||||
def _by_item(table: dict) -> dict[str, dict]:
|
||||
return {row["item"]: row for row in table["rows"]}
|
||||
|
||||
|
||||
def test_입력이면_서는_줄은_입력이_필요함() -> None:
|
||||
rows = _by_item(_table())
|
||||
for item in (
|
||||
"표토 운반·적치",
|
||||
"임목파쇄",
|
||||
"임목폐기물 처리",
|
||||
"국가지점번호판",
|
||||
"가설창고(컨테이너)",
|
||||
):
|
||||
assert rows[item]["status"] == STATUS_NEEDS_INPUT, item
|
||||
# 두께를 넣으면 거리 칸이 남는다 — 여전히 입력이 필요함.
|
||||
with_thickness = _by_item(_table(topsoil_thickness_m=0.2))
|
||||
assert with_thickness["표토 운반·적치"]["status"] == STATUS_NEEDS_INPUT
|
||||
|
||||
|
||||
def test_자료나_산식이_없는_줄은_근거가_없음() -> None:
|
||||
rows = _by_item(_table())
|
||||
assert rows["뿌리 운반"]["status"] == STATUS_PENDING
|
||||
assert (
|
||||
rows[next(k for k, r in rows.items() if r["group"] == "사방공")]["status"] == STATUS_PENDING
|
||||
)
|
||||
|
||||
|
||||
def test_앞_단계가_안_섰으면_입력이_필요함() -> None:
|
||||
rows = _by_item(_table(slope_totals={}, slope_rows=[], road_surface_area_m2=None))
|
||||
assert rows["표토제거"]["status"] == STATUS_NEEDS_INPUT
|
||||
assert rows["뿌리 적재"]["status"] == STATUS_NEEDS_INPUT
|
||||
assert rows["비탈 규준틀"]["status"] == STATUS_NEEDS_INPUT
|
||||
assert rows["수평 규준틀"]["status"] == STATUS_NEEDS_INPUT
|
||||
|
||||
|
||||
def test_규준틀이_필요_없는_노선은_해당_없음() -> None:
|
||||
assert _by_item(_table())["비탈 규준틀"]["status"] == STATUS_NOT_APPLICABLE
|
||||
|
||||
|
||||
def test_인계_막힘_갈래가_상태를_따른다() -> None:
|
||||
table = _table()
|
||||
handed = {row["name"]: row for row in build_handoff(preparation_table=table)["work_items"]}
|
||||
assert handed["표토 운반·적치"]["blocked_kind"] == "input_missing"
|
||||
assert handed["임목파쇄"]["blocked_kind"] == "input_missing"
|
||||
# 우리가 만들어야 하는 줄 — 「입력이 필요합니다」로 보내면 사용자가 헛걸음한다.
|
||||
assert handed["뿌리 운반"]["blocked_kind"] == "unit_data_missing"
|
||||
assert handed["골막이"]["blocked_kind"] == "unit_data_missing"
|
||||
# 해당 없음은 막힘이 아니다.
|
||||
assert handed["비탈 규준틀"]["blocked_kind"] is None
|
||||
# 공종이 품셈에 없는 부대시설은 개소를 넣어도 종전대로 「입력」(별도 단가) 갈래.
|
||||
entered = _table(ancillary_counts={"national_point_sign": 3})
|
||||
sign = next(
|
||||
row
|
||||
for row in build_handoff(preparation_table=entered)["work_items"]
|
||||
if row["name"] == "국가지점번호판"
|
||||
)
|
||||
assert sign["blocked_kind"] == "input_missing"
|
||||
|
||||
|
||||
def test_표_머리가_두_갈래를_따로_센다() -> None:
|
||||
table = _table()
|
||||
rows = table["rows"]
|
||||
assert table["input_count"] == sum(1 for r in rows if r["status"] == STATUS_NEEDS_INPUT)
|
||||
assert table["pending_count"] == sum(1 for r in rows if r["status"] == STATUS_PENDING)
|
||||
assert table["input_count"] > 0 and table["pending_count"] > 0
|
||||
@@ -23,7 +23,7 @@ sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation import ( # noqa: E402
|
||||
STATUS_COUNTED_ELSEWHERE,
|
||||
STATUS_PENDING,
|
||||
STATUS_NEEDS_INPUT,
|
||||
STATUS_READY,
|
||||
build_table,
|
||||
preparation_rows,
|
||||
@@ -61,7 +61,8 @@ def test_20m_이내면_제거_품에_들어_따로_안_셈() -> None:
|
||||
def test_거리를_안_넣으면_막고_사유() -> None:
|
||||
"""⚠ 「최고 홍수위보다 높은 장소」는 현장이 정한다 — 품셈이 거리를 주지 않는다."""
|
||||
row = 줄(0.2, None)["표토 운반·적치"]
|
||||
assert row["amount"] is None and row["status"] == STATUS_PENDING
|
||||
# 거리만 넣으면 서는 줄 — 「근거 없음」이 아니라 「입력이 필요함」(2026-09-14 ㉴).
|
||||
assert row["amount"] is None and row["status"] == STATUS_NEEDS_INPUT
|
||||
assert "운반거리가 아직 입력되지 않았습니다" in row["reason"]
|
||||
assert row["reference_amount"] == 200.0 # 값을 버리지 않는다
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation import ( # noqa: E402
|
||||
STATUS_PENDING,
|
||||
STATUS_NEEDS_INPUT,
|
||||
STATUS_READY,
|
||||
build_table,
|
||||
)
|
||||
@@ -43,7 +43,8 @@ def test_켜면_줄이_선다() -> None:
|
||||
|
||||
def test_켜도_부피는_지어내지_않는다() -> None:
|
||||
row = 표(True)["임목파쇄"]
|
||||
assert row["amount"] is None and row["status"] == STATUS_PENDING
|
||||
# 부피를 넣으면 서는 줄 — 「입력이 필요함」(2026-09-14 ㉴).
|
||||
assert row["amount"] is None and row["status"] == STATUS_NEEDS_INPUT
|
||||
assert "부피" in row["reason"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user