From 24fc6ec0901c72459469e2c9a83deff7032cb8a4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 01:14:34 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=ED=99=94=EB=A9=B4=EC=97=90=20?= =?UTF-8?q?=EC=95=88=20=EB=B3=B4=EC=9D=B4=EB=8D=98=20=ED=99=95=EC=A0=95=20?= =?UTF-8?q?=EB=8C=80=EA=B8=B0=20=EC=85=8B=EC=9D=84=20=EB=93=9C=EB=9F=AC?= =?UTF-8?q?=EB=83=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 목록에만 있고 화면에 없으면 사용자는 그것이 잠정인 줄도 모름. 셋 다 「지금 어떤 값으로 돌고 있는지 + 왜 잠정인지」를 함께 보임. - 콘크리트 타설 방식을 좌측 패널 칸으로 냄. ⚠ 금액에 바로 걸리는 값이라 기본값으로 돌고 있으면 「기본값 「레디믹스트」로 계산 중 — 아직 안 정한 값」 안내를 띄움. 설정 기본을 None 으로 바꿔 「안 정함」과 「일부러 레디믹스트를 고른 것」을 가름 — 값을 미리 넣으면 그 구별이 사라짐. 되돌리기도 됨. - 물구멍 근거에 잠정값을 적음 — 「관 Ø 미정(법 3~6㎝ / 실무 Ø50) · 간격 2.0㎡당 1개소(법 2~3㎡당 1개소 이상)」. 「미확정」만으로는 무엇을 정해야 하는지 모름. - 준비공의 벌목 줄에 공종 미확정 사유와 후보를 함께 적음(수확베기·단목베기· 위험목 베기 중 어느 것인지 원본이 말하지 않음). 검증 — 전체 580 passed, tsc 오류 0. 화면에서 셋 다 뜨는 것과 타설 방식 저장·되돌리기까지 확인 후 검증으로 바꾼 값은 원래대로 복원. Co-Authored-By: Claude Opus 5 (1M context) --- .../B08_Quantity_Engine_Preparation.py | 6 +++- .../B08_Quantity_Engine_UnitQuantity.py | 5 ++- B08_Quantity/B08_Quantity_Router_Earthwork.py | 16 ++++++++- B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts | 2 ++ .../B08_Quantity_UI_EarthworkGrid_Style.ts | 12 +++++++ B08_Quantity/B08_Quantity_UI_Page.ts | 33 +++++++++++++++++++ common_util/common_util_project_settings.py | 24 ++++++++++++-- ui_template/ui_template_locale_b2.ts | 10 ++++++ 8 files changed, 102 insertions(+), 6 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation.py b/B08_Quantity/B08_Quantity_Engine_Preparation.py index 7a3bf0ae..8c706584 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation.py @@ -55,7 +55,11 @@ def preparation_rows(slope_totals: dict[str, float] | None = None) -> list[dict[ "status": STATUS_COUNTED_ELSEWHERE, # ⚠ 값을 여기서 또 내면 같은 나무를 두 번 벤다. 참고로 면적만 보인다. "reference_amount": tree_area, - "reason": "토공집계의 「지장목제거」로 이미 섬 — 여기서 또 세우면 이중계상", + "reason": ( + "토공집계의 「지장목제거」로 이미 섬 — 여기서 또 세우면 이중계상. " + "⚠ 다만 **공종 미확정** — 품셈 4장이 벌목을 목적별로 갈라(수확베기·단목베기·" + "위험목 베기) 임도 지장목이 어디에 붙는지 원본이 말하지 않음. 지금은 공종코드 없이 감." + ), "work_item_code": None, }, { diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index ce9ff1da..8e7ea4a1 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -238,7 +238,10 @@ def stone_masonry( "m", masonry_area / constants["weep_hole_area_m2"] * constants["weep_hole_length_m"], DESTINATION["물구멍"], - "돌쌓기 ÷ 2㎡/개소 × 0.5 m/개소 (관 규격 미확정)", + # ⚠ 「미확정」만 적으면 사용자가 무엇을 정해야 하는지 모른다 — + # **지금 무슨 값으로 돌고 있는지**를 함께 적는다(원단위 미확보와 같은 방식). + "돌쌓기 ÷ 2㎡/개소 × 0.5 m/개소 · ⚠ 잠정: 관 Ø 미정(법 3~6㎝ / 실무 Ø50) ·" + " 간격 2.0㎡당 1개소(법 2~3㎡당 1개소 이상)", ) ) diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index a99b8f81..db9ab2f0 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -36,8 +36,10 @@ from B08_Quantity.B08_Quantity_Engine_HaulSummary import summary_input_rows from B08_Quantity.B08_Quantity_Engine_SlopeArea import build_table as build_slope_table from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slopes from common_util.common_util_project_settings import ( + CONCRETE_PLACING_METHODS, ROCK_METHODS, application_ratio, + concrete_placing_method, quantity_settings, rock_classes, save_section, @@ -99,6 +101,9 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: table["preparation"] = build_preparation_table( slope.get("totals") or {}, await _route_structures(project_id) ) + method, method_is_default = concrete_placing_method(settings) + # ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다. + table["concrete_placing"] = {"method": method, "is_default": method_is_default} table["settings"] = settings table["project_root_known"] = project_root is not None table["route_id"] = route_id @@ -164,6 +169,8 @@ class QuantitySettingsBody(BaseModel): # 자재별 관급/사급 — `{자재명: {"supply": …, "install_by": …}}`. # 표 안에서 줄마다 고른 값이 여기로 온다(2026-09-07 확정). material_supply: dict[str, Any] | None = None + # 콘크리트 타설 방식. `""` 는 「안 정함」으로 되돌리는 뜻이라 서버가 None 으로 만든다. + concrete_placing_method: str | None = None @router.put("/{project_id}/quantity/settings") @@ -183,6 +190,10 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) - content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."}, ) values = {key: value for key, value in body.model_dump().items() if value is not None} + 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 "rock_methods" in values: # 「안 정함」(빈 값)은 저장하지 않는다 — 정한 것과 구별이 안 된다. 통째로 갈아 끼우므로 # 여기서 버리면 그 갈래는 미지정으로 돌아간다. @@ -195,7 +206,10 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) - # ⚠ 고른 값을 **되돌릴 수 있어야** 하는 칸은 통째로 갈아 끼운다 — 병합이면 # 「안 정함」으로 되돌아가지 않는다(2026-09-07 화면에서 걸린 자리). saved = await asyncio.to_thread( - _save_quantity, root, values, ("rock_methods", "material_supply") + _save_quantity, + root, + values, + ("rock_methods", "material_supply", "concrete_placing_method"), ) except Exception: logger.exception("B08 설정 저장 실패(쓰기): project_id=%s", project_id) diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts index ade6b38f..9825c0e7 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -63,6 +63,8 @@ export interface QuantitySettings { rock_methods?: Record; /** 자재별 관급/사급 — 표 안에서 줄마다 고른 값. */ material_supply?: Record; + /** 콘크리트 타설 방식. `null`·없음이면 **아직 안 정한 것**이고 화면이 기본값 안내를 띄운다. */ + concrete_placing_method?: string | null; } export interface EarthworkTable { diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid_Style.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid_Style.ts index c3d9dcbb..fb8fe14d 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid_Style.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid_Style.ts @@ -32,6 +32,18 @@ const CSS = ` box-shadow: inset 2px 0 0 var(--color-accent, #6c8ebf); } +/* 잠정값 안내 — 금액에 걸리는 값이 조용히 기본으로 돌지 않게. */ +.b08-quantity__notice { + margin: 0.35rem 0 0; + padding: 0.4rem 0.5rem; + font-size: 0.85em; + line-height: 1.4; + color: var(--color-text); + background: var(--color-surface-raised); + border-left: 3px solid var(--color-warning, #c08a3e); + border-radius: 3px; +} + .b08-grid__caption { margin: 0; font-size: 12px; diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index 65dbbb1c..896e7d2e 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -83,6 +83,7 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr // 병합해 옛 값이 남아 되돌릴 길이 없다(화면에서 걸린 자리). 빈 값은 서버가 버린다. rock_methods: draft.rock_methods, material_supply: draft.material_supply, + concrete_placing_method: draft.concrete_placing_method, }), }, ); @@ -172,6 +173,8 @@ interface DraftSettings { application_ratios_pct: Record; // 갈래별 시공법 — `""` 는 「안 정함」이고 저장에서 빠진다. rock_methods: Record; + // 콘크리트 타설 방식 — `""` 는 「안 정함」이고 저장에서 지워진다. + concrete_placing_method: string; // 자재별 관급/사급 — 표 안에서 줄마다 고른 값. material_supply: Record; dirty: boolean; @@ -252,6 +255,35 @@ function buildQuantitySidePanel( } } + // ── 콘크리트 타설 방식 — ⚠ **금액에 바로 걸리는 값**이라 잠정임을 조용히 두지 않는다 ── + panel.append(field(L("B08_Quantity_Side_Placing"), "")); + panel.append( + selectField( + L("B08_Quantity_Side_Placing_Label"), + draft.concrete_placing_method, + [ + { value: "", label: L("B08_Quantity_Placing_Unset") }, + { value: "ready_mixed", label: L("B08_Quantity_Placing_Ready") }, + { value: "machine_mixed", label: L("B08_Quantity_Placing_Machine") }, + { value: "hand_mixed", label: L("B08_Quantity_Placing_Hand") }, + ], + (value) => { + draft.concrete_placing_method = value; + draft.dirty = true; + }, + ), + ); + const placing = ( + table as unknown as { concrete_placing?: { method: string; is_default: boolean } } + ).concrete_placing; + if (placing?.is_default) { + // 「확인 필요」만 있으면 무엇을 정해야 하는지 모른다 — **지금 무엇으로 돌고 있는지**를 함께 적는다. + const notice = document.createElement("p"); + notice.className = "b08-quantity__notice"; + notice.textContent = L("B08_Quantity_Placing_Default_Notice"); + panel.append(notice); + } + const saveButton = createButton({ label: L("B08_Quantity_Btn_Save"), variant: "ghost", @@ -431,6 +463,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise { rock_ratios_pct: { ...(stored.rock_ratios_pct ?? {}) }, application_ratios_pct: { ...(stored.application_ratios_pct ?? {}) }, rock_methods: { ...((stored.rock_methods ?? {}) as Record) }, + concrete_placing_method: (stored.concrete_placing_method as string) ?? "", material_supply: { ...((stored.material_supply ?? {}) as Record) }, dirty: false, }; diff --git a/common_util/common_util_project_settings.py b/common_util/common_util_project_settings.py index c274cf4d..25a5dd3a 100644 --- a/common_util/common_util_project_settings.py +++ b/common_util/common_util_project_settings.py @@ -94,9 +94,10 @@ def default_settings() -> dict[str, Any]: # 화면에 드러난다 — 사급으로 조용히 넘기면 관급자재대가 새 나간다. "material_supply": {}, # 콘크리트 타설 방식 — `ready_mixed`(FP-12-01-01) / `machine_mixed`(-02) / - # `hand_mixed`(-03). **설계 판단**이라 사용자가 고르는 값이고, 기본은 실무가 - # 쓰는 레디믹스트로 둔다. ⚠ 잠정이며 사용자 확정 대기 항목이다. - "concrete_placing_method": "ready_mixed", + # `hand_mixed`(-03). **설계 판단**이라 사용자가 고른다. + # ⚠ 기본은 `None` — 「안 정함」과 「일부러 레디믹스트를 고른 것」을 갈라야 + # 화면이 「기본값 적용 중」을 정직하게 띄운다. 값을 미리 넣으면 그 구별이 사라진다. + "concrete_placing_method": None, "dataset_versions": {}, }, "estimation": { @@ -189,6 +190,23 @@ def rock_classes(settings: dict[str, Any]) -> list[str]: 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 + + 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/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 73dae8c7..2073135f 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -628,6 +628,16 @@ export const ui_locales_b2 = { B08_Quantity_Method_Unset: ["안 정함", "Not set"], B08_Quantity_Method_Ripping: ["긁어내기(암절취)", "Ripping"], B08_Quantity_Method_Blasting: ["터뜨리기(발파암)", "Blasting"], + B08_Quantity_Side_Placing: ["콘크리트 타설", "Concrete Placing"], + B08_Quantity_Side_Placing_Label: ["타설 방식", "Method"], + B08_Quantity_Placing_Unset: ["안 정함(기본값 사용)", "Not set (default)"], + B08_Quantity_Placing_Ready: ["레디믹스트", "Ready-mixed"], + B08_Quantity_Placing_Machine: ["기계비빔", "Machine-mixed"], + B08_Quantity_Placing_Hand: ["인력비빔", "Hand-mixed"], + B08_Quantity_Placing_Default_Notice: [ + "⚠ 기본값 「레디믹스트」로 계산 중 — 아직 안 정한 값입니다. 타설 방식에 따라 단가가 갈립니다.", + "⚠ Using the default (ready-mixed) — not yet decided. Unit prices differ by method.", + ], B08_Quantity_Material_Failed: [ "자재총괄을 불러오지 못했습니다.", "Failed to load the material summary.",