/* ============================================================================= * line_stack_machine_work.ts * 기계 시간당 작업량 묶음(PLAN 846-2 · 요구 881 · 882 · 883) — 왼쪽 기계 제어 구역 · 기계 보기가 같이 씀 * 제어 구역(`buildWorkGroup`) = 방식 고르기(「품셈 일당」 · 「문서 식」 | 「시간당 식」 — 기계에 맞는 식 없으면 잠김 + 까닭) * 일당 = 그 풀이 읽기 글(산출근거와 같은 줄) · 시간당 = 식 고르기(여럿일 때) + 그 식 변수 칸(기본 = 기계 값 › * 식 문서 기본 · 고친 칸 「고침」 + [원래 값]) + 시간당 작업량 값 * 고침 = `기계덮음.<기계>.작업량 {방식 시간당, 식, 변수?}`(기본과 같은 변수 뺌 · 일당 = 칸 지움) · 읽기 = 잠김 * 기계 보기(`workVarRows`) = 같은 변수 칸으로 기계 작업량 줄 변수(기계 정본 값)를 고침 * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; import { createButton } from "@ui/ui_template_button"; import { eqText } from "@ui/ui_template_equation"; import { el } from "@ui/ui_template_elements"; import { createSelectField } from "@ui/ui_template_input"; import { createSegment } from "@ui/ui_template_segment"; import type { FormulaDoc } from "../common_util/common_util_formula_doc"; import { isNum } from "../common_util/common_util_formula_doc_calc"; import { HOURLY, workDefaults, type WorkCover, type WorkInfo, type WorkSpec, } from "../common_util/common_util_formula_doc_work"; import type { Values } from "../common_util/common_util_formula_doc_view"; import { varUnit } from "../common_util/common_util_line_stack_unit"; import { workUnit } from "../common_util/common_util_line_stack_work"; import { numBox } from "./line_stack_machine_ctl"; import { lt } from "./line_stack_text"; import { workRows } from "./line_stack_work"; const KINDS = ["입력", "선택", "숫자"]; const same = (a: unknown, b: unknown): boolean => typeof a === "number" && typeof b === "number" ? Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a)) : String(a ?? "") === String(b ?? ""); /** 기본과 같은 변수 뺌 */ export const pruneVars = ( vals: Record, base: Record, ): Record => Object.fromEntries(Object.entries(vals).filter(([k, v]) => !(k in base && same(v, base[k])))); export interface VarRowsOpts { tpl: FormulaDoc; /** 지금 값 */ now: Record; /** 원래 값(고침 표지 · [원래 값]) */ base: Record; ro: boolean; /** null = 원래대로 */ set: (key: string, v: number | string | null) => void; } /** 작업량 틀 변수 칸(입력 · 선택) — 이름(품셈 기호는 칸 풍선) = 칸 단위 */ export function workVarRows(o: VarRowsOpts): HTMLElement[] { return (o.tpl.변수 ?? []) .filter((v) => v.열쇠 && KINDS.includes(v.연결?.갈래 ?? "")) .map((v) => { const k = v.열쇠!; const now = k in o.now ? o.now[k] : o.base[k]; const name = v.이름 || k; const list = v.연결?.목록; const box = v.연결?.갈래 === "선택" && list?.length ? createSelectField({ options: list.map((x) => ({ value: String(x), text: String(x) })), value: String(now ?? ""), compact: true, size: 24, disabled: o.ro, ariaLabel: name, onChange: (x) => o.set(k, (list.find((y) => String(y) === x) ?? x) as number | string), // prettier-ignore }).root : numBox(name, now, varUnit(v), o.ro, (n) => o.set(k, n)); const changed = !same(now, o.base[k]); const sym = [v.기호, v.아래].filter(Boolean).join(" "); const row = el("div", { className: "lstk-frow", attrs: { "data-work-var": k }, children: [ el("b", { className: "lstk-sym", attrs: { title: [name, sym, (v as { 설명?: string }).설명].filter(Boolean).join(" — "), }, // 품셈 기호(q · f)는 그대로 곁에 children: [name, ...(v.기호 && v.기호 !== "S" && !v.아래 ? [" ", el("span", { className: "lstk-hint", text: v.기호 })] : [])], // prettier-ignore }), "=", box, ], }); if (changed) { const tag = createBadge(lt("Ctl_Changed"), "info", { size: 18 }); tag.title = `${lt("Ctl_Was")} ${o.base[k] === undefined ? "—" : eqText(o.base[k])}`; row.append(tag); if (!o.ro) row.append( createButton({ label: lt("Ctl_Back"), variant: "ghost", size: 24, iconStart: "undo", onClick: () => o.set(k, null) }), // prettier-ignore ); } return row; }); } export interface WorkGroupOpts { doc: FormulaDoc; /** 지금 셈 답 작업량(그 기계 첫 줄) */ info: WorkInfo; /** 덮음 없을 때 꼴(일당 · 식) — 고르기 첫 칸 이름 */ kind: string; entry: WorkCover | undefined; /** 그 기계 작업량 줄(기계 값 칸) */ specs: WorkSpec[]; subDoc: (key: string) => FormulaDoc | null; /** 문서 값(일당 풀이 글) */ vals: Values; ro: boolean; put: (w: WorkCover | undefined) => void; } /** 제어 구역 「시간당 작업량」 묶음 */ export function buildWorkGroup(o: WorkGroupOpts): HTMLElement[] { const hourly = o.entry?.방식 === HOURLY; const formulas = o.info.식들 ?? []; const first = o.kind === "식" ? "식" : "일당"; const seg = createSegment({ options: [ { value: "일당", label: lt(first === "식" ? "Work_식" : "Work_일당") }, { value: HOURLY, label: lt("Work_시간당"), disabled: !formulas.length && !hourly, ...(formulas.length ? {} : { title: lt("Work_None") }), }, ], value: hourly ? HOURLY : "일당", size: 24, ariaLabel: lt("Work_Way"), onChange: (v) => o.put(v === HOURLY ? { 방식: HOURLY, 식: formulas[0] } : undefined), }); if (o.ro) seg.root.querySelectorAll("button").forEach((b) => (b.disabled = true)); seg.root.dataset.workWay = hourly ? HOURLY : first; const head = el("div", { className: "lstk-fhead", children: [ el("span", { className: "lstk-kind", text: lt("Work_Head") }), ...(o.entry ? [createBadge(lt("Ctl_Changed"), "info", { size: 18 })] : []), ...(o.entry && !o.ro ? [createButton({ label: lt("Ctl_Back"), variant: "ghost", size: 24, iconStart: "undo", onClick: () => o.put(undefined) })] // prettier-ignore : []), ], }); const note = (text: string, cls = "lstk-empty"): HTMLElement => el("div", { className: cls, text }); // prettier-ignore const locked = !formulas.length && !hourly ? [note(lt("Work_None"))] : []; // 식 없는 기계 = 잠김 까닭 const out: HTMLElement[] = [head, el("div", { className: "lstk-work", children: [seg.root, ...locked] })]; // prettier-ignore if (!hourly) { const rows = workRows(o.info, { doc: o.doc, vals: o.vals, miss: () => undefined }); // 방식 작은 글은 고르기가 보임 — 풀이만 const bare = (r: { h: (string | HTMLElement)[] }) => r.h.filter((x) => typeof x === "string" || !x.dataset.work); // prettier-ignore out.push(...rows.map((r) => el("div", { className: "lstk-work", children: bare(r) }))); return out; } const pick = o.entry?.식 || o.info.율 || formulas[0] || ""; if (formulas.length > 1) out.push( el("div", { className: "lstk-frow", children: [ el("b", { className: "lstk-sym", text: lt("Work_Pick") }), "=", createSelectField({ options: formulas.map((k) => ({ value: k, text: o.subDoc(k)?.이름 ?? k })), value: pick, compact: true, size: 24, disabled: o.ro, ariaLabel: lt("Work_Pick"), onChange: (k) => o.put({ 방식: HOURLY, 식: k }), }).root, ], }), ); if (o.info.까닭) out.push(note(o.info.까닭, "lstk-miss")); const tpl = pick ? o.subDoc(pick) : null; if (!tpl) return [...out, note(lt("Work_Wait"))]; const base = workDefaults(tpl, o.specs.find((s) => s.식 === pick) ?? null); const mine = o.entry?.변수 ?? {}; out.push( el("div", { className: "lstk-work", children: [el("span", { className: "lstk-hint", text: tpl.이름 ?? pick })] }), // prettier-ignore ...workVarRows({ tpl, now: { ...base, ...mine }, base, ro: o.ro, set: (k, v) => { const next = { ...mine }; if (v === null) delete next[k]; else next[k] = v; const kept = pruneVars(next, base); o.put({ 방식: HOURLY, 식: pick, ...(Object.keys(kept).length ? { 변수: kept } : {}) }); }, }), el("div", { className: "lstk-work", children: [ el("b", { text: `${lt("Work_Head")} = ` }), isNum(o.info.값) ? `${eqText(o.info.값)} ${workUnit(o.info.단위)}`.trim() : "—", ], }), ); return out; }