/* ============================================================================= * B08_Quantity_UI_Side_TreeWaste.ts * 산출 조건 「임목폐기물」 구획 — 현장 조사값 넷 · 수동 처리단가 · 분리발주(2026-09-14 판정). * * 페이지 본문(`B08_Quantity_UI_Page.ts`)이 700줄을 넘어 새 칸을 이 파일로 뺌(CLAUDE.md 4장). * ⚠ 톤은 서버가 셈 — 여기는 값만 캐시에 쌓고 [저장]에서 정본으로 감(5장). * ========================================================================== */ import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } export interface TreeWasteDraft { tree_waste: Record; tree_waste_unit_price_krw_per_ton: number | null; waste_separate_order: boolean; /** 뿌리 산정법 — `""` 분배비 15/85(기본) · "root_ball" 뿌리분 체적 × 1,300. */ tree_waste_root_method: string; dirty: boolean; } /** 페이지의 칸 만들기 도우미 — 같은 모양을 쓰려고 받아 씀. */ export interface SideFieldHelpers { field(label: string, value: string): HTMLElement; optionalNumberField( label: string, value: number | null, step: string, onInput: (value: number | null) => void, ): HTMLElement; selectField( label: string, value: string, options: { value: string; label: string }[], onChange: (value: string) => void, ): HTMLElement; hintRow(text: string): HTMLElement; } /** 조사값 칸 — 서버 `TREE_WASTE_INPUTS` 와 같은 키. */ const INPUTS: [string, keyof typeof ui_locales, string][] = [ ["stems_per_1000m2", "B08_Quantity_TreeWaste_Stems", "1"], ["dbh_cm", "B08_Quantity_TreeWaste_Dbh", "0.1"], ["height_m", "B08_Quantity_TreeWaste_Height", "0.1"], ["unit_weight_kg_m3", "B08_Quantity_TreeWaste_UnitWeight", "10"], ]; /** 수동 단가 칸 — 값이 있으면 빨간 테두리(미확정). */ function markManual(row: HTMLElement, value: number | null): void { const input = row.querySelector("input"); if (input) input.style.outline = value ? "2px solid var(--color-danger, #d9534f)" : ""; } export function appendTreeWasteFields( panel: HTMLElement, draft: TreeWasteDraft, h: SideFieldHelpers, ): void { panel.append(h.field(L("B08_Quantity_Side_TreeWaste"), "")); for (const [key, label, step] of INPUTS) { panel.append( h.optionalNumberField(L(label), draft.tree_waste[key] ?? null, step, (value) => { draft.tree_waste[key] = value; draft.dirty = true; }), ); } panel.append(h.hintRow(L("B08_Quantity_Side_TreeWaste_Hint"))); // 뿌리 — 지침 부록1 이 두 방법을 둠. 뿌리분 직경은 흉고직경과 갈라 따로 받음(지침 표기가 모호). panel.append( h.selectField( L("B08_Quantity_TreeWaste_RootMethod"), draft.tree_waste_root_method, [ { value: "", label: L("B08_Quantity_TreeWaste_RootRatio") }, { value: "root_ball", label: L("B08_Quantity_TreeWaste_RootBall") }, ], (value) => { draft.tree_waste_root_method = value; draft.dirty = true; }, ), ); panel.append( h.optionalNumberField( L("B08_Quantity_TreeWaste_RootBallDiameter"), draft.tree_waste["root_ball_diameter_m"] ?? null, "0.01", (value) => { draft.tree_waste["root_ball_diameter_m"] = value; draft.dirty = true; }, ), ); panel.append(h.hintRow(L("B08_Quantity_TreeWaste_Root_Hint"))); const price = h.optionalNumberField( L("B08_Quantity_TreeWaste_Price"), draft.tree_waste_unit_price_krw_per_ton, "1000", (value) => { draft.tree_waste_unit_price_krw_per_ton = value; draft.dirty = true; markManual(price, value); }, ); markManual(price, draft.tree_waste_unit_price_krw_per_ton); panel.append(price); const priceHint = h.hintRow(L("B08_Quantity_TreeWaste_Price_Hint")); priceHint.classList.add("b08-quantity__hint--warn"); panel.append(priceHint); panel.append( h.selectField( L("B08_Quantity_TreeWaste_Separate"), draft.waste_separate_order ? "on" : "", [ { value: "", label: L("B08_Quantity_TreeWaste_Separate_Off") }, { value: "on", label: L("B08_Quantity_TreeWaste_Separate_On") }, ], (value) => { draft.waste_separate_order = value === "on"; draft.dirty = true; }, ), ); panel.append(h.hintRow(L("B08_Quantity_TreeWaste_Separate_Hint"))); }