- 환경친화적인 도로건설 지침 부록1 확인: 뿌리분 체적 V = 0.3927D³ × 1,300㎏/㎥ — 고르개로 넣음(기본은 분배비 15/85) - 뿌리분 직경 D 는 지침 표기 「뿌리분의 직경(DBH, m)」이 모호해 흉고직경과 따로 받음 - 지상부 식을 지침 원문대로 π 3.14 로 셈 — 실정보고 예 130.40㎥ 와 바로 맞음 - 분리발주 칸 밑·사유에 「임목폐기물은 분리발주 대상 아님(환경부 회신)」 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
393 lines
22 KiB
Python
393 lines
22 KiB
Python
"""프로젝트 설정 — 수량(B08)·원가(B09) 두 페이지가 함께 읽는 값 (PLAN 8-7).
|
||
|
||
자리
|
||
`<project_root>/project_settings.json` — 루트, `project_manifest.json` 옆.
|
||
매니페스트의 `stages` 는 **단계 산출물** 목록이고, 설정은 산출물이 아니라 **프로젝트 값**이다.
|
||
단계 폴더에 넣으면 주인이 애매해진다.
|
||
|
||
구획 — 페이지마다 자기 것만 쓴다
|
||
`quantity` = B08 · `estimation` = B09. 남의 구획은 **읽기만** 한다.
|
||
`dataset_versions` 도 구획마다 따로 둔다 — 한 칸을 둘이 쓰면 저장할 때마다 서로 지운다.
|
||
|
||
⚠ **경계를 코드로 막는다** — `save_section()` 은 이름 붙은 한 구획만 갈아 끼우고 나머지는
|
||
원본 그대로 둔다. 통째로 덮는 길을 두지 않는 까닭은, 두 페이지가 같은 파일을 쓰기 때문이다
|
||
(오늘 `main.py` 에서 같은 모양의 사고를 이미 겪었다).
|
||
|
||
⚠ `dataset_versions` 는 **기록**이지 정본이 아니다
|
||
여기 적히는 것은 「저장 시점에 무엇을 고른 상태였나」이고, 계산을 되살릴 때 쓰는 정본은
|
||
**프로젝트 스냅샷**이다. 둘이 어긋나면 **스냅샷이 이긴다.**
|
||
|
||
⚠ `*_override` 는 기본이 `None` 이다
|
||
「프로젝트가 안 정했으면 `config` 정본을 쓴다」는 뜻이다. 기본값을 복사해 넣으면 나중에
|
||
정본이 바뀌어도 옛 프로젝트가 안 따라온다. 값을 넣는 것은 **설계자가 일부러 바꿨을 때만**이다.
|
||
|
||
⚠ 반영률 기본은 100 이다 (PLAN 8-11 · 8-10 ★법대로)
|
||
실무 관측 80/50/80 은 설계자가 비고란에 손으로 적은 값이지 법정값이 아니다. 기본값으로
|
||
넣지 않는다.
|
||
|
||
작업본 3층 (CLAUDE.md 5장)
|
||
조작은 캐시(sessionStorage)에 쌓이고 [저장]·[확정]에서 이 파일로 간다. 자동저장은 만들지 않는다.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any, Iterable
|
||
|
||
from common_util.common_util_json import atomic_write_json
|
||
from config.config_system_design import (
|
||
EARTHWORK_CONVERSION_C_RANGES,
|
||
EARTHWORK_CONVERSION_FACTORS,
|
||
EARTHWORK_HAUL_EQUIPMENT_LIMITS_M,
|
||
EARTHWORK_HAUL_LIMIT_BASIS,
|
||
)
|
||
|
||
SETTINGS_FILENAME = "project_settings.json"
|
||
SCHEMA_VERSION = 1
|
||
|
||
# 반영률 키 — 사면 계열과 짝이다. 값은 퍼센트이고 기본은 전부 100.
|
||
APPLICATION_RATIO_KEYS = (
|
||
"fill_slope_compaction", # 성토면다짐
|
||
"seed_spray_fill", # 초류종자살포(성토면)
|
||
"seed_spray_cut", # 초류종자살포(절토면)
|
||
"obstacle_removal", # 지장목제거
|
||
)
|
||
|
||
# 암 갈래 세트 — **개수를 코드에 박지 않는다**(PLAN 8-13).
|
||
# 울진 2 · 거창 5 · 오솔길 BOM 1 로 공사마다 다르다. 프로젝트가 하나를 고른다.
|
||
ROCK_CLASS_SETS: dict[str, tuple[str, ...]] = {
|
||
"single": ("토사", "암"),
|
||
"uljin2": ("토사", "연암", "발파암"),
|
||
"geochang5": ("토사", "풍화암", "연암", "보통암", "경암"),
|
||
}
|
||
DEFAULT_ROCK_CLASS_SET = "geochang5"
|
||
|
||
# 암 시공법 — 품셈이 공종을 가르는 기준. `None` 은 「아직 안 정함」이고 기본값이다.
|
||
ROCK_METHOD_RIPPING = "ripping" # 긁어내기 — 암절취
|
||
ROCK_METHOD_BLASTING = "blasting" # 터뜨리기 — 발파암
|
||
ROCK_METHODS = (ROCK_METHOD_RIPPING, ROCK_METHOD_BLASTING)
|
||
|
||
|
||
def rock_method(settings: dict[str, Any], rock_class: str) -> str | None:
|
||
"""갈래 하나의 시공법. 안 정했으면 `None` — **기본값으로 때우지 않는다.**"""
|
||
value = (settings.get("rock_methods") or {}).get(rock_class)
|
||
return value if value in ROCK_METHODS else None
|
||
|
||
|
||
def default_settings() -> dict[str, Any]:
|
||
"""빈 설정. `estimation` 은 **자리만** 만든다 — 채우는 것은 B09 몫이다."""
|
||
return {
|
||
"schema_version": SCHEMA_VERSION,
|
||
"quantity": {
|
||
"rock_class_set": DEFAULT_ROCK_CLASS_SET,
|
||
"rock_classes": list(ROCK_CLASS_SETS[DEFAULT_ROCK_CLASS_SET]),
|
||
# 갈래별 비율(%). 설계자가 넣는 값이라 기본은 비워 둔다 —
|
||
# 측점별 암질 판정에 기대지 않는다는 것이 8-1 사용자 확정이다.
|
||
"rock_ratios_pct": {},
|
||
# 갈래별 **시공법** — `{갈래이름: "ripping"|"blasting"}`.
|
||
# ⚠ 갈래 이름(연암·보통암…)만으로는 **긁어내는 암인지 터뜨리는 암인지** 알 수 없고,
|
||
# 품셈은 그 둘을 다른 공종으로 둔다(암절취 FP-09-04 / 발파암 FP-09-05).
|
||
# 기본은 **비워 둔다** — 찍으면 공종이 조용히 틀린다. 안 정하면 인계에서
|
||
# 「시공법 미지정」으로 드러난다(2026-09-07 일감 9 에서 드러난 자리).
|
||
"rock_methods": {},
|
||
"conversion_factors_override": None,
|
||
# 도쟈 한계거리(m) — `None` 은 기본값(config 60 m, 근거 셋 일치)이 선다.
|
||
# 설계 조건이라 바꿀 수 있다(2026-09-13 판정). 종무대 20 m 는 규정이라 칸이 없다.
|
||
# ⚠ 종전 `haul_limits_m_override` 는 아무도 안 읽던 칸이라 이것으로 갈음했다.
|
||
"dozer_haul_limit_m": None,
|
||
"application_ratios_pct": {key: 100 for key in APPLICATION_RATIO_KEYS},
|
||
# 자재총괄의 관급/사급 구분 — `{자재명: "owner_supplied"|"contractor_supplied"}`
|
||
# 또는 `{자재명: {"supply": …, "install_by": "contractor"|"owner"}}`.
|
||
# ⚠ **법이 아니라 발주 결정**이라 기본은 비워 둔다. 안 정한 자재는 「미분류」로
|
||
# 화면에 드러난다 — 사급으로 조용히 넘기면 관급자재대가 새 나간다.
|
||
# 표토제거 두께(m) — ⚠ **품셈이 정하는 값이 아니다.** 9-15 [주]② 가
|
||
# 「T : 표토두께(m)」로 **공식의 입력 변수**로 두었다(2026-09-07 원문 확인).
|
||
# 기본값을 두지 않는다 — 안 넣으면 물량을 안 낸다(0 으로 때우지 않음).
|
||
"topsoil_thickness_m": None,
|
||
"material_supply": {},
|
||
# 콘크리트 타설 방식 — `ready_mixed`(FP-12-01-01) / `machine_mixed`(-02) /
|
||
# `hand_mixed`(-03). **설계 판단**이라 사용자가 고른다.
|
||
# ⚠ 기본은 `None` — 「안 정함」과 「일부러 레디믹스트를 고른 것」을 갈라야
|
||
# 화면이 「기본값 적용 중」을 정직하게 띄운다. 값을 미리 넣으면 그 구별이 사라진다.
|
||
"concrete_placing_method": None,
|
||
# 부대시설 개소 — `{항목키: 개소}` (2026-09-09 사용자 확정 ⑬).
|
||
# ⚠ **산식으로 만들지 않는다.** 국가지점번호판은 임도규정 제26조제5항이
|
||
# 「500미터 마다 설치·관리하되 **필요시 거리를 조정**」이라 하고, 기점 포함·
|
||
# 종점 잔여·갈림길 중복을 원문이 안 정한다(지식DB 부대시설 [구현]).
|
||
# ⇒ `ceil(연장÷500)` 을 확정 산식으로 쓰지 않고 **개소를 받는다.**
|
||
# 비워 두면 물량을 안 낸다(0 으로 때우지 않음).
|
||
"ancillary_counts": {},
|
||
# 층따기 길이(깊이, m) — ⭐ 2026-09-09 사용자 확정 2차 ①.
|
||
# ⚠ **면적이 정본**이고 부피는 **면적 × 이 길이**로 낸다. 그러면 품셈 ㎥ 단가를
|
||
# 그대로 쓸 수 있다(단위 불일치가 풀림).
|
||
# ⚠ 기본값을 두지 않는다 — 안 넣으면 물량을 안 낸다(0 으로 때우지 않음).
|
||
# 교본이 「층따기 높이·폭은 **설계도서에 명시**」라 해 설계 입력이다.
|
||
"bench_cut_depth_m": None,
|
||
# 사토장까지 운반거리(m) — 설계 입력이다. 품셈은 거리를 정하지 않는다.
|
||
# ⚠ `None` 은 「안 정함」이고, 그러면 사토 운반 줄이 **막힌 채로** 선다.
|
||
# (2026-09-09 확정 3차 ③ — 권고할 값이 없어 **물음으로 남긴 자리**다.)
|
||
"spoil_site_distance_m": None,
|
||
# 기초잡석 두께(m) — 2026-09-09 사용자 확정 3차 ② 「0.2 m」.
|
||
# ⚠ 품셈 12-25 는 ㎥당 품만 주고 두께를 정하지 않는다. 폭은 버림 폭과 같고
|
||
# (KCS 34 50 05) 두께가 이 값이다. 화면에서 바꿀 수 있다.
|
||
"rubble_base_thickness_m": 0.2,
|
||
# 구조물터파기 용수 유무 — 품셈 9-13 의 18구분 중 한 축.
|
||
# ⚠ **사용자 확정이 아니라 통상값**이다(확정 3차 ④). 화면·사유에 그 사실을
|
||
# 적어 두고 사용자가 뒤집을 수 있게 둔다.
|
||
"structure_trench_water": "육상",
|
||
# 표토 운반거리(m) — ⚠ 별표2 Ⅰ.2.차.(6) 「표토는 전량 제거한 후 … 최고 홍수위보다
|
||
# 높은 장소로 **운반하고 쌓아두어야** 한다」. 제거만 세면 법이 요구하는 운반이 빠진다.
|
||
# 거리는 현장값이라 품셈이 정하지 않는다 — 비면 운반 줄이 막힌 채로 선다.
|
||
"topsoil_haul_distance_m": None,
|
||
# 표토제거 대상 — `None` 은 기본 「노면 + 절토」(별표2 문언) · `"road_only"` 는 노면만.
|
||
# ⚠ 노면만은 실무 영월 한 건(1m당 3.34㎡) 관측이라 일반화하지 않고 설계자가 고름
|
||
# (2026-09-14 브레인 판정 — 3배 차이라 칸으로 둠).
|
||
"topsoil_target": None,
|
||
# 임목축적 등급 — 품셈 9-21 제근이 **소·중·밀** 셋으로 갈리는 축.
|
||
# [주]① 「소림 30~60㎥/㏊ · 중림 60~90 · 밀림 90 이상」 — 본수가 아니라 **축적**이다.
|
||
# ⚠ 산림조사부·영림계획에서 옮겨 적는 값이라 프로그램이 만들 수 없다. 비면 막는다.
|
||
"stand_volume_class": None,
|
||
# 규준틀 개소당 재료 — `{자재명: 개소당 수량}`. **비우면 제안값(실무 관측)이 선다.**
|
||
# 품셈 11-2·11-3 [주]④ 가 「재료량은 설계수량에 따른다」라 값을 안 주는 자리이고,
|
||
# 세는 것 자체는 확정이라 **제안값 + 고칠 수 있게**로 둔다(확정 ⑨·⑩ 과 같은 틀).
|
||
"frame_material": {},
|
||
# 임목파쇄 — **기본 꺼짐**(확정 5차 5번). 현장에 따라 켤 수 있는 칸.
|
||
# ⚠ 근주이식은 칸도 두지 않는다 — 「셀지 말지가 설계 판단」이라 켤 자리가 없으면
|
||
# 물을 일도 없다. 켜도 **부피는 지어내지 않는다**(아래 칸이 비면 줄만 서고 사유).
|
||
"wood_chipping_enabled": False,
|
||
"wood_chipping_volume_m3": None,
|
||
# 노체다짐(9-16-2) — **기본 꺼짐**(2026-09-13 판정). [주]⑤ 「대규모 성토지로서
|
||
# 층다짐이 필요한 경우 적용」 조건부라 늘 서지 않는다. 켜면 토공집계에 별도 줄.
|
||
"subgrade_compaction_enabled": False,
|
||
# 임목폐기물 — 현장 조사값 넷 `{stems_per_1000m2, dbh_cm, height_m, unit_weight_kg_m3}`.
|
||
# ⚠ 기본값을 두지 않는다 — 비면 톤이 안 선다(수종·임분마다 달라 지어내지 않음).
|
||
"tree_waste": {},
|
||
# 뿌리 산정법 — `None` 은 분배비 15/85(기본) · `"root_ball"` 은 뿌리분 체적 × 1,300㎏/㎥
|
||
# (환경친화적인 도로건설 지침 부록1). 뿌리분 직경은 `tree_waste.root_ball_diameter_m`.
|
||
"tree_waste_root_method": None,
|
||
# 처리단가(원/톤) — KCRA 중간처리단가 미확보라 **수동**. 넣으면 금액이 서되 미확정.
|
||
"tree_waste_unit_price_krw_per_ton": None,
|
||
# 폐기물 분리발주 — 기본 아님(경비로 원가에 듦 · 2026-09-14 판정).
|
||
"waste_separate_order": False,
|
||
"dataset_versions": {},
|
||
},
|
||
"estimation": {
|
||
# ⚠ 「연도」가 아니라 **판**을 가리킨다 — 조달청 제비율은 연중에도 개정된다
|
||
# (현행판 2026-04-13). 「2026년」만으로는 어느 판인지 안 정해진다.
|
||
# 값은 `dataset_id` + `effective_date` + `sha256` 세 쪽.
|
||
"rate_dataset": None,
|
||
"price_slot_names": {},
|
||
"dataset_versions": {},
|
||
},
|
||
}
|
||
|
||
|
||
def settings_path(project_root: str | Path) -> Path:
|
||
return Path(project_root) / SETTINGS_FILENAME
|
||
|
||
|
||
def load_settings(project_root: str | Path) -> dict[str, Any]:
|
||
"""설정을 읽는다. 파일이 없거나 깨졌으면 기본값을 돌려준다(예외를 올리지 않는다).
|
||
|
||
읽기가 실패해도 화면은 서야 한다 — 설정은 계산을 **거드는** 값이지 없으면 못 도는 값이 아니다.
|
||
"""
|
||
path = settings_path(project_root)
|
||
if not path.exists():
|
||
return default_settings()
|
||
try:
|
||
stored = json.loads(path.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
return default_settings()
|
||
if not isinstance(stored, dict):
|
||
return default_settings()
|
||
return _merge(default_settings(), stored)
|
||
|
||
|
||
def _merge(base: dict[str, Any], stored: dict[str, Any]) -> dict[str, Any]:
|
||
"""저장분을 기본값 위에 얹는다. **새로 생긴 키가 빠지지 않게** 한 겹만 재귀한다."""
|
||
merged = dict(base)
|
||
for key, value in stored.items():
|
||
current = merged.get(key)
|
||
if isinstance(current, dict) and isinstance(value, dict):
|
||
merged[key] = _merge(current, value)
|
||
else:
|
||
merged[key] = value
|
||
return merged
|
||
|
||
|
||
SECTIONS = ("quantity", "estimation")
|
||
|
||
|
||
def save_section(
|
||
project_root: str | Path,
|
||
section: str,
|
||
values: dict[str, Any],
|
||
*,
|
||
replace_keys: Iterable[str] = (),
|
||
) -> dict[str, Any]:
|
||
"""한 구획만 갈아 끼운다 — 남의 구획은 **손대지 않는다**.
|
||
|
||
두 페이지가 같은 파일을 쓰므로 통째로 덮으면 상대 값이 사라진다. 그래서 **통째로 쓰는
|
||
함수를 두지 않는다** — 쓰려면 반드시 구획 이름을 대야 한다.
|
||
|
||
⚠ `replace_keys` — **지울 수 있어야 하는 칸**은 병합이 아니라 통째로 갈아 끼운다.
|
||
「고른 값을 안 정함으로 되돌리기」가 병합으로는 안 되기 때문이다(2026-09-07 화면에서
|
||
걸린 자리 — 시공법을 한 번 고르면 되돌릴 길이 없었다).
|
||
"""
|
||
if section not in SECTIONS:
|
||
raise ValueError(f"모르는 구획: {section} (쓸 수 있는 것: {', '.join(SECTIONS)})")
|
||
settings = load_settings(project_root)
|
||
merged = _merge(settings.get(section) or {}, values)
|
||
for key in replace_keys:
|
||
if key in values:
|
||
merged[key] = values[key]
|
||
settings[section] = merged
|
||
settings["schema_version"] = SCHEMA_VERSION
|
||
atomic_write_json(settings_path(project_root), settings)
|
||
return settings
|
||
|
||
|
||
def quantity_settings(project_root: str | Path) -> dict[str, Any]:
|
||
"""B08 구획만 꺼낸다."""
|
||
return load_settings(project_root).get("quantity") or {}
|
||
|
||
|
||
def estimation_settings(project_root: str | Path) -> dict[str, Any]:
|
||
"""B09 구획만 꺼낸다 — 범위 계수·장비 규격처럼 **사용자가 고른 단가 조건**이 여기 산다."""
|
||
return load_settings(project_root).get("estimation") or {}
|
||
|
||
|
||
def rock_classes(settings: dict[str, Any]) -> list[str]:
|
||
"""이 프로젝트의 암 갈래 목록. 세트 이름이 낯설면 저장된 목록을 그대로 쓴다."""
|
||
stored = settings.get("rock_classes")
|
||
if isinstance(stored, list) and stored:
|
||
return [str(item) for item in stored]
|
||
name = str(settings.get("rock_class_set") or DEFAULT_ROCK_CLASS_SET)
|
||
return list(ROCK_CLASS_SETS.get(name, ROCK_CLASS_SETS[DEFAULT_ROCK_CLASS_SET]))
|
||
|
||
|
||
#: 타설 방식 — 안 정했을 때 쓰는 값. **금액에 바로 걸리는 자리**라 화면이 잠정임을 띄운다.
|
||
CONCRETE_PLACING_METHODS = ("ready_mixed", "machine_mixed", "hand_mixed")
|
||
DEFAULT_CONCRETE_PLACING_METHOD = "ready_mixed"
|
||
|
||
|
||
def concrete_placing_method(settings: dict[str, Any]) -> tuple[str, bool]:
|
||
"""(타설 방식, 기본값을 쓰고 있는가).
|
||
|
||
⚠ 둘째 값이 참이면 **사용자가 아직 안 정한 것**이다 — 화면이 「기본값 적용 중」을 띄워야
|
||
한다. 조용히 기본으로 돌면 사용자는 그것이 잠정인 줄도 모른다.
|
||
"""
|
||
stored = settings.get("concrete_placing_method")
|
||
if stored in CONCRETE_PLACING_METHODS:
|
||
return str(stored), False
|
||
return DEFAULT_CONCRETE_PLACING_METHOD, True
|
||
|
||
|
||
#: 표토제거 대상 — 기본은 법 문언(별표2 Ⅰ.2.차.(6) 「노면·절토대상지」).
|
||
TOPSOIL_TARGETS = ("road_and_cut", "road_only")
|
||
DEFAULT_TOPSOIL_TARGET = "road_and_cut"
|
||
|
||
|
||
def topsoil_target(settings: dict[str, Any]) -> str:
|
||
"""표토제거 대상. 모르는 값·안 정함은 기본(노면 + 절토)."""
|
||
stored = settings.get("topsoil_target")
|
||
return str(stored) if stored in TOPSOIL_TARGETS else DEFAULT_TOPSOIL_TARGET
|
||
|
||
|
||
def earthwork_conversion_factors(settings: dict[str, Any]) -> dict[str, dict[str, float]]:
|
||
"""이 프로젝트가 쓸 토량환산계수 — **기본값 위에 고른 값만 얹는다.**
|
||
|
||
⚠ 정의처는 여전히 `config_system_design.EARTHWORK_CONVERSION_FACTORS` 한 곳이다.
|
||
여기서 값을 새로 적지 않고, 설계자가 고른 갈래만 갈아 끼운다. 안 고른 갈래는
|
||
키 자체가 없어 정본이 그대로 선다 — 기본값을 복사해 넣지 않는 까닭은 이 파일
|
||
머리글 `*_override` 규칙과 같다.
|
||
|
||
⚠ 이 값은 토적표만 쓰는 것이 아니다 — 유토곡선(B06)·운반표·기초단가가 같이 읽는다.
|
||
그래서 읽는 자리마다 상수를 직접 들지 말고 **이 함수를 거친다.**
|
||
|
||
고른 값의 모양 — `conversion_factors_override`
|
||
`{"ripping_rock": {"compacted": 1.0, "reason": "토질시험 값"}}`
|
||
`reason` 은 품셈 범위 밖을 골랐을 때 남기는 사유이고 계산에 안 쓴다.
|
||
"""
|
||
resolved = {kind: dict(entry) for kind, entry in EARTHWORK_CONVERSION_FACTORS.items()}
|
||
override = settings.get("conversion_factors_override")
|
||
if not isinstance(override, dict):
|
||
return resolved
|
||
for kind, entry in override.items():
|
||
if kind not in resolved or not isinstance(entry, dict):
|
||
continue
|
||
value = entry.get("compacted")
|
||
if isinstance(value, (int, float)) and not isinstance(value, bool) and float(value) > 0:
|
||
resolved[kind]["compacted"] = float(value)
|
||
return resolved
|
||
|
||
|
||
def earthwork_conversion_choices(settings: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||
"""갈래별 「무엇을 골랐나」 — 화면이 기본값과 고른 값을 갈라 보이는 데 쓴다.
|
||
|
||
`{갈래: {"compacted", "default", "chosen", "in_range", "range", "reason"}}`.
|
||
`chosen` 이 거짓이면 기본값이 선 것이고, `in_range` 가 거짓이면 품셈 범위 밖이라
|
||
사유가 있어야 하는 자리다. **범위 밖이라고 막지 않는다**(품셈 원칙이 토질시험이다).
|
||
"""
|
||
override = settings.get("conversion_factors_override")
|
||
override = override if isinstance(override, dict) else {}
|
||
resolved = earthwork_conversion_factors(settings)
|
||
choices: dict[str, dict[str, Any]] = {}
|
||
for kind, entry in resolved.items():
|
||
default = float(EARTHWORK_CONVERSION_FACTORS[kind]["compacted"])
|
||
value = float(entry["compacted"])
|
||
low, high = EARTHWORK_CONVERSION_C_RANGES.get(kind, (None, None))
|
||
entry_override = override.get(kind)
|
||
reason = entry_override.get("reason") if isinstance(entry_override, dict) else None
|
||
choices[kind] = {
|
||
"compacted": value,
|
||
"default": default,
|
||
"chosen": value != default,
|
||
"in_range": low is None or low <= value <= high,
|
||
"range": [low, high] if low is not None else None,
|
||
"reason": str(reason) if reason else None,
|
||
}
|
||
return choices
|
||
|
||
|
||
def haul_equipment_limits(settings: dict[str, Any]) -> list[tuple[str, float | None]]:
|
||
"""이 프로젝트가 쓸 운반장비 거리 경계 — 기본값 위에 **도쟈 한계거리만** 얹는다.
|
||
|
||
⚠ 유토곡선(B06)·운반표가 같은 값으로 서야 한다 — 읽는 자리마다 상수를 들지 말고
|
||
이 함수를 거친다.
|
||
⚠ 종무대 경계 이하 값은 받지 않는다(도쟈 몫이 사라짐) — 기본값이 선다.
|
||
"""
|
||
limits = dict(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M)
|
||
value = settings.get("dozer_haul_limit_m")
|
||
if (
|
||
isinstance(value, (int, float))
|
||
and not isinstance(value, bool)
|
||
and float(value) > float(limits["free_haul"] or 0.0)
|
||
):
|
||
limits["dozer"] = float(value)
|
||
return [(key, limits[key]) for key, _ in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M]
|
||
|
||
|
||
def haul_limit_choice(settings: dict[str, Any]) -> dict[str, Any]:
|
||
"""도쟈 한계거리 칸 — 화면이 지금 값·기본값·근거를 함께 보인다."""
|
||
default = dict(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M)["dozer"]
|
||
value = dict(haul_equipment_limits(settings))["dozer"]
|
||
return {
|
||
"value": value,
|
||
"default": default,
|
||
"chosen": value != default,
|
||
"basis": EARTHWORK_HAUL_LIMIT_BASIS["dozer"],
|
||
"free_haul_m": dict(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M)["free_haul"],
|
||
"free_haul_basis": EARTHWORK_HAUL_LIMIT_BASIS["free_haul"],
|
||
}
|
||
|
||
|
||
def application_ratio(settings: dict[str, Any], key: str) -> float:
|
||
"""반영률을 0~1 로. 없으면 100 %(=1.0) — 실무 관측치를 기본값으로 쓰지 않는다."""
|
||
raw = (settings.get("application_ratios_pct") or {}).get(key, 100)
|
||
try:
|
||
return float(raw) / 100.0
|
||
except (TypeError, ValueError):
|
||
return 1.0
|