Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_desktop_1' into main_desktop_1
This commit is contained in:
@@ -73,13 +73,13 @@ from B06_Section.B06_Section_Schema import (
|
|||||||
SectionRegenerateRequest,
|
SectionRegenerateRequest,
|
||||||
SectionSummaryResponse,
|
SectionSummaryResponse,
|
||||||
)
|
)
|
||||||
|
from B06_Section.B06_Section_Server_Calc_Prebuild import conversion_factors_for
|
||||||
from common_util.common_util_auth import verify_session
|
from common_util.common_util_auth import verify_session
|
||||||
from common_util.common_util_storage import resolve_stored_project_path
|
from common_util.common_util_storage import resolve_stored_project_path
|
||||||
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||||
from common_util.common_util_workflow_state import get_workflow_state
|
from common_util.common_util_workflow_state import get_workflow_state
|
||||||
from config.config_db import get_db_pool, run_with_connection
|
from config.config_db import get_db_pool, run_with_connection
|
||||||
from config.config_system import (
|
from config.config_system import (
|
||||||
EARTHWORK_CONVERSION_FACTORS,
|
|
||||||
EARTHWORK_HAUL_EQUIPMENT_LIMITS_M,
|
EARTHWORK_HAUL_EQUIPMENT_LIMITS_M,
|
||||||
FOREST_ROAD_MIN_WIDTH_M,
|
FOREST_ROAD_MIN_WIDTH_M,
|
||||||
NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
||||||
@@ -144,7 +144,9 @@ async def get_section_context(project_id: UUID) -> SectionContextResponse | JSON
|
|||||||
stored_standard_cross_section=stored_standard,
|
stored_standard_cross_section=stored_standard,
|
||||||
rock_boundary_default_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
rock_boundary_default_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||||
rock_boundary_step_m=STANDARD_ROCK_BOUNDARY_STEP_M,
|
rock_boundary_step_m=STANDARD_ROCK_BOUNDARY_STEP_M,
|
||||||
earthwork_conversion=EARTHWORK_CONVERSION_FACTORS,
|
# ⚠ 상수를 직접 들지 않는다 — 프로젝트가 고른 계수가 있으면 화면도 그 값으로
|
||||||
|
# 그려야 서버가 뒤에 다시 셈한 값과 갈리지 않는다(CLAUDE.md 5장).
|
||||||
|
earthwork_conversion=await conversion_factors_for(project_id),
|
||||||
haul_equipment_limits=[
|
haul_equipment_limits=[
|
||||||
HaulEquipmentLimit(key=key, max_distance_m=limit)
|
HaulEquipmentLimit(key=key, max_distance_m=limit)
|
||||||
for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M
|
for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from fastapi.responses import JSONResponse
|
|||||||
from B06_Section.B06_Section_Server_Calc_Prebuild import (
|
from B06_Section.B06_Section_Server_Calc_Prebuild import (
|
||||||
BUNDLE,
|
BUNDLE,
|
||||||
_mass_haul_context,
|
_mass_haul_context,
|
||||||
|
conversion_factors_for,
|
||||||
haul_inputs_for,
|
haul_inputs_for,
|
||||||
)
|
)
|
||||||
from common_util.common_util_node_bundle import run_bundle_json
|
from common_util.common_util_node_bundle import run_bundle_json
|
||||||
@@ -63,6 +64,8 @@ async def compute_haul_plan(
|
|||||||
# 구조물 몫(공제·잔토)을 **넘겨야** 사토가 줄고 는다 — 인자 없이 부르면 늘 `None` 이라
|
# 구조물 몫(공제·잔토)을 **넘겨야** 사토가 줄고 는다 — 인자 없이 부르면 늘 `None` 이라
|
||||||
# 통로만 있고 값이 안 흐른다(2026-09-09 실측으로 드러난 자리).
|
# 통로만 있고 값이 안 흐른다(2026-09-09 실측으로 드러난 자리).
|
||||||
haul_inputs = await haul_inputs_for(project_id)
|
haul_inputs = await haul_inputs_for(project_id)
|
||||||
|
# 곡선이 쓰는 계수도 프로젝트가 고른 값으로 — 토적표·운반표와 같은 값이어야 한다.
|
||||||
|
factors = await conversion_factors_for(project_id)
|
||||||
try:
|
try:
|
||||||
output = await asyncio.to_thread(
|
output = await asyncio.to_thread(
|
||||||
run_bundle_json,
|
run_bundle_json,
|
||||||
@@ -70,7 +73,7 @@ async def compute_haul_plan(
|
|||||||
_NPM_SCRIPT,
|
_NPM_SCRIPT,
|
||||||
{
|
{
|
||||||
"haul_plan_for": result,
|
"haul_plan_for": result,
|
||||||
"context": _mass_haul_context(haul_inputs),
|
"context": _mass_haul_context(haul_inputs, factors),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ from B06_Section.B06_Section_Repository import (
|
|||||||
)
|
)
|
||||||
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs
|
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs
|
||||||
from common_util.common_util_node_bundle import run_bundle_json
|
from common_util.common_util_node_bundle import run_bundle_json
|
||||||
|
from common_util.common_util_project_settings import (
|
||||||
|
earthwork_conversion_factors,
|
||||||
|
quantity_settings,
|
||||||
|
)
|
||||||
from common_util.common_util_storage import resolve_stored_project_path
|
from common_util.common_util_storage import resolve_stored_project_path
|
||||||
from config.config_db import get_db_pool, run_with_connection
|
from config.config_db import get_db_pool, run_with_connection
|
||||||
from config.config_system import (
|
from config.config_system import (
|
||||||
@@ -79,7 +83,25 @@ async def haul_inputs_for(project_id: Any) -> dict[str, Any]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _mass_haul_context(haul_inputs: dict[str, Any] | None = None) -> dict[str, Any]:
|
async def conversion_factors_for(project_id: Any) -> dict[str, dict[str, float]]:
|
||||||
|
"""이 프로젝트가 쓸 토량환산계수. 못 읽으면 정본 기본값 — 화면은 그대로 선다.
|
||||||
|
|
||||||
|
⚠ 곡선·운반·토적표가 **같은 계수**로 서야 한다. 그래서 상수를 직접 들지 않고 이 함수를
|
||||||
|
거친다(고른 값은 프로젝트 설정 `conversion_factors_override` 에 산다).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
stored_path = await run_with_connection(get_project_storage_relative_path, project_id)
|
||||||
|
root = resolve_stored_project_path(stored_path)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("B06 프로젝트 경로를 못 찾음 — 기본 계수로 진행: project_id=%s", project_id)
|
||||||
|
return {kind: dict(entry) for kind, entry in EARTHWORK_CONVERSION_FACTORS.items()}
|
||||||
|
return earthwork_conversion_factors(quantity_settings(root))
|
||||||
|
|
||||||
|
|
||||||
|
def _mass_haul_context(
|
||||||
|
haul_inputs: dict[str, Any] | None = None,
|
||||||
|
factors: dict[str, dict[str, float]] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다.
|
"""유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다.
|
||||||
|
|
||||||
⚠ 채집석 공제(`collected_stone_deduction_m3`)만 상수가 아니라 **B08 이 내는 값**이다.
|
⚠ 채집석 공제(`collected_stone_deduction_m3`)만 상수가 아니라 **B08 이 내는 값**이다.
|
||||||
@@ -93,7 +115,8 @@ def _mass_haul_context(haul_inputs: dict[str, Any] | None = None) -> dict[str, A
|
|||||||
"""
|
"""
|
||||||
inputs = haul_inputs or {}
|
inputs = haul_inputs or {}
|
||||||
return {
|
return {
|
||||||
"earthwork_conversion": EARTHWORK_CONVERSION_FACTORS,
|
# 프로젝트가 고른 계수가 있으면 그것, 없으면 정본 기본값.
|
||||||
|
"earthwork_conversion": factors or EARTHWORK_CONVERSION_FACTORS,
|
||||||
"natural_spoil_min_ground_slope": NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
"natural_spoil_min_ground_slope": NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
||||||
"haul_equipment_limits": [
|
"haul_equipment_limits": [
|
||||||
{"key": key, "max_distance_m": limit}
|
{"key": key, "max_distance_m": limit}
|
||||||
@@ -196,7 +219,9 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int:
|
|||||||
_NPM_SCRIPT,
|
_NPM_SCRIPT,
|
||||||
{
|
{
|
||||||
"detail": detail,
|
"detail": detail,
|
||||||
"context": _mass_haul_context(haul_inputs),
|
"context": _mass_haul_context(
|
||||||
|
haul_inputs, earthwork_conversion_factors(quantity_settings(project_root))
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
marks.append(("Node 번들(면적·유토곡선)", time.perf_counter()))
|
marks.append(("Node 번들(면적·유토곡선)", time.perf_counter()))
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
보정량 = 체적 × 토량환산계수(다짐)
|
보정량 = 체적 × 토량환산계수(다짐)
|
||||||
절취한 흙이 다져지면 줄거나 부푼다. 성토에 쓸 수 있는 양으로 환산한 것이 보정량이다.
|
절취한 흙이 다져지면 줄거나 부푼다. 성토에 쓸 수 있는 양으로 환산한 것이 보정량이다.
|
||||||
계수의 유일한 정의처는 `config.config_system_design.EARTHWORK_CONVERSION_FACTORS` 이며
|
계수의 유일한 정의처는 `config.config_system_design.EARTHWORK_CONVERSION_FACTORS` 이며
|
||||||
여기서 값을 다시 적지 않는다.
|
여기서 값을 다시 적지 않는다. 프로젝트가 고른 값이 있으면 라우터가
|
||||||
|
`earthwork_conversion_factors(settings)` 로 풀어 `factors` 로 넘긴다 — 기본값은 그대로다.
|
||||||
|
|
||||||
측구터파기 토사·암 — 설계가 가른 값을 그대로 읽는다
|
측구터파기 토사·암 — 설계가 가른 값을 그대로 읽는다
|
||||||
B06 이 지반 유형 + 암반 경계선으로 이미 갈라 냈다(`ditch_soil_area_m2`·
|
B06 이 지반 유형 + 암반 경계선으로 이미 갈라 냈다(`ditch_soil_area_m2`·
|
||||||
@@ -49,9 +50,13 @@ _FALLBACK_BASIS = "cut_area_ratio_fallback"
|
|||||||
_FALLBACK_NOTE = "측구 가름값이 설계에 없어 절토 토사:암 면적비로 안분함"
|
_FALLBACK_NOTE = "측구 가름값이 설계에 없어 절토 토사:암 면적비로 안분함"
|
||||||
|
|
||||||
|
|
||||||
def _factor(kind: str) -> float:
|
#: 계수 묶음의 모양 — `{지반유형: {"compacted": C}}`.
|
||||||
|
Factors = dict[str, dict[str, float]]
|
||||||
|
|
||||||
|
|
||||||
|
def _factor(kind: str, factors: Factors) -> float:
|
||||||
"""지반유형 → 다짐 환산계수. 모르는 유형이면 토사로 본다."""
|
"""지반유형 → 다짐 환산계수. 모르는 유형이면 토사로 본다."""
|
||||||
entry = EARTHWORK_CONVERSION_FACTORS.get(kind) or EARTHWORK_CONVERSION_FACTORS["soil"]
|
entry = factors.get(kind) or factors["soil"]
|
||||||
return float(entry["compacted"])
|
return float(entry["compacted"])
|
||||||
|
|
||||||
|
|
||||||
@@ -155,8 +160,15 @@ def _split_ditch(area: StationArea) -> tuple[float, float, str]:
|
|||||||
return ditch * soil / total, ditch * rock / total, _FALLBACK_BASIS
|
return ditch * soil / total, ditch * rock / total, _FALLBACK_BASIS
|
||||||
|
|
||||||
|
|
||||||
def build_rows(stations: Iterable[StationArea]) -> list[EarthworkRow]:
|
def build_rows(
|
||||||
"""측점 목록 → 토적표 줄 목록. 측점은 이정 순으로 정렬해 받는다."""
|
stations: Iterable[StationArea], factors: Factors | None = None
|
||||||
|
) -> list[EarthworkRow]:
|
||||||
|
"""측점 목록 → 토적표 줄 목록. 측점은 이정 순으로 정렬해 받는다.
|
||||||
|
|
||||||
|
`factors` 는 프로젝트가 고른 토량환산계수다(`earthwork_conversion_factors`).
|
||||||
|
안 주면 정본 기본값이 선다 — 설정을 안 읽는 자리(시험·되짚기)를 위한 것이다.
|
||||||
|
"""
|
||||||
|
factors = factors or EARTHWORK_CONVERSION_FACTORS
|
||||||
ordered = sorted(stations, key=lambda s: s.chainage_m)
|
ordered = sorted(stations, key=lambda s: s.chainage_m)
|
||||||
rows: list[EarthworkRow] = []
|
rows: list[EarthworkRow] = []
|
||||||
previous: StationArea | None = None
|
previous: StationArea | None = None
|
||||||
@@ -165,8 +177,8 @@ def build_rows(stations: Iterable[StationArea]) -> list[EarthworkRow]:
|
|||||||
|
|
||||||
for station in ordered:
|
for station in ordered:
|
||||||
ditch_soil, ditch_rock, ditch_basis = _split_ditch(station)
|
ditch_soil, ditch_rock, ditch_basis = _split_ditch(station)
|
||||||
soil_factor = _factor("soil")
|
soil_factor = _factor("soil", factors)
|
||||||
rock_factor = _factor(station.cut_rock_kind or _DEFAULT_ROCK_KIND)
|
rock_factor = _factor(station.cut_rock_kind or _DEFAULT_ROCK_KIND, factors)
|
||||||
row = EarthworkRow(
|
row = EarthworkRow(
|
||||||
chainage_m=station.chainage_m,
|
chainage_m=station.chainage_m,
|
||||||
cut_soil_area_m2=station.cut_soil_area_m2,
|
cut_soil_area_m2=station.cut_soil_area_m2,
|
||||||
@@ -238,12 +250,17 @@ def totals(rows: list[EarthworkRow]) -> dict[str, float]:
|
|||||||
return {key: sum(getattr(row, key) for row in rows) for key in keys}
|
return {key: sum(getattr(row, key) for row in rows) for key in keys}
|
||||||
|
|
||||||
|
|
||||||
def build_table(stations: Iterable[StationArea]) -> dict[str, Any]:
|
def build_table(stations: Iterable[StationArea], factors: Factors | None = None) -> dict[str, Any]:
|
||||||
"""화면·API 가 그대로 쓰는 모양. 값은 자르지 않는다(PLAN 8-16)."""
|
"""화면·API 가 그대로 쓰는 모양. 값은 자르지 않는다(PLAN 8-16).
|
||||||
rows = build_rows(stations)
|
|
||||||
|
`conversion_factors` 로 **실제로 쓴 계수**를 되싣는다 — 프로젝트가 고른 값이면
|
||||||
|
그것이 나가야 화면이 「무엇으로 셌나」를 그대로 보인다.
|
||||||
|
"""
|
||||||
|
factors = factors or EARTHWORK_CONVERSION_FACTORS
|
||||||
|
rows = build_rows(stations, factors)
|
||||||
return {
|
return {
|
||||||
"method": "average_end_area",
|
"method": "average_end_area",
|
||||||
"conversion_factors": EARTHWORK_CONVERSION_FACTORS,
|
"conversion_factors": factors,
|
||||||
"rows": [row.__dict__ if not hasattr(row, "__slots__") else _as_dict(row) for row in rows],
|
"rows": [row.__dict__ if not hasattr(row, "__slots__") else _as_dict(row) for row in rows],
|
||||||
"totals": totals(rows),
|
"totals": totals(rows),
|
||||||
"station_count": len(rows),
|
"station_count": len(rows),
|
||||||
|
|||||||
@@ -43,14 +43,24 @@ GROUND_LABELS = {"ea_m3": "토사", "rr_m3": "리핑암", "br_m3": "발파암"}
|
|||||||
GROUND_KIND_OF = {"토사": "soil", "리핑암": "ripping_rock", "발파암": "blasting_rock"}
|
GROUND_KIND_OF = {"토사": "soil", "리핑암": "ripping_rock", "발파암": "blasting_rock"}
|
||||||
|
|
||||||
|
|
||||||
def _factor_of(ground: str) -> float | None:
|
#: 계수 묶음의 모양 — `{지반유형: {"compacted": C}}`. 안 주면 정본 기본값이 선다.
|
||||||
"""그 갈래의 다짐 환산계수 `C`. 모르면 `None`(받는 쪽이 환산했는지 되짚는 데 쓴다)."""
|
Factors = dict[str, dict[str, float]]
|
||||||
|
|
||||||
|
|
||||||
|
def _factor_of(ground: str, factors: Factors | None = None) -> float | None:
|
||||||
|
"""그 갈래의 다짐 환산계수 `C`. 모르면 `None`(받는 쪽이 환산했는지 되짚는 데 쓴다).
|
||||||
|
|
||||||
|
`factors` 는 프로젝트가 고른 계수다(`earthwork_conversion_factors`) — 안 주면 정본.
|
||||||
|
"""
|
||||||
kind = GROUND_KIND_OF.get(ground)
|
kind = GROUND_KIND_OF.get(ground)
|
||||||
entry = EARTHWORK_CONVERSION_FACTORS.get(kind) if kind else None
|
table = factors or EARTHWORK_CONVERSION_FACTORS
|
||||||
|
entry = table.get(kind) if kind else None
|
||||||
return float(entry["compacted"]) if entry else None
|
return float(entry["compacted"]) if entry else None
|
||||||
|
|
||||||
|
|
||||||
def natural_m3(compacted_volume_m3: float, ground: str) -> float | None:
|
def natural_m3(
|
||||||
|
compacted_volume_m3: float, ground: str, factors: Factors | None = None
|
||||||
|
) -> float | None:
|
||||||
"""**다짐상태 → 자연상태**(÷ C). 내역서에 오르는 수량은 자연상태다.
|
"""**다짐상태 → 자연상태**(÷ C). 내역서에 오르는 수량은 자연상태다.
|
||||||
|
|
||||||
근거 — `config_system_design` 5-4-3 에 이미 적혀 있던 문장이다.
|
근거 — `config_system_design` 5-4-3 에 이미 적혀 있던 문장이다.
|
||||||
@@ -69,7 +79,8 @@ def natural_m3(compacted_volume_m3: float, ground: str) -> float | None:
|
|||||||
⚠ 갈래를 모르면 `None` 이다 — 토사 계수로 눅이면 근거 없이 금액이 움직인다.
|
⚠ 갈래를 모르면 `None` 이다 — 토사 계수로 눅이면 근거 없이 금액이 움직인다.
|
||||||
"""
|
"""
|
||||||
kind = GROUND_KIND_OF.get(ground)
|
kind = GROUND_KIND_OF.get(ground)
|
||||||
entry = EARTHWORK_CONVERSION_FACTORS.get(kind) if kind else None
|
table = factors or EARTHWORK_CONVERSION_FACTORS
|
||||||
|
entry = table.get(kind) if kind else None
|
||||||
if not entry:
|
if not entry:
|
||||||
return None
|
return None
|
||||||
factor = float(entry["compacted"])
|
factor = float(entry["compacted"])
|
||||||
@@ -189,8 +200,11 @@ def summarize(legs: Iterable[HaulLeg]) -> list[HaulSummaryRow]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_table(plan: dict[str, Any] | None) -> dict[str, Any]:
|
def build_table(plan: dict[str, Any] | None, factors: Factors | None = None) -> dict[str, Any]:
|
||||||
"""화면·API 가 그대로 쓰는 모양. 내역 줄과 근거 줄을 함께 낸다."""
|
"""화면·API 가 그대로 쓰는 모양. 내역 줄과 근거 줄을 함께 낸다.
|
||||||
|
|
||||||
|
`factors` 는 프로젝트가 고른 토량환산계수다 — 다짐 → 자연 되돌리기가 이 값에 걸린다.
|
||||||
|
"""
|
||||||
legs = _legs_of(plan or {})
|
legs = _legs_of(plan or {})
|
||||||
rows = summarize(legs)
|
rows = summarize(legs)
|
||||||
return {
|
return {
|
||||||
@@ -203,9 +217,9 @@ def build_table(plan: dict[str, Any] | None) -> dict[str, Any]:
|
|||||||
"volume_m3": row.volume_m3,
|
"volume_m3": row.volume_m3,
|
||||||
"volume_basis": "compacted",
|
"volume_basis": "compacted",
|
||||||
# 내역서에 오르는 수량 = **자연상태**(÷C). 갈래를 모르면 `None`.
|
# 내역서에 오르는 수량 = **자연상태**(÷C). 갈래를 모르면 `None`.
|
||||||
"natural_m3": natural_m3(row.volume_m3, row.ground),
|
"natural_m3": natural_m3(row.volume_m3, row.ground, factors),
|
||||||
"natural_volume_basis": "natural",
|
"natural_volume_basis": "natural",
|
||||||
"conversion_c": _factor_of(row.ground),
|
"conversion_c": _factor_of(row.ground, factors),
|
||||||
"average_distance_m": row.average_distance_m,
|
"average_distance_m": row.average_distance_m,
|
||||||
"work_m3m": row.work_m3m,
|
"work_m3m": row.work_m3m,
|
||||||
"legs": row.legs,
|
"legs": row.legs,
|
||||||
|
|||||||
@@ -45,13 +45,18 @@ from common_util.common_util_project_settings import (
|
|||||||
ROCK_METHODS,
|
ROCK_METHODS,
|
||||||
application_ratio,
|
application_ratio,
|
||||||
concrete_placing_method,
|
concrete_placing_method,
|
||||||
|
earthwork_conversion_choices,
|
||||||
|
earthwork_conversion_factors,
|
||||||
quantity_settings,
|
quantity_settings,
|
||||||
rock_classes,
|
rock_classes,
|
||||||
save_section,
|
save_section,
|
||||||
)
|
)
|
||||||
from common_util.common_util_storage import resolve_stored_project_path
|
from common_util.common_util_storage import resolve_stored_project_path
|
||||||
from config.config_db import run_with_connection
|
from config.config_db import run_with_connection
|
||||||
from config.config_system_design import EARTHWORK_CONVERSION_FACTORS
|
from config.config_system_design import (
|
||||||
|
EARTHWORK_CONVERSION_FACTORS,
|
||||||
|
EARTHWORK_CONVERSION_PUMSEM_C_RANGES,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/api/projects", tags=["B08 Quantity"])
|
router = APIRouter(prefix="/api/projects", tags=["B08 Quantity"])
|
||||||
@@ -78,14 +83,24 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
|||||||
status_code=500,
|
status_code=500,
|
||||||
content={"status": "error", "message": "토적표를 만들지 못했습니다."},
|
content={"status": "error", "message": "토적표를 만들지 못했습니다."},
|
||||||
)
|
)
|
||||||
table = build_table(_stations(designs))
|
# ⚠ 설정을 **먼저** 읽는다 — 토량환산계수를 프로젝트가 골랐으면 표가 그 값으로 서야 한다.
|
||||||
|
settings, project_root = await _project_settings(project_id)
|
||||||
|
factors = earthwork_conversion_factors(settings)
|
||||||
|
table = build_table(_stations(designs), factors)
|
||||||
|
# 화면이 「무엇을 골랐나 · 품셈 범위 안인가」를 보이는 데 쓴다. 계산에는 안 들어간다.
|
||||||
|
table["conversion_factor_choices"] = earthwork_conversion_choices(settings)
|
||||||
|
# 품셈 암종별 범위 — **화면 안내용**이다. 정의처가 서버 한 곳이라 내려보내 쓴다
|
||||||
|
# (프론트에 다시 적으면 두 벌이 되어 갈린다).
|
||||||
|
table["conversion_factor_pumsem_ranges"] = [
|
||||||
|
{"name": name, "min": low, "max": high}
|
||||||
|
for name, low, high in EARTHWORK_CONVERSION_PUMSEM_C_RANGES
|
||||||
|
]
|
||||||
# 사면 계열은 저장된 설계선에서 유도한다.
|
# 사면 계열은 저장된 설계선에서 유도한다.
|
||||||
slope = build_slope_table(station_slopes(designs))
|
slope = build_slope_table(station_slopes(designs))
|
||||||
table["slope"] = slope
|
table["slope"] = slope
|
||||||
|
|
||||||
settings, project_root = await _project_settings(project_id)
|
|
||||||
plan = await _stored_haul_plan(project_id, route_id)
|
plan = await _stored_haul_plan(project_id, route_id)
|
||||||
haul = build_haul_table(plan)
|
haul = build_haul_table(plan, factors)
|
||||||
# 사토 — **운반 줄이 되는 값**인데 유토곡선의 띠·이동에는 안 들어 있다(잔량으로 남는다).
|
# 사토 — **운반 줄이 되는 값**인데 유토곡선의 띠·이동에는 안 들어 있다(잔량으로 남는다).
|
||||||
# 여기서 그 값을 운반표에 실어 인계가 「사토 운반」 한 줄을 세우게 한다.
|
# 여기서 그 값을 운반표에 실어 인계가 「사토 운반」 한 줄을 세우게 한다.
|
||||||
# ⚠ 거리는 품셈이 정하지 않는다 — 설계 입력(`spoil_site_distance_m`)이고 없으면 막힌다.
|
# ⚠ 거리는 품셈이 정하지 않는다 — 설계 입력(`spoil_site_distance_m`)이고 없으면 막힌다.
|
||||||
@@ -164,12 +179,14 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
|||||||
return JSONResponse(content=table)
|
return JSONResponse(content=table)
|
||||||
|
|
||||||
|
|
||||||
#: 갈래 칸 ↔ 다짐 환산계수 `C`. 정의처는 `config_system_design` 한 곳뿐이다.
|
#: 갈래 칸 ↔ 지반유형 이름. 계수의 정의처는 `config_system_design` 한 곳뿐이다.
|
||||||
_COMPACTED_FACTOR = {
|
_GROUND_KIND_OF = {"ea_m3": "soil", "rr_m3": "ripping_rock", "br_m3": "blasting_rock"}
|
||||||
"ea_m3": float(EARTHWORK_CONVERSION_FACTORS["soil"]["compacted"]),
|
|
||||||
"rr_m3": float(EARTHWORK_CONVERSION_FACTORS["ripping_rock"]["compacted"]),
|
|
||||||
"br_m3": float(EARTHWORK_CONVERSION_FACTORS["blasting_rock"]["compacted"]),
|
def _compacted_factor(settings: dict[str, Any]) -> dict[str, float]:
|
||||||
}
|
"""갈래 칸 ↔ 다짐 환산계수 `C` — 프로젝트가 고른 값이 있으면 그것이 선다."""
|
||||||
|
factors = earthwork_conversion_factors(settings)
|
||||||
|
return {key: float(factors[kind]["compacted"]) for key, kind in _GROUND_KIND_OF.items()}
|
||||||
|
|
||||||
|
|
||||||
def _spoil_sites(designs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _spoil_sites(designs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
@@ -295,10 +312,11 @@ def _spoil_of(
|
|||||||
# 여기서 ÷C 한 값을 함께 내 받는 쪽이 **또 환산하지 않게** 한다.
|
# 여기서 ÷C 한 값을 함께 내 받는 쪽이 **또 환산하지 않게** 한다.
|
||||||
# ⚠ 갈래를 못 붙인 몫은 계수가 없어 **환산하지 않는다** — 토사 계수로 눅이면 근거 없이
|
# ⚠ 갈래를 못 붙인 몫은 계수가 없어 **환산하지 않는다** — 토사 계수로 눅이면 근거 없이
|
||||||
# 금액이 움직인다. 그 사실을 사유로 낸다.
|
# 금액이 움직인다. 그 사실을 사유로 낸다.
|
||||||
|
compacted_factor = _compacted_factor(settings)
|
||||||
natural_by_ground = {
|
natural_by_ground = {
|
||||||
key: round(value / _COMPACTED_FACTOR[key], 3)
|
key: round(value / compacted_factor[key], 3)
|
||||||
for key, value in grounds.items()
|
for key, value in grounds.items()
|
||||||
if key in _COMPACTED_FACTOR
|
if key in compacted_factor
|
||||||
}
|
}
|
||||||
if unknown > 0:
|
if unknown > 0:
|
||||||
note_parts.append(f"⚠ 갈래를 못 붙인 {unknown:,.2f}㎥ 는 상태도 못 되돌림")
|
note_parts.append(f"⚠ 갈래를 못 붙인 {unknown:,.2f}㎥ 는 상태도 못 되돌림")
|
||||||
@@ -417,6 +435,10 @@ class QuantitySettingsBody(BaseModel):
|
|||||||
# 임목파쇄 — 기본 꺼짐(확정 5차 5번). 켜면 줄이 서고, 부피를 넣으면 값이 선다.
|
# 임목파쇄 — 기본 꺼짐(확정 5차 5번). 켜면 줄이 서고, 부피를 넣으면 값이 선다.
|
||||||
wood_chipping_enabled: bool | None = None
|
wood_chipping_enabled: bool | None = None
|
||||||
wood_chipping_volume_m3: float | None = None
|
wood_chipping_volume_m3: float | None = None
|
||||||
|
# 토량환산계수(다짐) — `{갈래: {"compacted": C, "reason": 사유}}`.
|
||||||
|
# ⚠ **기본값을 복사해 넣지 않는다** — 안 고른 갈래는 키가 없어야 정본이 선다.
|
||||||
|
# 빈 dict 는 「전부 기본값으로 되돌림」이라 통째로 갈아 끼운다.
|
||||||
|
conversion_factors_override: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
||||||
@@ -458,6 +480,21 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
|
|||||||
method = values["concrete_placing_method"]
|
method = values["concrete_placing_method"]
|
||||||
# 「안 정함」으로 되돌릴 수 있어야 한다 — 빈 값이면 지운다(8-22 ② 와 같은 자리).
|
# 「안 정함」으로 되돌릴 수 있어야 한다 — 빈 값이면 지운다(8-22 ② 와 같은 자리).
|
||||||
values["concrete_placing_method"] = method if method in CONCRETE_PLACING_METHODS else None
|
values["concrete_placing_method"] = method if method in CONCRETE_PLACING_METHODS 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:
|
if "rock_methods" in values:
|
||||||
# 「안 정함」(빈 값)은 저장하지 않는다 — 정한 것과 구별이 안 된다. 통째로 갈아 끼우므로
|
# 「안 정함」(빈 값)은 저장하지 않는다 — 정한 것과 구별이 안 된다. 통째로 갈아 끼우므로
|
||||||
# 여기서 버리면 그 갈래는 미지정으로 돌아간다.
|
# 여기서 버리면 그 갈래는 미지정으로 돌아간다.
|
||||||
@@ -473,7 +510,14 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
|
|||||||
_save_quantity,
|
_save_quantity,
|
||||||
root,
|
root,
|
||||||
values,
|
values,
|
||||||
("rock_methods", "material_supply", "concrete_placing_method", "ancillary_counts")
|
(
|
||||||
|
"rock_methods",
|
||||||
|
"material_supply",
|
||||||
|
"concrete_placing_method",
|
||||||
|
"ancillary_counts",
|
||||||
|
# 고른 계수를 **기본값으로 되돌릴 길**이 있어야 한다 — 병합이면 못 지운다.
|
||||||
|
"conversion_factors_override",
|
||||||
|
)
|
||||||
+ NULLABLE_SETTING_KEYS,
|
+ NULLABLE_SETTING_KEYS,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B08_Quantity_UI_ConversionFactors.ts
|
||||||
|
* 산출 조건 패널의 「토량환산계수(다짐)」 칸 — 고를 수 있게 열어 둔 자리.
|
||||||
|
*
|
||||||
|
* 왜 고르게 하나 (오솔길 대조 06절 3번)
|
||||||
|
* 품셈 체적변화율표가 암종마다 **범위**를 주고 「토질 시험하여 적용함을 원칙」이라 한다.
|
||||||
|
* 즉 정답 숫자가 하나가 아니다. 경쟁사(오솔길)가 전 구간 1.0 을 쓰는 것도 풍화암·연암
|
||||||
|
* 범위의 하한이라 틀린 값이 아니다. 그래서 **값을 못 박지 않고 범위를 보이며 고르게** 한다.
|
||||||
|
*
|
||||||
|
* ⚠ 기본값은 건드리지 않는다
|
||||||
|
* 정의처는 서버 `config_system_design.EARTHWORK_CONVERSION_FACTORS` 한 곳이다. 화면은
|
||||||
|
* 고른 값만 `conversion_factors_override` 로 보내고, 안 고른 갈래는 **키 자체를 안 보낸다** —
|
||||||
|
* 그래야 나중에 정본이 바뀌어도 옛 프로젝트가 따라온다.
|
||||||
|
*
|
||||||
|
* ⚠ 범위 밖을 막지 않는다
|
||||||
|
* 토질시험 값일 수 있다. 막는 대신 **사유를 적게** 하고, 그 사유가 정본에 함께 남는다.
|
||||||
|
*
|
||||||
|
* ⚠ 이 계수는 토적표만 쓰는 것이 아니다
|
||||||
|
* 유토곡선(B06) · 운반표 · 기초단가가 같은 값을 읽는다. 패널에 그 사실을 한 줄 보인다 —
|
||||||
|
* 안 보이면 「토적표만 바뀌겠지」로 읽힌다.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
|
|
||||||
|
import type { ConversionFactorChoice, PumsemRange } from "./B08_Quantity_UI_EarthworkGrid";
|
||||||
|
|
||||||
|
/** locale 헬퍼 — 페이지 쪽과 같은 모양으로 둔다(문구는 `ui_template_locale_b2`). */
|
||||||
|
function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 화면이 들고 있는 고른 값 — `compacted` 가 `null` 이면 「안 고름」이라 저장에서 빠진다. */
|
||||||
|
export interface FactorDraft {
|
||||||
|
compacted: number | null;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 서버 키 → 사람이 읽는 이름. 모르는 키는 **지어내지 않고** 그대로 보인다. */
|
||||||
|
const GROUND_LABELS: Record<string, string> = {
|
||||||
|
soil: "토사",
|
||||||
|
ripping_rock: "리핑암",
|
||||||
|
blasting_rock: "발파암",
|
||||||
|
};
|
||||||
|
|
||||||
|
function groundLabel(kind: string): string {
|
||||||
|
return GROUND_LABELS[kind] ?? kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hint(text: string): HTMLElement {
|
||||||
|
const row = document.createElement("p");
|
||||||
|
row.className = "b08-quantity__hint";
|
||||||
|
row.textContent = text;
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 범위 밖인가 — 서버가 준 범위로 판정한다. 범위를 모르면 **밖이라고 하지 않는다.** */
|
||||||
|
function outOfRange(value: number, range: [number, number] | null): boolean {
|
||||||
|
if (!range) return false;
|
||||||
|
return value < range[0] || value > range[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 갈래 한 줄 — 값 칸 + 기본값·품셈 범위 안내 + (범위 밖일 때만) 사유 칸.
|
||||||
|
*
|
||||||
|
* 값을 비우면 「안 고름」으로 돌아가 기본값이 선다. 그 되돌리는 길이 있어야
|
||||||
|
* 한 번 넣은 값이 영영 남지 않는다.
|
||||||
|
*/
|
||||||
|
function factorRow(
|
||||||
|
kind: string,
|
||||||
|
choice: ConversionFactorChoice,
|
||||||
|
draft: Record<string, FactorDraft>,
|
||||||
|
onChange: () => void,
|
||||||
|
): HTMLElement {
|
||||||
|
const box = document.createElement("div");
|
||||||
|
box.className = "b08-quantity__factor";
|
||||||
|
|
||||||
|
const row = document.createElement("label");
|
||||||
|
row.className = "b08-quantity__field";
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.textContent = groundLabel(kind);
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "number";
|
||||||
|
input.className = "b08-quantity__input";
|
||||||
|
input.min = "0";
|
||||||
|
input.step = "0.01";
|
||||||
|
input.placeholder = String(choice.default);
|
||||||
|
const current = draft[kind]?.compacted;
|
||||||
|
input.value = current === null || current === undefined ? "" : String(current);
|
||||||
|
row.append(name, input);
|
||||||
|
box.append(row);
|
||||||
|
|
||||||
|
const range = (choice.range ?? null) as [number, number] | null;
|
||||||
|
box.append(
|
||||||
|
hint(
|
||||||
|
`${L("B08_Quantity_Factor_Default")} ${choice.default}` +
|
||||||
|
(range
|
||||||
|
? ` · ${L("B08_Quantity_Factor_Range")} ${range[0].toFixed(2)}~${range[1].toFixed(2)}`
|
||||||
|
: ""),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 사유 칸은 **범위 밖일 때만** 선다 — 늘 띄우면 채우지 않아도 되는 칸으로 읽힌다.
|
||||||
|
const reasonRow = document.createElement("label");
|
||||||
|
reasonRow.className = "b08-quantity__field";
|
||||||
|
const reasonName = document.createElement("span");
|
||||||
|
reasonName.textContent = L("B08_Quantity_Factor_Reason");
|
||||||
|
const reasonInput = document.createElement("input");
|
||||||
|
reasonInput.type = "text";
|
||||||
|
reasonInput.className = "b08-quantity__input";
|
||||||
|
reasonInput.value = draft[kind]?.reason ?? "";
|
||||||
|
reasonRow.append(reasonName, reasonInput);
|
||||||
|
const warning = hint(L("B08_Quantity_Factor_OutOfRange"));
|
||||||
|
warning.classList.add("b08-quantity__hint--warn");
|
||||||
|
|
||||||
|
const sync = (): void => {
|
||||||
|
const value = input.value.trim() === "" ? null : Number(input.value);
|
||||||
|
const outside = value !== null && Number.isFinite(value) && outOfRange(value, range);
|
||||||
|
warning.hidden = !outside;
|
||||||
|
reasonRow.hidden = !outside;
|
||||||
|
};
|
||||||
|
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
const raw = input.value.trim();
|
||||||
|
const value = raw === "" ? null : Number(raw);
|
||||||
|
draft[kind] = {
|
||||||
|
compacted: value !== null && Number.isFinite(value) ? value : null,
|
||||||
|
reason: draft[kind]?.reason ?? "",
|
||||||
|
};
|
||||||
|
sync();
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
reasonInput.addEventListener("input", () => {
|
||||||
|
draft[kind] = {
|
||||||
|
compacted: draft[kind]?.compacted ?? null,
|
||||||
|
reason: reasonInput.value,
|
||||||
|
};
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
box.append(warning, reasonRow);
|
||||||
|
sync();
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「토량환산계수(다짐)」 구획 전체. 서버가 준 갈래만 그린다 — 갈래 수를 화면에 안 박는다.
|
||||||
|
*
|
||||||
|
* `choices` 가 없으면(옛 응답) **아무것도 그리지 않는다** — 빈 칸을 지어내지 않는다.
|
||||||
|
*/
|
||||||
|
export function renderConversionFactorFields(
|
||||||
|
choices: Record<string, ConversionFactorChoice> | undefined,
|
||||||
|
pumsem: PumsemRange[] | undefined,
|
||||||
|
draft: Record<string, FactorDraft>,
|
||||||
|
onChange: () => void,
|
||||||
|
): HTMLElement | null {
|
||||||
|
const entries = Object.entries(choices ?? {});
|
||||||
|
if (!entries.length) return null;
|
||||||
|
|
||||||
|
const box = document.createElement("div");
|
||||||
|
box.className = "b08-quantity__factors";
|
||||||
|
const title = document.createElement("div");
|
||||||
|
title.className = "b08-quantity__field";
|
||||||
|
const titleName = document.createElement("span");
|
||||||
|
titleName.textContent = L("B08_Quantity_Side_Factors");
|
||||||
|
title.append(titleName);
|
||||||
|
box.append(title);
|
||||||
|
// 어디까지 닿는 값인지 먼저 보인다 — 토적표만 바뀌는 줄 알면 함부로 고친다.
|
||||||
|
box.append(hint(L("B08_Quantity_Factor_Reach")));
|
||||||
|
|
||||||
|
for (const [kind, choice] of entries) {
|
||||||
|
box.append(factorRow(kind, choice, draft, onChange));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 품셈 암종별 범위 — 서버가 내려 준 값을 그대로 보인다(화면에 다시 적지 않는다).
|
||||||
|
if (pumsem?.length) {
|
||||||
|
box.append(
|
||||||
|
hint(
|
||||||
|
`${L("B08_Quantity_Factor_Pumsem")} — ` +
|
||||||
|
pumsem
|
||||||
|
.map((item) => `${item.name} ${item.min.toFixed(2)}~${item.max.toFixed(2)}`)
|
||||||
|
.join(" · "),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 저장 몸통에 실을 모양 — **고른 갈래만** 담는다. 빈 dict 는 「전부 기본값」이다. */
|
||||||
|
export function conversionOverridePayload(
|
||||||
|
draft: Record<string, FactorDraft>,
|
||||||
|
): Record<string, { compacted: number; reason?: string }> {
|
||||||
|
const payload: Record<string, { compacted: number; reason?: string }> = {};
|
||||||
|
for (const [kind, entry] of Object.entries(draft)) {
|
||||||
|
if (entry.compacted === null || !Number.isFinite(entry.compacted) || entry.compacted <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
payload[kind] = entry.reason.trim()
|
||||||
|
? { compacted: entry.compacted, reason: entry.reason.trim() }
|
||||||
|
: { compacted: entry.compacted };
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
@@ -61,6 +61,8 @@ export interface SlopeTable {
|
|||||||
|
|
||||||
/** 산출 조건 — `project_settings.json` 의 `quantity` 구획. */
|
/** 산출 조건 — `project_settings.json` 의 `quantity` 구획. */
|
||||||
export interface QuantitySettings {
|
export interface QuantitySettings {
|
||||||
|
/** 고른 토량환산계수 — `{갈래: {compacted, reason?}}`. 안 고르면 키가 없다. */
|
||||||
|
conversion_factors_override?: Record<string, { compacted?: number; reason?: string }> | null;
|
||||||
rock_class_set?: string;
|
rock_class_set?: string;
|
||||||
rock_classes?: string[];
|
rock_classes?: string[];
|
||||||
rock_ratios_pct?: Record<string, number>;
|
rock_ratios_pct?: Record<string, number>;
|
||||||
@@ -95,6 +97,25 @@ export interface QuantitySettings {
|
|||||||
wood_chipping_volume_m3?: number | null;
|
wood_chipping_volume_m3?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 갈래 하나의 「무엇을 골랐나」. 서버 `earthwork_conversion_choices` 와 짝이다. */
|
||||||
|
export interface ConversionFactorChoice {
|
||||||
|
compacted: number;
|
||||||
|
default: number;
|
||||||
|
/** 기본값과 다른 값을 골랐나. */
|
||||||
|
chosen: boolean;
|
||||||
|
/** 품셈 범위 안인가. 밖이어도 **막지 않고** 사유를 받는다. */
|
||||||
|
in_range: boolean;
|
||||||
|
range: [number, number] | null;
|
||||||
|
reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 품셈 암종별 체적변화율 범위 — **화면 안내용**이고 계산에 안 쓴다. */
|
||||||
|
export interface PumsemRange {
|
||||||
|
name: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EarthworkTable {
|
export interface EarthworkTable {
|
||||||
method: string;
|
method: string;
|
||||||
station_count: number;
|
station_count: number;
|
||||||
@@ -109,6 +130,10 @@ export interface EarthworkTable {
|
|||||||
/** 운반계획은 [저장]·[확정]에서 정본에 남는 값 — 아직 없으면 false. */
|
/** 운반계획은 [저장]·[확정]에서 정본에 남는 값 — 아직 없으면 false. */
|
||||||
haul_available?: boolean;
|
haul_available?: boolean;
|
||||||
settings?: QuantitySettings;
|
settings?: QuantitySettings;
|
||||||
|
/** 갈래별 토량환산계수 선택 상태 — 산출 조건 패널이 그린다. */
|
||||||
|
conversion_factor_choices?: Record<string, ConversionFactorChoice>;
|
||||||
|
/** 품셈 암종별 범위(안내용). 정의처가 서버라 내려받아 보인다. */
|
||||||
|
conversion_factor_pumsem_ranges?: PumsemRange[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 표 칸에 들어갈 수 있는 열 — 숫자 칸만 고른다(사유·주기는 표 밖이다). */
|
/** 표 칸에 들어갈 수 있는 열 — 숫자 칸만 고른다(사유·주기는 표 밖이다). */
|
||||||
|
|||||||
@@ -186,6 +186,11 @@ const CSS = `
|
|||||||
.b08-quantity__field-value { color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }
|
.b08-quantity__field-value { color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }
|
||||||
/* 칸 밑 근거 한 줄 — 왜 그 값인지 화면에서 보이게 한다(2026-09-09 사용자 지시). */
|
/* 칸 밑 근거 한 줄 — 왜 그 값인지 화면에서 보이게 한다(2026-09-09 사용자 지시). */
|
||||||
.b08-quantity__hint { margin: 0 0 6px; font-size: 11px; line-height: 1.4; color: var(--color-text-secondary); }
|
.b08-quantity__hint { margin: 0 0 6px; font-size: 11px; line-height: 1.4; color: var(--color-text-secondary); }
|
||||||
|
/* 품셈 범위 밖을 고른 칸 — **막지 않고** 사유를 받는 자리라 경고 색만 준다. */
|
||||||
|
.b08-quantity__hint--warn { color: var(--color-warning, #b45309); }
|
||||||
|
/* 토량환산계수 구획 — 갈래마다 (값 · 안내 · 사유)가 한 덩어리로 붙는다. */
|
||||||
|
.b08-quantity__factors { margin: 4px 0 10px; }
|
||||||
|
.b08-quantity__factor { margin-bottom: 6px; }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/** 스타일을 한 번만 넣는다 — 페이지를 다시 그려도 중복되지 않는다. */
|
/** 스타일을 한 번만 넣는다 — 페이지를 다시 그려도 중복되지 않는다. */
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ import {
|
|||||||
WORKFLOW_STEP_ROUTES,
|
WORKFLOW_STEP_ROUTES,
|
||||||
} from "../A00_Common/b_workflow_nav";
|
} from "../A00_Common/b_workflow_nav";
|
||||||
import { renderEarthworkGrid, type EarthworkTable } from "./B08_Quantity_UI_EarthworkGrid";
|
import { renderEarthworkGrid, type EarthworkTable } from "./B08_Quantity_UI_EarthworkGrid";
|
||||||
|
import {
|
||||||
|
conversionOverridePayload,
|
||||||
|
renderConversionFactorFields,
|
||||||
|
type FactorDraft,
|
||||||
|
} from "./B08_Quantity_UI_ConversionFactors";
|
||||||
import { injectEarthworkGridStyles } from "./B08_Quantity_UI_EarthworkGrid_Style";
|
import { injectEarthworkGridStyles } from "./B08_Quantity_UI_EarthworkGrid_Style";
|
||||||
import {
|
import {
|
||||||
renderHaulGrid,
|
renderHaulGrid,
|
||||||
@@ -98,6 +103,8 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr
|
|||||||
wood_chipping_volume_m3: draft.wood_chipping_volume_m3,
|
wood_chipping_volume_m3: draft.wood_chipping_volume_m3,
|
||||||
// 개소는 **통째로** 보낸다 — 지운 항목까지 그대로 가야 되돌릴 길이 있다.
|
// 개소는 **통째로** 보낸다 — 지운 항목까지 그대로 가야 되돌릴 길이 있다.
|
||||||
ancillary_counts: draft.ancillary_counts,
|
ancillary_counts: draft.ancillary_counts,
|
||||||
|
// 토량환산계수 — 고른 갈래만 담긴다. 빈 dict 는 「전부 기본값으로 되돌림」이다.
|
||||||
|
conversion_factors_override: conversionOverridePayload(draft.conversion_factors),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -243,16 +250,6 @@ function selectField(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 지반 종류 표기 — 서버 키가 화면에 새지 않게. 모르는 키는 그대로 보인다. */
|
/** 지반 종류 표기 — 서버 키가 화면에 새지 않게. 모르는 키는 그대로 보인다. */
|
||||||
const GROUND_LABELS: Record<string, string> = {
|
|
||||||
soil: "토사",
|
|
||||||
ripping_rock: "리핑암",
|
|
||||||
blasting_rock: "발파암",
|
|
||||||
};
|
|
||||||
|
|
||||||
function groundLabel(kind: string): string {
|
|
||||||
return GROUND_LABELS[kind] ?? kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 자재 한 줄의 관급/사급. `install_by` 는 **관급 줄에만** 뜻이 있다. */
|
/** 자재 한 줄의 관급/사급. `install_by` 는 **관급 줄에만** 뜻이 있다. */
|
||||||
export interface SupplyChoice {
|
export interface SupplyChoice {
|
||||||
supply: string;
|
supply: string;
|
||||||
@@ -291,6 +288,8 @@ interface DraftSettings {
|
|||||||
wood_chipping_volume_m3: number | null;
|
wood_chipping_volume_m3: number | null;
|
||||||
// 자재별 관급/사급 — 표 안에서 줄마다 고른 값.
|
// 자재별 관급/사급 — 표 안에서 줄마다 고른 값.
|
||||||
material_supply: Record<string, SupplyChoice>;
|
material_supply: Record<string, SupplyChoice>;
|
||||||
|
// 갈래별 토량환산계수(다짐) — `compacted` 가 `null` 이면 「안 고름」이라 기본값이 선다.
|
||||||
|
conversion_factors: Record<string, FactorDraft>;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,18 +382,18 @@ function buildQuantitySidePanel(
|
|||||||
panel.append(devUnlockRow(projectId, reload));
|
panel.append(devUnlockRow(projectId, reload));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 계수는 서버 상수가 유일한 정의처다 — 화면은 보여 주기만 하고 값을 다시 적지 않는다.
|
|
||||||
panel.append(field(L("B08_Quantity_Side_Method"), L("B08_Quantity_Side_Method_Value")));
|
panel.append(field(L("B08_Quantity_Side_Method"), L("B08_Quantity_Side_Method_Value")));
|
||||||
const entries = Object.entries(table?.conversion_factors ?? {});
|
// 토량환산계수 — **고를 수 있는 값**이다(오솔길 대조 06절 3번). 기본값 정의처는 서버 한 곳이고,
|
||||||
if (entries.length) {
|
// 화면은 고른 값만 보낸다. 유토곡선·운반표·기초단가가 같이 읽는다는 안내도 그 칸이 낸다.
|
||||||
panel.append(field(L("B08_Quantity_Side_Factors"), ""));
|
const factorFields = renderConversionFactorFields(
|
||||||
for (const [kind, value] of entries) {
|
table?.conversion_factor_choices,
|
||||||
// ⚠ 서버 키(`soil`·`ripping_rock`·`blasting_rock`)를 그대로 내보내지 않는다 —
|
table?.conversion_factor_pumsem_ranges,
|
||||||
// 2026-09-08 ㉕ 화면 통과에서 좌측 세 줄이 개발자 키로 떠 있었다(`soil_guard` 와 같은 병).
|
draft.conversion_factors,
|
||||||
// 모르는 키는 **지어내지 않고** 그대로 보인다.
|
() => {
|
||||||
panel.append(field(groundLabel(kind), String((value as { compacted: number }).compacted)));
|
draft.dirty = true;
|
||||||
}
|
},
|
||||||
}
|
);
|
||||||
|
if (factorFields) panel.append(factorFields);
|
||||||
|
|
||||||
// ── 지반 구성비 — 갈래 수는 프로젝트 세트가 정한다(코드에 안 박음, PLAN 8-13) ──
|
// ── 지반 구성비 — 갈래 수는 프로젝트 세트가 정한다(코드에 안 박음, PLAN 8-13) ──
|
||||||
const classes = [...(table?.summary?.rock_classes ?? [])];
|
const classes = [...(table?.summary?.rock_classes ?? [])];
|
||||||
@@ -887,6 +886,16 @@ export async function renderB08Quantity(root: HTMLElement): Promise<void> {
|
|||||||
...((stored.ancillary_counts ?? {}) as Record<string, number | null>),
|
...((stored.ancillary_counts ?? {}) as Record<string, number | null>),
|
||||||
},
|
},
|
||||||
material_supply: { ...((stored.material_supply ?? {}) as Record<string, SupplyChoice>) },
|
material_supply: { ...((stored.material_supply ?? {}) as Record<string, SupplyChoice>) },
|
||||||
|
// ⚠ 저장분에 있는 갈래만 담는다 — 기본값을 복사해 넣으면 「안 고름」이 사라진다.
|
||||||
|
conversion_factors: Object.fromEntries(
|
||||||
|
Object.entries(stored.conversion_factors_override ?? {}).map(([kind, entry]) => [
|
||||||
|
kind,
|
||||||
|
{
|
||||||
|
compacted: typeof entry?.compacted === "number" ? entry.compacted : null,
|
||||||
|
reason: typeof entry?.reason === "string" ? entry.reason : "",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
dirty: false,
|
dirty: false,
|
||||||
};
|
};
|
||||||
const reload = (): void => {
|
const reload = (): void => {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ from pathlib import Path
|
|||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
from common_util.common_util_json import atomic_write_json
|
from common_util.common_util_json import atomic_write_json
|
||||||
|
from config.config_system_design import (
|
||||||
|
EARTHWORK_CONVERSION_C_RANGES,
|
||||||
|
EARTHWORK_CONVERSION_FACTORS,
|
||||||
|
)
|
||||||
|
|
||||||
SETTINGS_FILENAME = "project_settings.json"
|
SETTINGS_FILENAME = "project_settings.json"
|
||||||
SCHEMA_VERSION = 1
|
SCHEMA_VERSION = 1
|
||||||
@@ -258,6 +262,62 @@ def concrete_placing_method(settings: dict[str, Any]) -> tuple[str, bool]:
|
|||||||
return DEFAULT_CONCRETE_PLACING_METHOD, True
|
return DEFAULT_CONCRETE_PLACING_METHOD, True
|
||||||
|
|
||||||
|
|
||||||
|
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 application_ratio(settings: dict[str, Any], key: str) -> float:
|
def application_ratio(settings: dict[str, Any], key: str) -> float:
|
||||||
"""반영률을 0~1 로. 없으면 100 %(=1.0) — 실무 관측치를 기본값으로 쓰지 않는다."""
|
"""반영률을 0~1 로. 없으면 100 %(=1.0) — 실무 관측치를 기본값으로 쓰지 않는다."""
|
||||||
raw = (settings.get("application_ratios_pct") or {}).get(key, 100)
|
raw = (settings.get("application_ratios_pct") or {}).get(key, 100)
|
||||||
|
|||||||
@@ -372,6 +372,26 @@ EARTHWORK_CONVERSION_FACTORS = {
|
|||||||
"blasting_rock": {"loose": 1.60, "compacted": 1.30},
|
"blasting_rock": {"loose": 1.60, "compacted": 1.30},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 다짐 계수 `C` 를 설계자가 고를 때 보이는 **품셈 범위**. 위 기본값이 선 근거와 같은 표다.
|
||||||
|
# ⚠ **막는 값이 아니다.** 품셈이 「토질 시험하여 적용함을 원칙」이라 하므로 범위 밖 값도
|
||||||
|
# 받되 **사유를 적게** 한다(프로젝트 설정 `conversion_factors_override` 의 `reason`).
|
||||||
|
# ⚠ 기본값을 여기서 다시 적지 않는다 — 기본값의 정의처는 위 상수 한 곳뿐이다.
|
||||||
|
EARTHWORK_CONVERSION_C_RANGES = {
|
||||||
|
"soil": (0.75, 0.90), # 풍화토 0.80~0.90 ~ 점토 0.75~0.90
|
||||||
|
"ripping_rock": (1.00, 1.30), # 풍화암 1.00~1.15 ~ 연암 1.00~1.30
|
||||||
|
"blasting_rock": (1.20, 1.40), # 보통암 1.20~1.40
|
||||||
|
}
|
||||||
|
|
||||||
|
# 품셈 체적변화율표 암종별 `C` 원문 — **화면 안내용**이다.
|
||||||
|
# 우리 3갈래와 1:1 이 아니라(풍화암·연암이 리핑암 하나로 접힌다) 계산에 쓰지 않는다.
|
||||||
|
# 경쟁사(오솔길)가 전 구간 1.0 을 쓰는 것도 풍화암·연암 범위 하한이라 범위 안이다.
|
||||||
|
EARTHWORK_CONVERSION_PUMSEM_C_RANGES = (
|
||||||
|
("풍화암", 1.00, 1.15),
|
||||||
|
("연암", 1.00, 1.30),
|
||||||
|
("보통암", 1.20, 1.40),
|
||||||
|
("경암", 1.30, 1.50),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
# 5-4-5. 토공 운반장비 선정 거리 경계 (B06 유토곡선 운반계획)
|
# 5-4-5. 토공 운반장비 선정 거리 경계 (B06 유토곡선 운반계획)
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""토량환산계수를 「고를 수 있는 값」으로 연 자리 검사 (오솔길 대조 06절 3번).
|
||||||
|
|
||||||
|
못 박는 것 셋
|
||||||
|
① **기본값이 안 바뀐다** — 안 고른 프로젝트는 정본 그대로 선다.
|
||||||
|
② 고른 값은 **토적표·운반표가 같이** 읽는다(계수 정의처는 여전히 한 곳).
|
||||||
|
③ 품셈 범위 **밖도 막지 않는다** — 사유와 함께 선다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from B08_Quantity.B08_Quantity_Engine_EarthworkTable import ( # noqa: E402
|
||||||
|
StationArea,
|
||||||
|
build_rows,
|
||||||
|
)
|
||||||
|
from B08_Quantity.B08_Quantity_Engine_HaulSummary import natural_m3 # noqa: E402
|
||||||
|
from common_util.common_util_project_settings import ( # noqa: E402
|
||||||
|
default_settings,
|
||||||
|
earthwork_conversion_choices,
|
||||||
|
earthwork_conversion_factors,
|
||||||
|
)
|
||||||
|
from config.config_system_design import ( # noqa: E402
|
||||||
|
EARTHWORK_CONVERSION_C_RANGES,
|
||||||
|
EARTHWORK_CONVERSION_FACTORS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_안_고르면_기본값이_그대로() -> None:
|
||||||
|
"""빈 설정·기본 설정 둘 다 정본과 한 글자도 달라지지 않아야 한다."""
|
||||||
|
assert earthwork_conversion_factors({}) == EARTHWORK_CONVERSION_FACTORS
|
||||||
|
assert earthwork_conversion_factors(default_settings()["quantity"]) == (
|
||||||
|
EARTHWORK_CONVERSION_FACTORS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_고른_갈래만_갈아_끼움() -> None:
|
||||||
|
settings = {"conversion_factors_override": {"ripping_rock": {"compacted": 1.0}}}
|
||||||
|
resolved = earthwork_conversion_factors(settings)
|
||||||
|
assert resolved["ripping_rock"]["compacted"] == pytest.approx(1.0)
|
||||||
|
# 나머지 갈래는 정본 그대로다.
|
||||||
|
assert resolved["soil"] == EARTHWORK_CONVERSION_FACTORS["soil"]
|
||||||
|
assert resolved["blasting_rock"] == EARTHWORK_CONVERSION_FACTORS["blasting_rock"]
|
||||||
|
# ⚠ 정본 dict 를 건드리지 않았는가 — 얕은 복사였다면 여기서 걸린다.
|
||||||
|
assert EARTHWORK_CONVERSION_FACTORS["ripping_rock"]["compacted"] == pytest.approx(1.15)
|
||||||
|
|
||||||
|
|
||||||
|
def test_모르는_갈래와_말이_안_되는_값은_버림() -> None:
|
||||||
|
settings = {
|
||||||
|
"conversion_factors_override": {
|
||||||
|
"unknown_rock": {"compacted": 2.0},
|
||||||
|
"soil": {"compacted": 0},
|
||||||
|
"blasting_rock": {"compacted": "많이"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert earthwork_conversion_factors(settings) == EARTHWORK_CONVERSION_FACTORS
|
||||||
|
|
||||||
|
|
||||||
|
def test_토적표가_고른_계수로_섬() -> None:
|
||||||
|
"""오솔길처럼 암 계수 1.0 을 고르면 보정량이 그 값으로 선다."""
|
||||||
|
stations = [
|
||||||
|
StationArea(chainage_m=0.0),
|
||||||
|
StationArea(chainage_m=10.0, cut_rock_area_m2=2.0, cut_rock_kind="ripping_rock"),
|
||||||
|
]
|
||||||
|
factors = earthwork_conversion_factors(
|
||||||
|
{"conversion_factors_override": {"ripping_rock": {"compacted": 1.0}}}
|
||||||
|
)
|
||||||
|
row = build_rows(stations, factors)[1]
|
||||||
|
assert row.cut_rock_volume_m3 == pytest.approx(10.0)
|
||||||
|
assert row.cut_rock_adjusted_m3 == pytest.approx(10.0) # 기본값 1.15 였다면 11.5
|
||||||
|
# 안 주면 기본값 — 같은 측점이 11.5 로 선다.
|
||||||
|
assert build_rows(stations)[1].cut_rock_adjusted_m3 == pytest.approx(11.5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_운반표도_같은_계수를_씀() -> None:
|
||||||
|
"""다짐 → 자연 되돌리기(÷C)도 고른 값으로 돌아야 표끼리 안 갈린다."""
|
||||||
|
factors = earthwork_conversion_factors(
|
||||||
|
{"conversion_factors_override": {"ripping_rock": {"compacted": 1.0}}}
|
||||||
|
)
|
||||||
|
assert natural_m3(11.5, "리핑암", factors) == pytest.approx(11.5)
|
||||||
|
assert natural_m3(11.5, "리핑암") == pytest.approx(11.5 / 1.15)
|
||||||
|
|
||||||
|
|
||||||
|
def test_범위_밖도_막지_않고_사유와_함께_섬() -> None:
|
||||||
|
low, _high = EARTHWORK_CONVERSION_C_RANGES["blasting_rock"]
|
||||||
|
settings = {
|
||||||
|
"conversion_factors_override": {
|
||||||
|
"blasting_rock": {"compacted": low - 0.5, "reason": "토질시험 값"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolved = earthwork_conversion_factors(settings)
|
||||||
|
assert resolved["blasting_rock"]["compacted"] == pytest.approx(low - 0.5)
|
||||||
|
choice = earthwork_conversion_choices(settings)["blasting_rock"]
|
||||||
|
assert choice["chosen"] is True
|
||||||
|
assert choice["in_range"] is False # 밖이라고 말은 하되 값은 그대로 선다.
|
||||||
|
assert choice["reason"] == "토질시험 값"
|
||||||
|
|
||||||
|
|
||||||
|
def test_선택_상태는_기본값과_범위를_함께_냄() -> None:
|
||||||
|
choice = earthwork_conversion_choices({})["ripping_rock"]
|
||||||
|
assert choice["chosen"] is False
|
||||||
|
assert choice["in_range"] is True
|
||||||
|
assert choice["default"] == pytest.approx(1.15)
|
||||||
|
assert choice["range"] == [1.00, 1.30]
|
||||||
@@ -758,6 +758,18 @@ export const ui_locales_b2 = {
|
|||||||
B08_Quantity_Side_Method: ["산출법", "Method"],
|
B08_Quantity_Side_Method: ["산출법", "Method"],
|
||||||
B08_Quantity_Side_Method_Value: ["평균단면적법", "Average end area"],
|
B08_Quantity_Side_Method_Value: ["평균단면적법", "Average end area"],
|
||||||
B08_Quantity_Side_Factors: ["토량환산계수(다짐)", "Conversion factors (compacted)"],
|
B08_Quantity_Side_Factors: ["토량환산계수(다짐)", "Conversion factors (compacted)"],
|
||||||
|
B08_Quantity_Factor_Reach: [
|
||||||
|
"이 계수는 유토곡선·운반표·기초단가에도 같이 닿습니다.",
|
||||||
|
"These factors also feed the mass-haul curve, haul table and basis units.",
|
||||||
|
],
|
||||||
|
B08_Quantity_Factor_Default: ["기본값", "Default"],
|
||||||
|
B08_Quantity_Factor_Range: ["품셈 범위", "Standard range"],
|
||||||
|
B08_Quantity_Factor_OutOfRange: [
|
||||||
|
"품셈 범위 밖입니다 — 사유를 적어 주세요(토질시험 값일 수 있습니다).",
|
||||||
|
"Outside the standard range — please note why (it may be a soil-test value).",
|
||||||
|
],
|
||||||
|
B08_Quantity_Factor_Reason: ["사유", "Reason"],
|
||||||
|
B08_Quantity_Factor_Pumsem: ["품셈 체적변화율(C)", "Standard volume-change (C)"],
|
||||||
|
|
||||||
/* --- B09_Estimation 원가계산 --- */
|
/* --- B09_Estimation 원가계산 --- */
|
||||||
B09_Estimation_Title: ["원가계산", "Cost Estimate"],
|
B09_Estimation_Title: ["원가계산", "Cost Estimate"],
|
||||||
|
|||||||
Reference in New Issue
Block a user