- 산출근거: 일당 「시간당 작업량 = 시공량 560 ㎥/일 ÷ (1 대 × 8 시간) = 70 ㎥/시간」 · 시간당 식 = q · K · f · E · cm 값 줄 + 한 층 분수 Q 식 · 비목 줄 「59,020 원/시간 ÷ 70 ㎥/시간 = 843.1 원」(금액 그대로) - 제어 구역: 「시간당 작업량」 묶음 맨 위 — 방식 고르기(식 없는 기계 = 잠김 + 까닭) · 시간당 = 작업량 틀 변수 칸(고침 · 원래 값) · 덮음 = 기계덮음.<기계>.작업량 - 기계 보기: 왼쪽 작업량 카드(기계 정본 변수) · 오른쪽 풀이 · 서버가 작업량 틀 연결을 실음(M02_MyMachine_Sheet._work) - 시험: test_846_2_work_view 새로 · test_845_2 GC000088 비목 줄 기대값을 새 꼴로 고침(846-2 표시 변경) · 헬퍼 가짜 DOM 이 진짜 DOM 처럼 요소를 옮김(같은 요소 두 줄 재사용 흠을 잡게) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
6.3 KiB
TypeScript
144 lines
6.3 KiB
TypeScript
/* =============================================================================
|
||
* 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<string, string>,
|
||
): 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)?.이름 ?? "";
|