Files
Aislo/A00_Common/line_stack_work.ts
T
eomsangdonandClaude Opus 5.5 c8cd98dec1 feat(846-2): 기계 줄 시간당 작업량 화면 — 산출근거 「시간당 작업량」 줄 먼저 · 비목 ÷ 시간당 작업량 · 제어 구역 일당 / 시간당 식 고르기 · 변수 칸 · 기계 보기 작업량 식
- 산출근거: 일당 「시간당 작업량 = 시공량 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>
2026-11-16 00:00:35 +09:00

144 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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)?.이름 ?? "";