feat(B08): 층따기 길이 칸 — 면적 × 길이로 ㎥ 를 냄 (확정 2차 ①)
⭐ 사용자 확정 2차 ① — 「㎡ 유지. 부피는 **사용자가 지정한 길이값을 곱해** 쓴다」. 그러면 품셈 ㎥ 단가를 그대로 쓸 수 있어 **단위 불일치가 풀림**(2,124만원 자리). - 설정 `quantity.bench_cut_depth_m` 신설. [저장]으로도 넣을 수 있게 요청 모양에 칸을 두고, 「안 정함」으로 되돌릴 수 있게 nullable 목록에도 넣음. - 인계 줄: 길이가 있으면 **면적 × 길이 = ㎥** 로 내고, 규격 칸에 「면적 13,696.75㎡ × 길이 0.5m」로 **어디서 온 값인지** 남김. - ⚠ **면적을 없애지 않음** — 횡단도 하단 표가 면적을 씀. ㎥ 를 덧붙이는 것. - ⚠ **길이 기본값을 임의로 박지 않음** — 없으면 종전처럼 막고 사유를 냄 (교본 「층따기 높이·폭은 **설계도서에 명시**」라 설계 입력임). - 매핑 밑수를 ㎥ 로 되돌리고 그 까닭을 적음(공식이 체적 기준). 실측 — 길이 0.5m 를 넣으면 13,696.75㎡ → **6,848.375㎥** 로 서고 in_bill True. 안 넣으면 0 · input_missing · 「길이를 넣으면 면적 × 길이로 물량이 섭니다」. 시험 748 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,7 @@ def build_handoff(
|
||||
ground_classes: list[str] | None = None,
|
||||
ground_methods: dict[str, str | None] | None = None,
|
||||
concrete_placing_method: str | None = None,
|
||||
bench_cut_depth_m: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""B09 가 그대로 받는 모양. 없는 표는 건너뛰되 **빈 표와 구별해 적는다**."""
|
||||
table = mapping or load_mapping()
|
||||
@@ -112,7 +113,7 @@ def build_handoff(
|
||||
|
||||
methods = {key: value for key, value in (ground_methods or {}).items() if value}
|
||||
if summary_table:
|
||||
rows, misses = _earthwork_rows(summary_table, table, methods)
|
||||
rows, misses = _earthwork_rows(summary_table, table, methods, bench_cut_depth_m)
|
||||
work_items.extend(rows)
|
||||
unmatched.extend(misses)
|
||||
if haul_table:
|
||||
|
||||
@@ -89,10 +89,17 @@ def _basis_mismatch(entry: dict[str, Any] | None, unit: str) -> tuple[str, str,
|
||||
)
|
||||
|
||||
|
||||
#: 면적(㎡)으로 서지만 **길이를 곱해 ㎥ 로 내보내는** 공종 — 지금은 층따기뿐이다.
|
||||
#: ⭐ 2026-09-09 사용자 확정 2차 ① — 「면적이 정본이고 부피는 사용자가 지정한 길이를 곱해 쓴다」.
|
||||
#: ⚠ 면적을 없애지 않는다(횡단도 하단 표가 면적을 쓴다) — **㎥ 를 덧붙이는 것**이다.
|
||||
AREA_TIMES_DEPTH_GROUPS = {"층따기"}
|
||||
|
||||
|
||||
def _earthwork_rows(
|
||||
summary_table: dict[str, Any],
|
||||
mapping: WorkItemMapping,
|
||||
methods: dict[str, str | None],
|
||||
bench_cut_depth_m: float | None = None,
|
||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
"""토공집계표 줄을 내역 줄로 옮긴다.
|
||||
|
||||
@@ -117,7 +124,12 @@ def _earthwork_rows(
|
||||
amount = float(row.get("amount") or 0.0)
|
||||
mismatch = _basis_mismatch(entry, unit) if code else None
|
||||
spec_detail = ""
|
||||
if mismatch is not None:
|
||||
# 층따기 — 길이가 들어오면 **면적 × 길이**로 ㎥ 를 내 품셈 단가를 그대로 쓴다.
|
||||
depth = float(bench_cut_depth_m or 0.0)
|
||||
if group in AREA_TIMES_DEPTH_GROUPS and unit == "㎡" and depth > 0:
|
||||
spec_detail = f"면적 {amount:,.2f}㎡ × 길이 {depth:g}m"
|
||||
unit, amount, mismatch = "㎥", amount * depth, None
|
||||
elif mismatch is not None:
|
||||
spec_detail = f"집계 {amount:,.2f} {unit} (품셈 밑수 {mismatch[0]})"
|
||||
unit, amount = mismatch[0], 0.0
|
||||
if code is None and not is_subtotal:
|
||||
|
||||
@@ -217,11 +217,13 @@ class QuantitySettingsBody(BaseModel):
|
||||
# ⚠ 산식(연장÷500)으로 만들지 않는다 — 임도규정이 「필요시 거리를 조정」이라 하고
|
||||
# 기점 포함·갈림길 중복을 원문이 정하지 않는다. **설계자가 넣는 값**이다.
|
||||
ancillary_counts: dict[str, float] | None = None
|
||||
# 층따기 길이(깊이, m). 면적 × 이 값 = ㎥ (확정 2차 ①).
|
||||
bench_cut_depth_m: float | None = None
|
||||
|
||||
|
||||
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
||||
#: 빈 문자열로 되돌리는 칸(시공법·타설 방식)과 달리 숫자 칸은 되돌릴 값이 `None` 뿐이다.
|
||||
NULLABLE_SETTING_KEYS = ("topsoil_thickness_m",)
|
||||
NULLABLE_SETTING_KEYS = ("topsoil_thickness_m", "bench_cut_depth_m")
|
||||
|
||||
|
||||
@router.put("/{project_id}/quantity/settings")
|
||||
|
||||
@@ -203,6 +203,8 @@ async def get_handoff(project_id: UUID) -> JSONResponse:
|
||||
ground_methods={name: rock_method(settings, name) for name in rock_classes(settings)},
|
||||
# 타설 방식 — 안 정했으면 기본값으로 서되 그 사실을 `placing_notes` 가 알린다.
|
||||
concrete_placing_method=concrete_placing_method(settings)[0],
|
||||
# 층따기 길이 — 면적 × 이 값으로 ㎥ 를 낸다(확정 2차 ①). 안 넣었으면 막히고 사유가 감.
|
||||
bench_cut_depth_m=settings.get("bench_cut_depth_m"),
|
||||
)
|
||||
handoff["summary"] = summarize(handoff)
|
||||
handoff["skipped_structures"] = skipped
|
||||
|
||||
@@ -109,6 +109,12 @@ def default_settings() -> dict[str, Any]:
|
||||
# ⇒ `ceil(연장÷500)` 을 확정 산식으로 쓰지 않고 **개소를 받는다.**
|
||||
# 비워 두면 물량을 안 낸다(0 으로 때우지 않음).
|
||||
"ancillary_counts": {},
|
||||
# 층따기 길이(깊이, m) — ⭐ 2026-09-09 사용자 확정 2차 ①.
|
||||
# ⚠ **면적이 정본**이고 부피는 **면적 × 이 길이**로 낸다. 그러면 품셈 ㎥ 단가를
|
||||
# 그대로 쓸 수 있다(단위 불일치가 풀림).
|
||||
# ⚠ 기본값을 두지 않는다 — 안 넣으면 물량을 안 낸다(0 으로 때우지 않음).
|
||||
# 교본이 「층따기 높이·폭은 **설계도서에 명시**」라 해 설계 입력이다.
|
||||
"bench_cut_depth_m": None,
|
||||
"dataset_versions": {},
|
||||
},
|
||||
"estimation": {
|
||||
|
||||
@@ -75,9 +75,10 @@
|
||||
"group": "층따기",
|
||||
"work_item_code": "FP-09-18",
|
||||
"master_name": "층따기",
|
||||
"basis_unit": "㎡",
|
||||
"basis_source": "⭐ 2026-09-09 사용자 확정 ⑦ — 층따기 수량 단위는 **㎡**(실무 관행). 밑수는 **원지반 표면의 경사길이**(B06 `design.bench_cut_length_m`)를 평균단면적법으로 면적화한 값이다. ⚠ 품셈 9-18 [주]는 `Q1 = 3600×q×K×f×E/㎝ = ㎥/시간` 이라 **공식은 체적 기준**이다 — 단의 높이·폭이 설계도서 값이라 체적을 지어낼 수 없어 면적으로 간다. ⇒ **받는 쪽(B09)이 ㎡ 단가를 세워야 한다**(㎥ 단가를 그대로 곱하면 금액이 틀림).",
|
||||
"unit_note": "앞서 2026-09-08 에는 이 어긋남을 「막힘」으로 두어 금액이 안 섰다(410만원 자리). 사용자 확정으로 ㎡ 가 정본이 되었으므로 막지 않는다."
|
||||
"basis_unit": "㎥",
|
||||
"basis_source": "품셈 9-18 [주] 「Q1 = 3600×q×K×f×E/㎝ = ㎥/시간」 — 공식이 체적 기준이다. ⭐ 2026-09-09 사용자 확정 2차 ① 로 **면적(㎡)을 정본으로 두고 사용자가 넣은 길이를 곱해 ㎥ 를 낸다** — 그러면 품셈 ㎥ 단가를 그대로 쓸 수 있다(단위 불일치가 풀림).",
|
||||
"mismatch_reason": "층따기 길이(깊이)가 아직 입력되지 않았습니다 — 산출 조건에서 넣으면 면적 × 길이로 물량이 섭니다(면적은 이미 섰습니다).",
|
||||
"mismatch_kind": "input_missing"
|
||||
},
|
||||
{
|
||||
"group": "면고르기",
|
||||
|
||||
Reference in New Issue
Block a user