- 자재 = 다짐 후 부피 ÷ C × L · 두께 제안 0.10(교본) · C 0.85 · L 1.25 제안(소광 관측 · 원문 표에 혼합석 줄 없음 · 역 C 와 방향 반대 사유) - 종단 경사는 계획선에서 B05 포장 제안과 같은 한 벌(local_grade_pct 로 뺌 — 종단 파일 측점 경사엔 비정규 측점이 없었음) - 해당 측점이 없으면 0 원 줄 안 세움 · 다짐 줄은 공종 없어 사유 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
186 lines
11 KiB
Python
186 lines
11 KiB
Python
"""B08 산출 조건 [저장] — 받는 모양과 값 거르기 (2026-09-15 `Router_Earthwork` 700줄에서 가름).
|
||
|
||
⚠ 순수 분리 — 칸·거르기 규칙은 그대로 옮김. 저장 자리(`save_quantity_settings`)는 라우터에 남음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from pydantic import BaseModel
|
||
|
||
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import (
|
||
FACE_DRESSING_CUT_CLASSES,
|
||
FACE_DRESSING_FILL_CLASSES,
|
||
ROOT_REMOVAL_EXCAVATOR_SIZES,
|
||
SEED_SPRAY_GROUNDS,
|
||
)
|
||
from common_util.common_util_project_settings import CONCRETE_PLACING_METHODS, ROCK_METHODS
|
||
from config.config_system_design import (
|
||
EARTHWORK_CONVERSION_FACTORS,
|
||
EARTHWORK_HAUL_EQUIPMENT_LIMITS_M,
|
||
)
|
||
|
||
|
||
class QuantitySettingsBody(BaseModel):
|
||
"""[저장]이 보내는 산출 조건. 보내지 않은 칸은 저장분을 그대로 둔다."""
|
||
|
||
rock_class_set: str | None = None
|
||
rock_classes: list[str] | None = None
|
||
rock_ratios_pct: dict[str, float] | None = None
|
||
# 갈래별 시공법 — 값은 "ripping"·"blasting". 안 정한 갈래는 보내지 않는다.
|
||
rock_methods: dict[str, str] | None = None
|
||
application_ratios_pct: dict[str, float] | None = None
|
||
# 자재별 관급/사급 — `{자재명: {"supply": …, "install_by": …}}`.
|
||
# 표 안에서 줄마다 고른 값이 여기로 온다(2026-09-07 확정).
|
||
material_supply: dict[str, Any] | None = None
|
||
# 자재별 할증률 — `{이름 규격: %}`. 품셈 1-3-1 「표의 값 이내」라 0~표값만 씀(엔진이 거름).
|
||
material_surcharge: dict[str, Any] | None = None
|
||
# 콘크리트 타설 방식. `""` 는 「안 정함」으로 되돌리는 뜻이라 서버가 None 으로 만든다.
|
||
concrete_placing_method: str | None = None
|
||
# 표토제거 두께(m). 품셈이 정하는 값이 아니라 설계 입력이다(9-15 [주]②).
|
||
topsoil_thickness_m: float | None = None
|
||
# 부대시설 개소 — `{항목키: 개소}`(2026-09-09 확정 ⑬).
|
||
# ⚠ 산식(연장÷500)으로 만들지 않는다 — 임도규정이 「필요시 거리를 조정」이라 하고
|
||
# 기점 포함·갈림길 중복을 원문이 정하지 않는다. **설계자가 넣는 값**이다.
|
||
ancillary_counts: dict[str, float] | None = None
|
||
# 층따기 길이(깊이, m). 면적 × 이 값 = ㎥ (확정 2차 ①).
|
||
bench_cut_depth_m: float | None = None
|
||
# 사토장까지 운반거리(m). 유토곡선이 낸 사토를 **실어 내는 줄**이 이 값으로 선다.
|
||
spoil_site_distance_m: float | None = None
|
||
# 기초잡석 두께(m) — 확정 3차 ② 0.2. 폭은 버림 폭과 같다(KCS 34 50 05).
|
||
rubble_base_thickness_m: float | None = None
|
||
# 구조물터파기 용수 유무 — "육상"·"용수". ⚠ 기본 육상은 **통상값**이지 사용자 확정이 아니다.
|
||
structure_trench_water: str | None = None
|
||
# 표토 운반거리(m) — 별표2 가 요구하는 운반·적치의 밑수. 비면 그 줄이 막힌다.
|
||
topsoil_haul_distance_m: float | None = None
|
||
# 표토제거 대상 — "road_only" 는 노면만. `""` 는 기본(노면 + 절토, 별표2 문언)으로 되돌림.
|
||
topsoil_target: str | None = None
|
||
# 임목축적 등급 — "소림"·"중림"·"밀림"(품셈 9-21 [주]①). `""` 는 「안 정함」이다.
|
||
stand_volume_class: str | None = None
|
||
# 면고르기 갈래 — 절토면 토질 · 성토면 시공·토질(9-19-1 원문 표). `""` 는 「안 정함」.
|
||
face_dressing_cut_class: str | None = None
|
||
# 제근 굴착기 크기 — "0.2"·"0.7"(품셈 9-21 갈래). `""` 는 「안 정함」.
|
||
root_removal_excavator_m3: str | None = None
|
||
# 초류종자살포 비탈면 토질 — "일반"·"마사토"(품셈 5-24 잎). `""` 는 「안 정함」.
|
||
seed_spray_ground: str | None = None
|
||
face_dressing_fill_class: str | None = None
|
||
# 면고르기 면적 덮어쓰기(㎡) — `None` 은 파종 면적을 그대로(2026-09-14 판정 Ⓐ).
|
||
face_dressing_fill_area_m2: float | None = None
|
||
face_dressing_cut_area_m2: float | None = None
|
||
# 규준틀 개소당 재료 — `{자재명: 수량}`. 비우면 제안값(실무 관측)이 선다.
|
||
frame_material: dict[str, Any] | None = None
|
||
# 임목파쇄 — 기본 꺼짐(확정 5차 5번). 켜면 줄이 서고, 부피를 넣으면 값이 선다.
|
||
wood_chipping_enabled: bool | None = None
|
||
wood_chipping_volume_m3: float | None = None
|
||
# 노체다짐 — 기본 꺼짐(9-16-2 [주]⑤ 조건부). 켜면 토공집계에 별도 줄.
|
||
subgrade_compaction_enabled: bool | None = None
|
||
# 임목폐기물 — 현장 조사값 넷(통째로 갈아 끼움) · 수동 처리단가 · 분리발주.
|
||
tree_waste: dict[str, float | None] | None = None
|
||
tree_waste_unit_price_krw_per_ton: float | None = None
|
||
waste_separate_order: bool | None = None
|
||
# 뿌리 산정법 — "root_ball"(뿌리분 체적 × 1,300) · `""` 는 기본(분배비 15/85)으로 되돌림.
|
||
tree_waste_root_method: str | None = None
|
||
# 토량환산계수(다짐) — `{갈래: {"compacted": C, "reason": 사유}}`.
|
||
# ⚠ **기본값을 복사해 넣지 않는다** — 안 고른 갈래는 키가 없어야 정본이 선다.
|
||
# 빈 dict 는 「전부 기본값으로 되돌림」이라 통째로 갈아 끼운다.
|
||
conversion_factors_override: dict[str, Any] | None = None
|
||
# 도쟈 한계거리(m) — `None` 은 기본값(60 m). 종무대 20 m 보다 커야 한다(도쟈 몫이 사라짐).
|
||
dozer_haul_limit_m: float | None = None
|
||
# 혼합석 부설(2026-09-15 브레인 포장 둘 ⓑ) — 「비포장 전 구간」 고르개(교본 3-2) · 연약·습윤 구간
|
||
# `[{from_m, to_m}]`(법령 별표2 Ⅰ.2.바.(2) 8% 이하 조건 — 판정 근거가 없어 설계자 칸) ·
|
||
# 두께(다짐 후)·C·L 은 비면 제안값(교본 0.10 · 소광 0.85/1.25).
|
||
gravel_all_unpaved: bool | None = None
|
||
gravel_soft_wet_ranges: list[dict[str, Any]] | None = None
|
||
gravel_thickness_m: float | None = None
|
||
gravel_conversion_c: float | None = None
|
||
gravel_conversion_l: float | None = None
|
||
|
||
|
||
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
||
#: 빈 문자열로 되돌리는 칸(시공법·타설 방식)과 달리 숫자 칸은 되돌릴 값이 `None` 뿐이다.
|
||
NULLABLE_SETTING_KEYS = (
|
||
"topsoil_thickness_m",
|
||
"bench_cut_depth_m",
|
||
"spoil_site_distance_m",
|
||
"rubble_base_thickness_m",
|
||
"topsoil_haul_distance_m",
|
||
"wood_chipping_volume_m3",
|
||
"dozer_haul_limit_m",
|
||
"tree_waste_unit_price_krw_per_ton",
|
||
"face_dressing_fill_area_m2",
|
||
"face_dressing_cut_area_m2",
|
||
"gravel_thickness_m",
|
||
"gravel_conversion_c",
|
||
"gravel_conversion_l",
|
||
)
|
||
|
||
|
||
def clean_setting_values(values: dict[str, Any]) -> str | None:
|
||
"""받은 값을 **그 자리에서** 거른다 — 거절할 값이면 사람이 읽는 까닭, 아니면 `None`."""
|
||
dozer_limit = values.get("dozer_haul_limit_m")
|
||
free_haul = dict(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M)["free_haul"] or 0.0
|
||
if dozer_limit is not None and dozer_limit <= free_haul:
|
||
# 조용히 기본값으로 돌리지 않는다 — 넣은 값이 안 쓰이는 줄 모른다.
|
||
return f"도쟈 한계거리는 종무대 {free_haul:g} m 보다 커야 합니다."
|
||
for key, choices in (
|
||
("face_dressing_cut_class", FACE_DRESSING_CUT_CLASSES),
|
||
("face_dressing_fill_class", FACE_DRESSING_FILL_CLASSES),
|
||
("root_removal_excavator_m3", ROOT_REMOVAL_EXCAVATOR_SIZES),
|
||
("seed_spray_ground", SEED_SPRAY_GROUNDS),
|
||
):
|
||
if key in values and values[key] not in choices:
|
||
values[key] = "" # 선택지 밖·빈 값은 「안 정함」 — 가까운 갈래로 안 고침
|
||
if "concrete_placing_method" in values:
|
||
method = values["concrete_placing_method"]
|
||
# 「안 정함」으로 되돌릴 수 있어야 한다 — 빈 값이면 지운다(8-22 ② 와 같은 자리).
|
||
values["concrete_placing_method"] = method if method in CONCRETE_PLACING_METHODS else None
|
||
if "tree_waste" in values:
|
||
# 양수만 남긴다 — 빈 칸은 키가 없어야 「안 넣음」으로 읽힌다.
|
||
values["tree_waste"] = {
|
||
key: float(value)
|
||
for key, value in (values["tree_waste"] or {}).items()
|
||
if isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0
|
||
}
|
||
if "tree_waste_root_method" in values:
|
||
method = values["tree_waste_root_method"]
|
||
values["tree_waste_root_method"] = method if method == "root_ball" else None
|
||
if "topsoil_target" in values:
|
||
# 기본(노면 + 절토)은 저장하지 않는다 — 「안 정함」과 같게 둬 법 문언이 선다.
|
||
target = values["topsoil_target"]
|
||
values["topsoil_target"] = target if target == "road_only" else None
|
||
if "conversion_factors_override" in values:
|
||
# 아는 갈래·양수만 남긴다. 사유는 값이 있을 때만 따라간다(계산에는 안 쓴다).
|
||
cleaned: dict[str, Any] = {}
|
||
for kind, entry in (values["conversion_factors_override"] or {}).items():
|
||
if kind not in EARTHWORK_CONVERSION_FACTORS or not isinstance(entry, dict):
|
||
continue
|
||
value = entry.get("compacted")
|
||
if not isinstance(value, (int, float)) or isinstance(value, bool) or float(value) <= 0:
|
||
continue
|
||
kept: dict[str, Any] = {"compacted": float(value)}
|
||
reason = entry.get("reason")
|
||
if isinstance(reason, str) and reason.strip():
|
||
kept["reason"] = reason.strip()
|
||
cleaned[kind] = kept
|
||
values["conversion_factors_override"] = cleaned
|
||
if "rock_methods" in values:
|
||
# 「안 정함」(빈 값)은 저장하지 않는다 — 정한 것과 구별이 안 된다. 통째로 갈아 끼우므로
|
||
# 여기서 버리면 그 갈래는 미지정으로 돌아간다.
|
||
values["rock_methods"] = {
|
||
name: method
|
||
for name, method in values["rock_methods"].items()
|
||
if method in ROCK_METHODS
|
||
}
|
||
if "gravel_soft_wet_ranges" in values:
|
||
# 숫자 둘이 다 선 구간만 · 앞뒤를 바로잡음 — 빈 목록은 「구간 없음」으로 통째로 갈아 끼움.
|
||
values["gravel_soft_wet_ranges"] = [
|
||
{"from_m": min(a, b), "to_m": max(a, b)}
|
||
for entry in values["gravel_soft_wet_ranges"] or []
|
||
if isinstance(entry, dict)
|
||
for a, b in [(entry.get("from_m"), entry.get("to_m"))]
|
||
if all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in (a, b))
|
||
and a != b
|
||
]
|
||
return None
|