From 6c61f5d5ac7677690632a1d71586bb3ba918b6cc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 15:39:15 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=EC=82=B0=EC=B6=9C=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EB=A9=B4=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EC=B9=B8=20?= =?UTF-8?q?=EB=84=B7=20=E2=80=94=20=EC=A0=88=ED=86=A0=EB=A9=B4=20=ED=86=A0?= =?UTF-8?q?=EC=A7=88=20=C2=B7=20=EC=84=B1=ED=86=A0=EB=A9=B4=20=EC=8B=9C?= =?UTF-8?q?=EA=B3=B5=C2=B7=ED=86=A0=EC=A7=88=20=EA=B3=A0=EB=A5=B4=EA=B8=B0?= =?UTF-8?q?(=EC=84=9C=EB=B2=84=20=EC=84=A0=ED=83=9D=EC=A7=80=20=EA=B7=B8?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=C2=B7=20=EC=A0=9C=EC=95=88=EA=B0=92=20?= =?UTF-8?q?=EC=97=86=EC=9D=8C)=20+=20=EB=A9=B4=EC=A0=81=20=EB=8D=AE?= =?UTF-8?q?=EC=96=B4=EC=93=B0=EA=B8=B0=20=EB=91=98(=EB=B9=84=EC=9A=B0?= =?UTF-8?q?=EB=A9=B4=20=ED=8C=8C=EC=A2=85=20=EB=A9=B4=EC=A0=81)=20=C2=B7?= =?UTF-8?q?=20=EB=A9=94=EC=9D=B8=20ee9368ec=20=EC=A7=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배정 프로젝트: 절토면 토질을 고르고 [저장] → 내역 3-8-2 사유 「입력이 필요합니다」 → 「단가가 일부만 — 공기압축기(이동식) 3.5」 · 「안 정함」으로 되돌려 저장(네 칸 빈 값) Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts | 7 ++ B08_Quantity/B08_Quantity_UI_Page.ts | 23 +++++ .../B08_Quantity_UI_Side_FaceDressing.ts | 95 +++++++++++++++++++ .../test_b08_face_dressing_fields_ui.py | 32 +++++++ ui_template/ui_template_locale_b2.ts | 11 +++ 5 files changed, 168 insertions(+) create mode 100644 B08_Quantity/B08_Quantity_UI_Side_FaceDressing.ts create mode 100644 resources/tester/test_b08_face_dressing_fields_ui.py diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts index 7d5e3a51..0f0c20d1 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -100,6 +100,11 @@ export interface QuantitySettings { ancillary_counts?: Record; /** 임목축적 등급 — `"소림"`·`"중림"`·`"밀림"`(품셈 9-21 [주]①). 본수가 아니라 축적이다. */ stand_volume_class?: string | null; + /** 면고르기 갈래(서버 선택지 안) · 면적 덮어쓰기(㎡, 없음 = 파종 면적). */ + face_dressing_cut_class?: string | null; + face_dressing_fill_class?: string | null; + face_dressing_fill_area_m2?: number | null; + face_dressing_cut_area_m2?: number | null; /** 표토제거 대상 — 없음이면 기본(노면 + 절토) · `"road_only"` 는 노면만. */ topsoil_target?: string | null; /** 규준틀 개소당 재료 — `{자재명: 수량}`. 비우면 제안값(실무 관측)이 선다. */ @@ -155,6 +160,8 @@ export interface EarthworkTable { settings?: QuantitySettings; /** 갈래별 토량환산계수 선택 상태 — 산출 조건 패널이 그린다. */ conversion_factor_choices?: Record; + /** 면고르기 갈래 선택지 — 품셈 9-19-1 원문 표 두 벌(서버 한 곳). 화면은 그대로 보임. */ + face_dressing_choices?: { cut: string[]; fill: string[] }; /** 품셈 암종별 범위(안내용). 정의처가 서버라 내려받아 보인다. */ conversion_factor_pumsem_ranges?: PumsemRange[]; /** 도쟈 한계거리 — 지금 값·기본값·근거(서버가 정본). 종무대 20 m 는 규정이라 값만 보인다. */ diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index 64fabd9d..cbbde4fd 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -41,6 +41,7 @@ import { import { renderStructureSheets } from "./B08_Quantity_UI_StructureSheet"; import { renderStructureSummary } from "./B08_Quantity_UI_StructureSummary"; import { appendTreeWasteFields } from "./B08_Quantity_UI_Side_TreeWaste"; +import { appendFaceDressingFields } from "./B08_Quantity_UI_Side_FaceDressing"; import { appendBenchCutFields } from "./B08_Quantity_UI_Side_BenchCut"; /** locale 헬퍼 */ @@ -108,6 +109,11 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr // `""` 는 기본(노면 + 절토)으로 되돌림 — 서버가 None 으로 둔다. topsoil_target: draft.topsoil_target, stand_volume_class: draft.stand_volume_class, + // 면고르기 — 갈래 `""`·면적 `null` 도 그대로(「안 정함」·「파종 면적 그대로」로 되돌리는 길). + face_dressing_cut_class: draft.face_dressing_cut_class, + face_dressing_fill_class: draft.face_dressing_fill_class, + face_dressing_fill_area_m2: draft.face_dressing_fill_area_m2, + face_dressing_cut_area_m2: draft.face_dressing_cut_area_m2, frame_material: draft.frame_material, wood_chipping_enabled: draft.wood_chipping_enabled, wood_chipping_volume_m3: draft.wood_chipping_volume_m3, @@ -301,6 +307,11 @@ interface DraftSettings { ancillary_counts: Record; // 임목축적 등급 — "소림"·"중림"·"밀림". ⚠ 본수가 아니라 축적이다(품셈 9-21 [주]①). stand_volume_class: string; + // 면고르기 — 갈래 둘(서버 선택지) · 면적 덮어쓰기 둘(`null` = 파종 면적). 칸은 `_Side_FaceDressing`. + face_dressing_cut_class: string; + face_dressing_fill_class: string; + face_dressing_fill_area_m2: number | null; + face_dressing_cut_area_m2: number | null; // 규준틀 개소당 재료 — 비우면 **제안값(실무 관측)**이 선다. 값이 아니라 「고칠 수 있음」이 요점. frame_material: Record; // 임목파쇄 — **기본 꺼짐**(확정 5차 5번). 켜야 줄이 선다. 근주이식은 칸 자체가 없다. @@ -510,6 +521,14 @@ function buildQuantitySidePanel( } } + // ── 면고르기 — 밑수가 파종 면적이라 반영률 바로 밑. 선택지는 서버 목록 그대로(칸은 따로 뺀 파일). + appendFaceDressingFields(panel, draft, table?.face_dressing_choices, { + field, + optionalNumberField, + selectField, + hintRow, + }); + // ── 표토 두께 — 표토 운반 부피(제거 ㎡ × T)에 씀 · 제거 줄은 ㎡ 라 안 곱함(2026-09-13 「실무대로」) ── // ⚠ 2026-09-08 ㉘ 자기 감사: 서버·엔진은 이 값을 받고 있었는데 **화면에 넣을 칸이 없었다.** // 「죽은 칸」(넣어도 안 쓰임)의 반대 짝이다 — 쓰이는데 넣을 데가 없던 자리. @@ -1002,6 +1021,10 @@ export async function renderB08Quantity(root: HTMLElement): Promise { topsoil_haul_distance_m: (stored.topsoil_haul_distance_m as number | null) ?? null, topsoil_target: (stored.topsoil_target as string) ?? "", stand_volume_class: (stored.stand_volume_class as string) ?? "", + face_dressing_cut_class: (stored.face_dressing_cut_class as string) ?? "", + face_dressing_fill_class: (stored.face_dressing_fill_class as string) ?? "", + face_dressing_fill_area_m2: (stored.face_dressing_fill_area_m2 as number | null) ?? null, + face_dressing_cut_area_m2: (stored.face_dressing_cut_area_m2 as number | null) ?? null, frame_material: { ...((stored.frame_material ?? {}) as Record) }, wood_chipping_enabled: Boolean(stored.wood_chipping_enabled), wood_chipping_volume_m3: (stored.wood_chipping_volume_m3 as number | null) ?? null, diff --git a/B08_Quantity/B08_Quantity_UI_Side_FaceDressing.ts b/B08_Quantity/B08_Quantity_UI_Side_FaceDressing.ts new file mode 100644 index 00000000..dfed952a --- /dev/null +++ b/B08_Quantity/B08_Quantity_UI_Side_FaceDressing.ts @@ -0,0 +1,95 @@ +/* ============================================================================= + * B08_Quantity_UI_Side_FaceDressing.ts + * 산출 조건 「면고르기」 구획 — 절토면 토질 · 성토면 시공·토질 고르기 둘 + 면적 덮어쓰기 둘 + * (2026-09-14 브레인 판정 Ⓐ~Ⓓ · 서버 ee9368ec). + * + * 페이지 본문(`B08_Quantity_UI_Page.ts`)이 700줄을 넘어 새 칸을 이 파일로 뺌(CLAUDE.md 4장). + * ⚠ 선택지는 **서버가 내려준 목록 그대로**(`face_dressing_choices`) — 화면에 다시 적지 않음(두 벌 금지). + * ⚠ 제안값 없음(판정 Ⓒ) — 첫 보기가 「안 정함」이고 비면 내역 줄이 입력 사유로 섬. + * ⚠ 면적은 화면이 셈하지 않음 — 비우면 서버가 파종 면적(초류종자살포 × 반영률)을 씀. + * ========================================================================== */ + +import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; +import type { SideFieldHelpers } from "./B08_Quantity_UI_Side_TreeWaste"; + +function L(key: keyof typeof ui_locales): string { + return ui_locales[key][currentLanguageIndex]; +} + +export interface FaceDressingDraft { + /** `""` 는 「안 정함」 — 서버가 선택지 밖 값도 `""` 로 되돌림. */ + face_dressing_cut_class: string; + face_dressing_fill_class: string; + /** `null` 은 「파종 면적 그대로」. */ + face_dressing_fill_area_m2: number | null; + face_dressing_cut_area_m2: number | null; + dirty: boolean; +} + +export interface FaceDressingChoices { + cut: string[]; + fill: string[]; +} + +export function appendFaceDressingFields( + panel: HTMLElement, + draft: FaceDressingDraft, + choices: FaceDressingChoices | undefined, + h: SideFieldHelpers, +): void { + if (!choices) return; + const pick = ( + label: keyof typeof ui_locales, + value: string, + list: string[], + set: (value: string) => void, + ): HTMLElement => + h.selectField( + L(label), + value, + [ + { value: "", label: L("B08_Quantity_FaceDressing_Unset") }, + ...list.map((name) => ({ value: name, label: name })), + ], + (picked) => { + set(picked); + draft.dirty = true; + }, + ); + const area = ( + label: keyof typeof ui_locales, + value: number | null, + set: (v: number | null) => void, + ) => { + const row = h.optionalNumberField(L(label), value, "0.01", (typed) => { + set(typed); + draft.dirty = true; + }); + const input = row.querySelector("input"); + if (input) input.placeholder = L("B08_Quantity_FaceDressing_Area_Placeholder"); + return row; + }; + + panel.append(h.field(L("B08_Quantity_Side_FaceDressing"), "")); + panel.append( + pick("B08_Quantity_FaceDressing_Cut", draft.face_dressing_cut_class, choices.cut, (v) => { + draft.face_dressing_cut_class = v; + }), + ); + panel.append( + pick("B08_Quantity_FaceDressing_Fill", draft.face_dressing_fill_class, choices.fill, (v) => { + draft.face_dressing_fill_class = v; + }), + ); + panel.append( + area("B08_Quantity_FaceDressing_FillArea", draft.face_dressing_fill_area_m2, (v) => { + draft.face_dressing_fill_area_m2 = v; + }), + ); + panel.append( + area("B08_Quantity_FaceDressing_CutArea", draft.face_dressing_cut_area_m2, (v) => { + draft.face_dressing_cut_area_m2 = v; + }), + ); + panel.append(h.hintRow(L("B08_Quantity_Side_FaceDressing_Hint"))); +} diff --git a/resources/tester/test_b08_face_dressing_fields_ui.py b/resources/tester/test_b08_face_dressing_fields_ui.py new file mode 100644 index 00000000..9a7e8e71 --- /dev/null +++ b/resources/tester/test_b08_face_dressing_fields_ui.py @@ -0,0 +1,32 @@ +"""면고르기 산출 조건 칸 넷 — 화면이 서버 선택지를 그대로 쓰고 [저장]에 네 키를 싣는지(2026-09-14 메인 부탁). + +서버(ee9368ec)는 `face_dressing_cut_class`·`face_dressing_fill_class`·`face_dressing_fill_area_m2`· +`face_dressing_cut_area_m2` 를 받고 선택지는 `earthwork-table` 의 `face_dressing_choices` 로 내려줌. +⚠ 선택지를 화면에 다시 적으면 두 벌이 됨 — TS 에 갈래 이름이 없어야 함. +""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +PAGE = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Page.ts").read_text(encoding="utf-8") +SIDE = ROOT / "B08_Quantity" / "B08_Quantity_UI_Side_FaceDressing.ts" +KEYS = ( + "face_dressing_cut_class", + "face_dressing_fill_class", + "face_dressing_fill_area_m2", + "face_dressing_cut_area_m2", +) + + +def test_저장에_네_키를_싣고_불러온다() -> None: + for key in KEYS: + assert f"{key}: draft.{key}" in PAGE, f"[저장]에 {key} 없음" + assert f"{key}: (stored.{key}" in PAGE, f"불러오기에 {key} 없음" + + +def test_선택지는_서버_목록_그대로() -> None: + side = SIDE.read_text(encoding="utf-8") + assert "appendFaceDressingFields(" in PAGE and "face_dressing_choices" in PAGE + assert "choices.cut" in side and "choices.fill" in side + for name in ("모래ㆍ사질토", "성토면 · 인력", "성토면 · 기계"): + assert name not in side and name not in PAGE, f"갈래 이름 {name} 을 화면에 다시 적음" diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 92073029..69cd4fb0 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -746,6 +746,17 @@ export const ui_locales_b2 = { "⚠ 임목폐기물은 분리발주 대상 아님(환경부 질의회신 — 임목폐기물처리 실정보고 p4 · 건설폐기물 업무처리지침: 건설폐기물 아닌 사업장 일반폐기물) — 기본 꺼짐으로 원가계산서 경비에 듭니다. 이 칸은 다른 건설폐기물(위탁처리 100톤 이상 분리발주 대상)이 생길 때 쓰는 칸 — 켜면 총원가 밖에 서고 총공사비에만 더합니다. 5톤 이상은 배출자 신고 대상(폐기물관리법 시행령 제2조8호)", "Tree waste is not subject to separate ordering (Ministry of Environment reply; classified as business general waste) — off by default, inside the expense category. Use this switch for other construction waste (100 t or more). When on, it sits outside total cost. 5 t or more requires a discharge report", ], + B08_Quantity_Side_FaceDressing: ["면고르기", "Face dressing"], + B08_Quantity_FaceDressing_Cut: ["절토면 토질", "Cut face soil"], + B08_Quantity_FaceDressing_Fill: ["성토면 시공·토질", "Fill face method·soil"], + B08_Quantity_FaceDressing_Unset: ["안 정함(금액 안 섬)", "Not set (no amount)"], + B08_Quantity_FaceDressing_FillArea: ["성토면 면적(㎡)", "Fill face area (㎡)"], + B08_Quantity_FaceDressing_CutArea: ["절토면 면적(㎡)", "Cut face area (㎡)"], + B08_Quantity_FaceDressing_Area_Placeholder: ["파종 면적 그대로", "Same as seeding area"], + B08_Quantity_Side_FaceDressing_Hint: [ + "품셈 9-19-1 원문 표 두 벌(절토면 토질 6 · 성토면 시공·토질) — 제안값 없음, 안 고르면 내역 줄이 입력 사유로 섭니다. 면적은 비우면 초류종자살포 면적(× 반영률) 그대로 · 넣으면 그 값으로 덮어씀", + "Standard estimate 9-19-1 tables (cut face soil · fill face method/soil) — no suggested value; unset leaves the bill row with an input reason. Area left blank uses the seeding area (× ratio); a value overrides it", + ], B08_Quantity_Side_SubgradeCompaction: ["노체다짐", "Subgrade Compaction"], B08_Quantity_SubgradeCompaction_Label: ["노체다짐을 셀 것인가", "Count subgrade compaction"], B08_Quantity_Side_SubgradeCompaction_Hint: [