From 7bd1dc1e3a0a19a6b7677316fc78023be9f90ec4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 13 Sep 2026 23:33:10 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=EB=8F=84=EC=9E=90=20=ED=95=9C?= =?UTF-8?q?=EA=B3=84=EA=B1=B0=EB=A6=AC=EB=A5=BC=20=EC=82=B0=EC=B6=9C=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=B9=B8=EC=9C=BC=EB=A1=9C=20=E2=80=94=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=2060=20m=20=EC=99=80=20=EA=B7=BC=EA=B1=B0=20?= =?UTF-8?q?=EC=85=8B=EC=9D=84=20=ED=99=94=EB=A9=B4=EC=97=90=20=EA=B0=99?= =?UTF-8?q?=EC=9D=B4=20=EB=B3=B4=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 유토곡선 장비 경계의 도자 한계거리가 config 붙박이였고 설정 칸 haul_limits_m_override 는 아무도 안 읽었음. dozer_haul_limit_m 칸으로 갈음해 B06 유토곡선 문맥·배분·B08 산출 조건이 같은 값을 읽음(common_util 한 곳). 화면에 기본값 60 m · 근거(품셈 8-1-1 · 산림과임업기술 5장 · 실무 EARTH.DAT 전수)를 보이고, 종무대 20 m 는 규정이라 값만 보임. 종무대 이하 값은 저장에서 막음 · 비우면 기본값. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- B06_Section/B06_Section_Router.py | 5 +- B06_Section/B06_Section_Router_HaulPlan.py | 5 +- .../B06_Section_Server_Calc_Prebuild.py | 20 +++++- B08_Quantity/B08_Quantity_Router_Earthwork.py | 18 +++++ B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts | 11 +++ B08_Quantity/B08_Quantity_UI_Page.ts | 29 ++++++++ common_util/common_util_project_settings.py | 39 +++++++++- config/config_system_design.py | 10 +++ .../tester/test_b08_earthwork_summary.py | 3 +- resources/tester/test_haul_limit_setting.py | 72 +++++++++++++++++++ ui_template/ui_template_locale_b2.ts | 7 ++ 11 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 resources/tester/test_haul_limit_setting.py diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index ae4ec6d8..52613824 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -73,14 +73,13 @@ from B06_Section.B06_Section_Schema import ( SectionRegenerateRequest, SectionSummaryResponse, ) -from B06_Section.B06_Section_Server_Calc_Prebuild import conversion_factors_for +from B06_Section.B06_Section_Server_Calc_Prebuild import conversion_factors_for, haul_limits_for 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_surface_confirmation import get_surface_confirmation_params 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_system import ( - EARTHWORK_HAUL_EQUIPMENT_LIMITS_M, FOREST_ROAD_MIN_WIDTH_M, NATURAL_SPOIL_MIN_GROUND_SLOPE, SECTION_VERTICAL_EXAGGERATION, @@ -149,7 +148,7 @@ async def get_section_context(project_id: UUID) -> SectionContextResponse | JSON earthwork_conversion=await conversion_factors_for(project_id), haul_equipment_limits=[ HaulEquipmentLimit(key=key, max_distance_m=limit) - for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M + for key, limit in await haul_limits_for(project_id) ], natural_spoil_min_ground_slope=NATURAL_SPOIL_MIN_GROUND_SLOPE, ) diff --git a/B06_Section/B06_Section_Router_HaulPlan.py b/B06_Section/B06_Section_Router_HaulPlan.py index 4c599456..fcb3c585 100644 --- a/B06_Section/B06_Section_Router_HaulPlan.py +++ b/B06_Section/B06_Section_Router_HaulPlan.py @@ -27,6 +27,7 @@ from B06_Section.B06_Section_Server_Calc_Prebuild import ( _mass_haul_context, conversion_factors_for, haul_inputs_for, + haul_limits_for, ) from common_util.common_util_node_bundle import run_bundle_json @@ -66,6 +67,8 @@ async def compute_haul_plan( haul_inputs = await haul_inputs_for(project_id) # 곡선이 쓰는 계수도 프로젝트가 고른 값으로 — 토적표·운반표와 같은 값이어야 한다. factors = await conversion_factors_for(project_id) + # 장비 거리 경계도 프로젝트 값으로 — 도쟈 한계거리를 고쳤으면 배분이 그 값으로 선다. + limits = await haul_limits_for(project_id) try: output = await asyncio.to_thread( run_bundle_json, @@ -73,7 +76,7 @@ async def compute_haul_plan( _NPM_SCRIPT, { "haul_plan_for": result, - "context": _mass_haul_context(haul_inputs, factors), + "context": _mass_haul_context(haul_inputs, factors, limits), }, ) except Exception: diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 6c0913cd..f87df21b 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -40,6 +40,7 @@ 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_project_settings import ( earthwork_conversion_factors, + haul_equipment_limits, quantity_settings, ) from common_util.common_util_storage import resolve_stored_project_path @@ -98,9 +99,21 @@ async def conversion_factors_for(project_id: Any) -> dict[str, dict[str, float]] return earthwork_conversion_factors(quantity_settings(root)) +async def haul_limits_for(project_id: Any) -> list[tuple[str, float | None]]: + """이 프로젝트가 쓸 운반장비 거리 경계(도쟈 한계거리를 고쳤으면 그 값). 못 읽으면 기본값.""" + 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 list(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M) + return haul_equipment_limits(quantity_settings(root)) + + def _mass_haul_context( haul_inputs: dict[str, Any] | None = None, factors: dict[str, dict[str, float]] | None = None, + limits: list[tuple[str, float | None]] | None = None, ) -> dict[str, Any]: """유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다. @@ -118,9 +131,10 @@ def _mass_haul_context( # 프로젝트가 고른 계수가 있으면 그것, 없으면 정본 기본값. "earthwork_conversion": factors or EARTHWORK_CONVERSION_FACTORS, "natural_spoil_min_ground_slope": NATURAL_SPOIL_MIN_GROUND_SLOPE, + # 프로젝트가 도쟈 한계거리를 고쳤으면 그 값, 없으면 정본 기본값. "haul_equipment_limits": [ {"key": key, "max_distance_m": limit} - for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M + for key, limit in (limits or EARTHWORK_HAUL_EQUIPMENT_LIMITS_M) ], # ⚠ B08 이 아직 이 값을 내지 않는다(2026-09-09) — 그때까지 `None`(아직 안 옴)이다. # 값을 내기 시작하면 여기에 실어 주기만 하면 통로가 이어진다. @@ -220,7 +234,9 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: { "detail": detail, "context": _mass_haul_context( - haul_inputs, earthwork_conversion_factors(quantity_settings(project_root)) + haul_inputs, + earthwork_conversion_factors(quantity_settings(project_root)), + haul_equipment_limits(quantity_settings(project_root)), ), }, ) diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index ad881d51..d64f9023 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -48,6 +48,7 @@ from common_util.common_util_project_settings import ( concrete_placing_method, earthwork_conversion_choices, earthwork_conversion_factors, + haul_limit_choice, quantity_settings, rock_classes, save_section, @@ -57,6 +58,7 @@ from config.config_db import run_with_connection from config.config_system_design import ( EARTHWORK_CONVERSION_FACTORS, EARTHWORK_CONVERSION_PUMSEM_C_RANGES, + EARTHWORK_HAUL_EQUIPMENT_LIMITS_M, ) logger = logging.getLogger(__name__) @@ -90,6 +92,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: table = build_table(_stations(designs), factors) # 화면이 「무엇을 골랐나 · 품셈 범위 안인가」를 보이는 데 쓴다. 계산에는 안 들어간다. table["conversion_factor_choices"] = earthwork_conversion_choices(settings) + # 도쟈 한계거리 — 지금 값·기본값·근거를 함께 보인다(유토곡선 장비 경계, 2026-09-13 판정). + table["haul_limit_choice"] = haul_limit_choice(settings) # 품셈 암종별 범위 — **화면 안내용**이다. 정의처가 서버 한 곳이라 내려보내 쓴다 # (프론트에 다시 적으면 두 벌이 되어 갈린다). table["conversion_factor_pumsem_ranges"] = [ @@ -445,6 +449,8 @@ class QuantitySettingsBody(BaseModel): # ⚠ **기본값을 복사해 넣지 않는다** — 안 고른 갈래는 키가 없어야 정본이 선다. # 빈 dict 는 「전부 기본값으로 되돌림」이라 통째로 갈아 끼운다. conversion_factors_override: dict[str, Any] | None = None + # 도쟈 한계거리(m) — `None` 은 기본값(60 m). 종무대 20 m 보다 커야 한다(도쟈 몫이 사라짐). + dozer_haul_limit_m: float | None = None #: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**. @@ -456,6 +462,7 @@ NULLABLE_SETTING_KEYS = ( "rubble_base_thickness_m", "topsoil_haul_distance_m", "wood_chipping_volume_m3", + "dozer_haul_limit_m", ) @@ -482,6 +489,17 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) - for key in NULLABLE_SETTING_KEYS: if key in body.model_fields_set: values[key] = getattr(body, key) + 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 JSONResponse( + status_code=400, + content={ + "status": "error", + "message": f"도쟈 한계거리는 종무대 {free_haul:g} m 보다 커야 합니다.", + }, + ) if "concrete_placing_method" in values: method = values["concrete_placing_method"] # 「안 정함」으로 되돌릴 수 있어야 한다 — 빈 값이면 지운다(8-22 ② 와 같은 자리). diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts index ac19b7b3..81de4af1 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -92,6 +92,8 @@ export interface QuantitySettings { structure_trench_water?: string | null; /** 표토 운반거리(m) — 별표2 가 요구하는 운반·적치의 밑수. 비면 그 줄이 막힌다. */ topsoil_haul_distance_m?: number | null; + /** 도쟈 한계거리(m) — `null`·없음이면 기본값(서버 정본 60 m)이 선다. */ + dozer_haul_limit_m?: number | null; /** 부대시설 개소 — `{항목키: 개소}`. ⚠ 산식으로 만들지 않는다(확정 13). */ ancillary_counts?: Record; /** 임목축적 등급 — `"소림"`·`"중림"`·`"밀림"`(품셈 9-21 [주]①). 본수가 아니라 축적이다. */ @@ -141,6 +143,15 @@ export interface EarthworkTable { conversion_factor_choices?: Record; /** 품셈 암종별 범위(안내용). 정의처가 서버라 내려받아 보인다. */ conversion_factor_pumsem_ranges?: PumsemRange[]; + /** 도쟈 한계거리 — 지금 값·기본값·근거(서버가 정본). 종무대 20 m 는 규정이라 값만 보인다. */ + haul_limit_choice?: { + value: number; + default: number; + chosen: boolean; + basis: string; + free_haul_m: number; + free_haul_basis: string; + }; /** 근거 사전 — ⚠ **개발환경에서만** 실려 온다. 운영에서는 칸 자체가 없다. */ provenance?: ProvenancePayload; } diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index bdc66d2c..463e2e3c 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -110,6 +110,8 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr ancillary_counts: draft.ancillary_counts, // 토량환산계수 — 고른 갈래만 담긴다. 빈 dict 는 「전부 기본값으로 되돌림」이다. conversion_factors_override: conversionOverridePayload(draft.conversion_factors), + // 도쟈 한계거리 — `null` 도 보낸다(기본값으로 되돌리는 길). + dozer_haul_limit_m: draft.dozer_haul_limit_m, }), }, ); @@ -295,6 +297,8 @@ interface DraftSettings { material_supply: Record; // 갈래별 토량환산계수(다짐) — `compacted` 가 `null` 이면 「안 고름」이라 기본값이 선다. conversion_factors: Record; + // 도쟈 한계거리(m) — `null` 은 기본값(서버 정본 60 m)이 선다. + dozer_haul_limit_m: number | null; dirty: boolean; } @@ -403,6 +407,30 @@ function buildQuantitySidePanel( ); if (factorFields) panel.append(factorFields); + // ── 운반장비 거리 경계 — 도쟈 한계거리는 설계 조건이라 칸으로(2026-09-13 판정) ── + // ⚠ 기본값과 근거를 **서버가 준 그대로** 옆에 보인다 — 「이 값이 어디서 왔나」가 화면에 있어야 함. + const haul = table?.haul_limit_choice; + if (haul) { + panel.append(field(L("B08_Quantity_Side_HaulLimits"), "")); + const dozer = optionalNumberField( + L("B08_Quantity_Side_DozerLimit_Label"), + draft.dozer_haul_limit_m, + "5", + (value) => { + draft.dozer_haul_limit_m = value; + draft.dirty = true; + }, + ); + const input = dozer.querySelector("input"); + if (input) input.placeholder = String(haul.default); + panel.append(dozer); + panel.append(hintRow(`${L("B08_Quantity_Factor_Default")} ${haul.default} m — ${haul.basis}`)); + panel.append( + hintRow(`${L("B08_Quantity_Side_FreeHaul")} ${haul.free_haul_m} m — ${haul.free_haul_basis}`), + ); + panel.append(hintRow(L("B08_Quantity_Side_HaulLimits_Reach"))); + } + // ── 지반 구성비 — 갈래 수는 프로젝트 세트가 정한다(코드에 안 박음, PLAN 8-13) ── const classes = [...(table?.summary?.rock_classes ?? [])]; // ⚠ 비율을 아직 안 넣었으면 집계가 **「암」 한 줄**로 나온다(갈래로 안 갈림). 그 줄에도 @@ -940,6 +968,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise { }, ]), ), + dozer_haul_limit_m: (stored.dozer_haul_limit_m as number | null) ?? null, dirty: false, }; const reload = (): void => { diff --git a/common_util/common_util_project_settings.py b/common_util/common_util_project_settings.py index 74d0a170..811d2a50 100644 --- a/common_util/common_util_project_settings.py +++ b/common_util/common_util_project_settings.py @@ -39,6 +39,8 @@ 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" @@ -90,7 +92,10 @@ def default_settings() -> dict[str, Any]: # 「시공법 미지정」으로 드러난다(2026-09-07 일감 9 에서 드러난 자리). "rock_methods": {}, "conversion_factors_override": None, - "haul_limits_m_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"}}`. @@ -318,6 +323,38 @@ def earthwork_conversion_choices(settings: dict[str, Any]) -> dict[str, dict[str 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) diff --git a/config/config_system_design.py b/config/config_system_design.py index 7f6d6fa6..3342217f 100644 --- a/config/config_system_design.py +++ b/config/config_system_design.py @@ -422,6 +422,16 @@ EARTHWORK_HAUL_EQUIPMENT_LIMITS_M = ( ("dozer", 60.0), ("dump_truck", None), ) +# 경계값 근거 — **화면이 기본값 옆에 그대로 보인다**(2026-09-13 브레인 판정 · PLAN 10장). +# ⚠ 성격이 다르다: 종무대 20 m 는 품셈 규정이라 칸을 안 두고, 도쟈 60 m 는 설계 조건이라 +# 프로젝트 설정 `dozer_haul_limit_m` 으로 바꿀 수 있다. +EARTHWORK_HAUL_LIMIT_BASIS = { + "free_haul": "품셈 1-2-7 「소운반 20 m 이내는 품에 포함」 — 규정이라 안 바꿈", + "dozer": ( + "근거 셋 일치 — 표준품셈 8-1-1(60 m 이하 도저) · 산림과임업기술 5장 p.441·478" + "(60 m 이하 도저 / 초과 덤프) · 실무 오솔길 EARTH.DAT 6개 공사지 전부 60 m" + ), +} # ───────────────────────────────────────────────────────────────────────── diff --git a/resources/tester/test_b08_earthwork_summary.py b/resources/tester/test_b08_earthwork_summary.py index 6e73018a..e0d4708d 100644 --- a/resources/tester/test_b08_earthwork_summary.py +++ b/resources/tester/test_b08_earthwork_summary.py @@ -239,7 +239,8 @@ def test_기본설정_모양(tmp_path: Path) -> None: quantity = settings["quantity"] # override 는 기본이 None — 「안 정했으면 config 정본을 쓴다」는 뜻. assert quantity["conversion_factors_override"] is None - assert quantity["haul_limits_m_override"] is None + # 도쟈 한계거리 — 안 읽히던 `haul_limits_m_override` 를 갈음(2026-09-13). None 이면 기본 60 m. + assert quantity["dozer_haul_limit_m"] is None # 반영률 기본 100. 실무 관측 80/50/80 을 넣지 않는다. assert quantity["application_ratios_pct"] == {key: 100 for key in APPLICATION_RATIO_KEYS} # estimation 은 자리만 — 채우는 것은 B09 몫. diff --git a/resources/tester/test_haul_limit_setting.py b/resources/tester/test_haul_limit_setting.py new file mode 100644 index 00000000..8890785f --- /dev/null +++ b/resources/tester/test_haul_limit_setting.py @@ -0,0 +1,72 @@ +"""도쟈 한계거리 칸 (PLAN 5장 · 2026-09-13 판정 「도자 60 m 확정 — 설계 조건이라 칸으로」). + +지키는 것 + ① 안 넣으면 정본 60 m · 넣으면 유토곡선 장비 경계가 그 값 · 종무대 20 m 는 규정이라 안 바뀜 + ② 화면이 기본값과 근거(근거 셋 일치)를 함께 받음 + ③ 종무대 이하 값은 저장에서 막음(도쟈 몫이 사라짐) · null 로 기본값 되돌림 + ④ B06 유토곡선 계산 문맥이 프로젝트 값을 실음 +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +import B08_Quantity.B08_Quantity_Router_Earthwork as earthwork_router # noqa: E402 +from B06_Section.B06_Section_Server_Calc_Prebuild import _mass_haul_context # noqa: E402 +from common_util.common_util_project_settings import ( # noqa: E402 + haul_equipment_limits, + haul_limit_choice, + load_settings, +) + +PROJECT_ID = "55555555-5555-5555-5555-555555555555" + + +def test_기본은_60m_넣으면_그_값_종무대는_그대로() -> None: + assert haul_equipment_limits({}) == [("free_haul", 20.0), ("dozer", 60.0), ("dump_truck", None)] + assert dict(haul_equipment_limits({"dozer_haul_limit_m": 70}))["dozer"] == 70.0 + assert dict(haul_equipment_limits({"dozer_haul_limit_m": 15}))["dozer"] == 60.0 # 종무대 이하 + choice = haul_limit_choice({"dozer_haul_limit_m": 70}) + assert (choice["value"], choice["default"], choice["chosen"]) == (70.0, 60.0, True) + assert "8-1-1" in choice["basis"] and "EARTH.DAT" in choice["basis"] + assert "1-2-7" in choice["free_haul_basis"] + + +def test_유토곡선_문맥이_프로젝트_경계를_싣는다() -> None: + limits = haul_equipment_limits({"dozer_haul_limit_m": 80}) + context = _mass_haul_context(None, None, limits) + assert {row["key"]: row["max_distance_m"] for row in context["haul_equipment_limits"]} == { + "free_haul": 20.0, + "dozer": 80.0, + "dump_truck": None, + } + assert _mass_haul_context()["haul_equipment_limits"][1]["max_distance_m"] == 60.0 + + +@pytest.fixture() +def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient: + async def fake_run(func, *args): + return "project" + + monkeypatch.setattr(earthwork_router, "run_with_connection", fake_run) + monkeypatch.setattr(earthwork_router, "resolve_stored_project_path", lambda _p: str(tmp_path)) + app = FastAPI() + app.include_router(earthwork_router.router) + return TestClient(app) + + +def test_저장은_종무대_이하를_막고_null_로_되돌린다(client: TestClient, tmp_path: Path) -> None: + url = f"/api/projects/{PROJECT_ID}/quantity/settings" + assert client.put(url, json={"dozer_haul_limit_m": 20}).status_code == 400 + assert client.put(url, json={"dozer_haul_limit_m": 70}).status_code == 200 + assert load_settings(tmp_path)["quantity"]["dozer_haul_limit_m"] == 70 + assert client.put(url, json={"dozer_haul_limit_m": None}).status_code == 200 + assert load_settings(tmp_path)["quantity"]["dozer_haul_limit_m"] is None diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 44baf8b5..d3f60bfe 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -765,6 +765,13 @@ export const ui_locales_b2 = { "These factors also feed the mass-haul curve, haul table and basis units.", ], B08_Quantity_Factor_Default: ["기본값", "Default"], + B08_Quantity_Side_HaulLimits: ["운반장비 거리 경계", "Haul equipment distance limits"], + B08_Quantity_Side_DozerLimit_Label: ["도쟈 한계거리(m)", "Dozer haul limit (m)"], + B08_Quantity_Side_FreeHaul: ["종무대(바꾸지 않음)", "Free haul (fixed)"], + B08_Quantity_Side_HaulLimits_Reach: [ + "유토곡선 운반 배분(무대·도쟈·덤프)에 닿습니다 — 바꾼 뒤 종횡단에서 다시 저장해야 운반표가 따라옵니다.", + "Feeds the mass-haul split (free/dozer/dump) — re-save the profile/cross sections for the haul table to follow.", + ], B08_Quantity_Factor_Range: ["품셈 범위", "Standard range"], B08_Quantity_Factor_OutOfRange: [ "품셈 범위 밖입니다 — 사유를 적어 주세요(토질시험 값일 수 있습니다).",