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>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
/* =============================================================================
|
||||
* 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<string, unknown>,
|
||||
base: Record<string, unknown>,
|
||||
): Record<string, unknown> =>
|
||||
Object.fromEntries(Object.entries(vals).filter(([k, v]) => !(k in base && same(v, base[k]))));
|
||||
|
||||
export interface VarRowsOpts {
|
||||
tpl: FormulaDoc;
|
||||
/** 지금 값 */
|
||||
now: Record<string, unknown>;
|
||||
/** 원래 값(고침 표지 · [원래 값]) */
|
||||
base: Record<string, unknown>;
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user