/* ============================================================================= * line_stack_work.ts * 산출근거 시간당 작업량 줄(PLAN 846-2 · 요구 879 · 881 · 882 · 883) — 기계 줄은 「시간당 작업량」 을 먼저 보이고 * 비목 줄을 「노무비 : 59,020 원/시간 ÷ 70 ㎥/시간 = 843.1 원」 으로(금액 = 계산기 답 그대로 · 그리기만) * 일당 = 「시간당 작업량 = 시공량 560 ㎥/일 ÷ (1 대 × 8 시간) = 70 ㎥/시간」 * 문서 식 = 「시간당 작업량 = Q_작업 = 24.89 ㎥/시간」(식 풀이는 위 공식 줄) * 시간당 식 = 그 기계 작업량 틀 풀이 — 값 줄(q · K · f · E · cm) + 「Q = 3,600 × q × K × f × E ÷ cm = … ㎥/시간」 * (원문 한 층 분수 · 겹분수 규칙 = 845-2 `frac` 1 층) · 기계 보기도 같은 줄(`hourlyRows`) * 머리 작은 글 = 방식(「품셈 일당」 · 「문서 식」 · 「시간당 식」) · 자료 = `common_util_line_stack_work` * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; import { el } from "@ui/ui_template_elements"; import { eqText } from "@ui/ui_template_equation"; import { varId, type FormulaDoc, type Node } from "../common_util/common_util_formula_doc"; import { calcDoc, isNum, type DocAnswer, type DocLinks, } from "../common_util/common_util_formula_doc_calc"; import type { WorkInfo } from "../common_util/common_util_formula_doc_work"; import type { Values } from "../common_util/common_util_formula_doc_view"; import { nodeOf } from "../common_util/common_util_line_stack"; import { shownUnitOf } from "../common_util/common_util_line_stack_unit"; import { dailyParts, dayUnit, hourlyParts, restOfQty, workUnit, } from "../common_util/common_util_line_stack_work"; import { katexNode, symTex, tex, txt, valTex } from "./line_stack_tex"; import { lt, type LsKey } from "./line_stack_text"; export interface WorkRow { h: (string | HTMLElement)[]; cls?: string; } /** 방식 작은 글 */ export function workTag(kind: string, title = ""): HTMLElement { const key = `Work_${kind}` as LsKey; const b = createBadge(lt(key) || kind, "neutral", { size: 18 }); b.dataset.work = kind; if (title) b.title = title; return b; } /** 「a = b = c」 — 조각마다 조판(좁은 폭에서 조각 사이로 줄이 바뀜 · 공식 줄과 같음) */ const chain = (...xs: string[]): (string | HTMLElement)[] => xs.flatMap((x, i) => (i ? [" = ", katexNode(x)] : [katexNode(x)])); const unitTex = (u: string): string => (u ? `\\ ${txt(u)}` : ""); const amount = (v: unknown, u: string): string => `${valTex(v)}${unitTex(u)}`; /** 작업량 틀 풀이 — 값 줄 + 결과 식 줄(없으면 빈 목록) */ export function hourlyRows( tpl: FormulaDoc, a: DocAnswer, look?: (p: Values) => unknown, ): WorkRow[] { const parts = hourlyParts(tpl); if (!parts) return []; const vals = (a.값 ?? {}) as Values; const vars = new Map((tpl.변수 ?? []).map((v) => [varId(v), v])); const unit = (id: string): string => workUnit(shownUnitOf(vars.get(id), vals, look)); const pack = parts.uses.map((id) => katexNode(`${symTex(id)} = ${isNum(vals[id]) ? amount(vals[id], unit(id)) : txt("—")}`), ); const res = vals[parts.id]; const sym = tex(parts.node, { side: "sym", vals, frac: 1 }); const val = tex(parts.node, { side: "val", vals, frac: 1 }); const end = isNum(res) ? amount(res, workUnit(tpl.결과단위)) : txt("—"); return [ ...(pack.length ? [{ h: pack, cls: "packs" }] : []), { h: chain(symTex(parts.id), sym, val, end), cls: "work" }, ]; } export interface WorkCtx { doc: FormulaDoc; vals: Values; /** 시간당 식 풀이 때만(작업량 틀 · 표) */ links?: DocLinks; /** 캐시에 없는 연결(작업량 틀) — 뒤에서 서버 */ miss: () => void; look?: (p: Values) => unknown; } /** 시간당 작업량 줄(머리 + 풀이) — 값 없으면 빈 목록(까닭은 금액 줄이 보임) */ export function workRows(w: WorkInfo, c: WorkCtx): WorkRow[] { const head = lt("Work_Head"); const u = workUnit(w.단위); if (w.꼴 === "시간당") { let tpl: FormulaDoc | null = null; try { tpl = c.links?.doc(w.율) ?? null; } catch { c.miss(); } const tag = workTag(w.꼴, tpl?.이름 ?? w.율); if (!tpl) return [{ h: [tag, `${head} = `, isNum(w.값) ? `${eqText(w.값)} ${u}`.trim() : lt("Work_Wait")] }]; // prettier-ignore const a = calcDoc(tpl, c.links!, w.변수 ?? {}, [], new Map(), {}); const name = el("span", { className: "lstk-hint", text: tpl.이름 ?? w.율 }); return [{ h: [tag, name] }, ...hourlyRows(tpl, a, c.look)]; } if (!isNum(w.값)) return []; const tag = workTag(w.꼴 || "일당"); const res = amount(w.값, u); const parts = w.꼴 === "일당" ? dailyParts(w.율) : null; if (parts) { const val = (n: Node): string => tex(n, { side: "val", vals: c.vals, inline: true }); const dayName = parts.day[0] === "var" ? nameOf(c.doc, parts.day[1]) : ""; const day = `${dayName ? `${txt(dayName)}\\ ` : ""}${val(parts.day)}${unitTex(dayUnit(w.단위))}`; const per = parts.per .map((p) => `${val(p.node)}${unitTex(lt(p.hours ? "Work_Hours" : "Work_Unit"))}`) .join(" \\times "); const den = parts.per.length > 1 ? `\\left(${per}\\right)` : per; return [{ h: [tag, ...chain(txt(head), `${day} \\div ${den}`, res)], cls: "work" }]; } const n = nodeOf(w.율); const body = n ? [tex(n, { side: "sym", vals: c.vals, inline: true })] : []; return [{ h: [tag, ...chain(txt(head), ...body, res)], cls: "work" }]; } /** 비목 줄 수량 자리 — 「÷ 70 ㎥/시간 (× 나머지)」 · 꼴이 다르면 null(지금 꼴 그대로) */ export function workQty( doc: FormulaDoc, qty: unknown, w: WorkInfo | null, scope: Values, alias?: Record, ): HTMLElement[] | null { if (!w || !isNum(w.값) || w.값 <= 0) return null; const got = restOfQty(doc, qty); if (!got) return null; const by = katexNode(`{}\\div ${amount(w.값, workUnit(w.단위))}`); // {} = 두 항 사이 ÷ 간격 if (!got.rest) return [by]; return [by, katexNode(`\\times ${tex(got.rest, { side: "val", vals: scope, inline: true, alias })}`)]; // prettier-ignore } const nameOf = (doc: FormulaDoc, id: string): string => (doc.변수 ?? []).find((v) => varId(v) === id)?.이름 ?? "";