/* ============================================================================= * B09_Estimation_UI_Haul_Steps.ts * 「운반 단가」 창 오른쪽 계산 칸(PLAN 277) — 칸 차례 · 칸마다 제목 줄 · 몸 · 바닥 결과(굵게). * ① 거리(바닥 운반거리) ② 사이클시간 건설품셈 8-2-8(TS 짝 `B08_Quantity_Haul_Cycle` · 바닥 Cm) * ③ 단가산출 = 서버 `logic-preview` 의 `blocks` 하나(277-2) — 머리 글 → 입력값 · 중간값(이름 · 값 크게, * 식 작게 흐리게) · 호표 줄(수량 · 금액) · 바닥 단가 + 노무 · 재료 · 경비(빈칸 = 까닭 빨강). * 셈은 서버 그대로 — 여기는 그리기만 · 테마 변수(`B09_Estimation_UI_Haul_Steps.css`). * 머리 줄 나눔 · 그리기(`headSteps`) = B09 단가산출근거 박스보기도 같이 씀(278-2 · `Tabs_Groups`). * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import { CYCLE_DEFAULTS, cycleTime, type Dist } from "../B08_Quantity/B08_Quantity_Haul_Cycle"; import "./B09_Estimation_UI_Haul_Steps.css"; /** 호표 줄 하나(서버 `SHOWN_LINE`) */ export interface StepLine { name: string; spec: string; qty: number | null; unit: string; kind: string; labor: number | null; material: number | null; expense: number | null; why: string | null; text?: string[] | null; } /** 산근 블록 하나(서버 `_block_shown`) */ export interface StepBlock { no: number; logic: string; name: string; unit: string; source: string; head: string[]; foot: string; price: number | null; parts: Record; why: string | null; lines: StepLine[]; } interface StepRow { name: string; value: string; formula?: string; bad?: boolean; /** 머리 글 몇째 줄(`splitHead`) */ at?: number; } const PART_TEXT: Record = { labor: "노무", material: "재료", expense: "경비" }; /** 값 — 천 단위 쉼표 · 빈 값 「—」 */ export const stepNum = (v: number | null | undefined, digits = 4): string => v == null || !Number.isFinite(v) ? "—" : v.toLocaleString("ko-KR", { maximumFractionDigits: digits }); const rowEl = (r: StepRow, ats?: number[]): HTMLElement => el("div", { className: "b09-haulstep__row", ...(ats && r.at !== undefined ? { attrs: { "data-at": String(ats[r.at]) } } : {}), children: [ el("span", { text: r.name }), el("span", { className: `b09-haulstep__value${r.bad ? " b09-haulstep__bad" : ""}`, text: r.value, }), ...(r.formula ? [el("span", { className: "b09-haulstep__formula", text: r.formula })] : []), ], }); const group = (label: string, rows: StepRow[], ats?: number[]): HTMLElement[] => rows.length ? [ el("div", { className: "b09-haulstep__group", text: label }), ...rows.map((r) => rowEl(r, ats)), ] : []; /** 칸 하나 — 제목 줄(제목 · 곁글) · 몸 · 바닥(굵게) */ function box(title: string, sub: string, body: HTMLElement[], foot: HTMLElement[]): HTMLElement { return el("section", { className: "b09-haulstep", children: [ el("div", { className: "b09-haulstep__head", children: [ el("span", { className: "b09-haulstep__title", text: title }), ...(sub ? [el("span", { className: "b09-haulstep__sub", text: sub })] : []), ], }), ...(body.length ? [el("div", { className: "b09-haulstep__body", children: body })] : []), ...(foot.length ? [el("div", { className: "b09-haulstep__foot", children: foot })] : []), ], }); } const stepFoot = (name: string, value: string, bad = false): HTMLElement[] => [ el("span", { text: name }), el("span", { className: `b09-haulstep__value${bad ? " b09-haulstep__bad" : ""}`, text: value }), ]; /** 알림 칸(제목 줄만) — 셈 중 · 못 셈 · 조합 안내 · 연결 없음 */ export function noteBox(text: string, bad = false): HTMLElement { const got = box(text, "", [], []); if (bad) got.querySelector(".b09-haulstep__title")?.classList.add("b09-haulstep__bad"); return got; } /** ① 거리 — 거리이정표 포장 · 비포장 → 운반거리 */ export function distBox(dist: Dist, input: Record): HTMLElement { const rows = [ { name: "포장 L1", value: `${stepNum(dist?.L1)} km` }, { name: "비포장 L2", value: `${stepNum(dist?.L2)} km` }, ]; return box( "거리", "거리이정표", rows.map((r) => rowEl(r)), stepFoot("운반거리", `${stepNum(cycleTime(input).L)} km`), ); } /** ② 사이클시간 — 건설품셈 8-2-8 덤프트럭(기본값 + 거리) */ export function cycleBox(input: Record): HTMLElement { const v: Record = { ...CYCLE_DEFAULTS }; for (const k of Object.keys(v)) if (typeof input[k] === "number") v[k] = input[k] as number; const r = cycleTime(input); const rows: StepRow[] = [ { name: "q", value: `${stepNum(r.q)} ㎥`, formula: `T / rt × f = ${v.T} / ${v.rt} × ${v.f}` }, { name: "n", value: `${stepNum(r.n)} 회`, formula: `q / (qs × k) = ${r.q} / (${v.qs} × ${v.k})`, }, { name: "t1", value: `${stepNum(r.t1)} 분`, formula: `Cms × n / (60 × Es) = ${v.Cms} × ${r.n} / (60 × ${v.Es})`, }, { name: "t2", value: `${stepNum(r.t2)} 분`, formula: `(L1/V1 + L1/V2 + L2/V3 + L2/V4) × 60 = (${v.L1}/${v.V1} + ${v.L1}/${v.V2} + ${v.L2}/${v.V3} + ${v.L2}/${v.V4}) × 60`, }, { name: "Cm", value: `${stepNum(r.cm)} 분`, formula: `t1 + t2 + t3 + t4 + t5 + t6 = ${r.t1} + ${r.t2} + ${v.t3} + ${v.t4} + ${v.t5} + ${v.t6}`, }, ]; return box( "사이클시간", "건설품셈 8-2-8 덤프트럭", rows.map((r) => rowEl(r)), stepFoot("Cm", `${stepNum(r.cm)} 분`), ); } type HeadOf = Pick; /** 머리 글 → 입력값(「이름 : 뜻 = 값」) · 중간값(「이름 = 식 = 값」 · 「└」 = 앞 줄 식에 붙임) */ export function splitHead(b: HeadOf): { inputs: StepRow[]; mids: StepRow[] } { const inputs: StepRow[] = []; const mids: StepRow[] = []; let last: StepRow | null = null; for (const [at, line] of b.head.entries()) { if (line.startsWith(`${b.name} / `) || (b.source && line === `${b.source} 적용`)) continue; if (line.startsWith("└") && last) { last.formula = [last.formula, line].filter(Boolean).join("\n"); continue; } const colon = line.indexOf(" : "); const eq = line.indexOf(" = "); const end = line.lastIndexOf(" = "); if (colon >= 0 && (eq < 0 || colon < eq)) { const value = end > colon ? line.slice(end + 3) : line.slice(colon + 3); const note = end > colon ? line.slice(colon + 3, end) : ""; inputs.push((last = { name: line.slice(0, colon), value, formula: note, at })); } else if (eq >= 0) { const formula = end > eq ? line.slice(eq + 3, end) : ""; mids.push((last = { name: line.slice(0, eq), value: line.slice(end + 3), formula, at })); } else mids.push((last = { name: line, value: "", at })); } return { inputs, mids }; } /** 머리 글 → 「입력값」 · 「중간값」 묶음(③ 칸 몸 · 278-2 B09 단가산출근거 박스보기가 같이 씀) · * ats = 머리 줄마다 부르는 쪽 줄 번호(주면 줄에 `data-at` — 누름 · 고름) */ export function headSteps(b: HeadOf, ats?: number[]): HTMLElement[] { const { inputs, mids } = splitHead(b); return [...group("입력값", inputs, ats), ...group("중간값", mids, ats)]; } /** ③ 단가산출 칸 — 블록 하나(맨 위 = 「단가산출」 · 하위 = 「하위 산근」) */ export function logicBox(b: StepBlock, label = "단가산출"): HTMLElement { const unit = b.unit ? `원/${b.unit.replace(/^원\//, "")}` : "원"; const lines: StepRow[] = b.lines.map((ln) => { const parts = [ln.labor, ln.material, ln.expense]; const money = parts.some((p) => p == null) ? null : parts.reduce((a, p) => a! + p!, 0); return { name: ln.spec ? `${ln.name} ${ln.spec}` : ln.name, value: ln.why ? `빈칸 — ${ln.why}` : `${stepNum(ln.qty)} ${ln.unit} · ${stepNum(money, 1)} 원`, formula: (ln.text || []).join("\n"), bad: !!ln.why, }; }); const foot = b.price == null ? stepFoot("단가", `빈칸 — ${b.why ?? ""}`, true) : [ ...stepFoot("단가", `${stepNum(b.price, 1)} ${unit}`), el("span", { className: "b09-haulstep__formula", text: Object.entries(b.parts) .map(([p, v]) => `${PART_TEXT[p] ?? p} ${stepNum(v, 1)}`) .join(" · "), }), ]; return box( `${label} ${b.logic} ${b.name}`, [unit, b.source].filter(Boolean).join(" · "), [...headSteps(b), ...group("호표", lines)], foot, ); }