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:
@@ -530,3 +530,20 @@
|
||||
.lstk-mctl .lstk-line.k-ctl > :last-child {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
/* 846-2 시간당 작업량 줄 — 방식 작은 글 + 풀이(왼쪽 제어 구역 · 기계 보기) */
|
||||
.lstk-work {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4) var(--spacing-8);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lstk-work.packs {
|
||||
column-gap: var(--spacing-24);
|
||||
}
|
||||
|
||||
.lstk-out tr.work .lstk-h {
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import { shownUnitOf, type TableLook } from "../common_util/common_util_line_sta
|
||||
import { createUnitPick, NO_UNIT } from "../M02_MasterTemplete/DataFlow/M02_DF_UnitPick";
|
||||
import { reach } from "./formula_doc_render";
|
||||
import { attachDrag } from "./line_stack_drag";
|
||||
import type { WorkSpec } from "../common_util/common_util_formula_doc_work";
|
||||
import { openMachineWindow, type MachineCtx } from "./line_stack_machine";
|
||||
import { buildMachineCtl, machineOfLine } from "./line_stack_machine_ctl";
|
||||
import { openStackPick, type StackTake } from "./line_stack_pick";
|
||||
@@ -82,6 +83,8 @@ export interface EditHooks {
|
||||
lists?: (given: Record<string, unknown>) => Record<string, unknown> | null;
|
||||
/** 845-2 표 한 칸 읽기(캐시) — 「줄 따름」 값의 고른 줄 단위 */
|
||||
look?: TableLook;
|
||||
/** 846-2 그 기계 작업량 줄 */
|
||||
work?: (machine: string) => WorkSpec[];
|
||||
}
|
||||
|
||||
const field = (
|
||||
@@ -513,6 +516,7 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
const zone = buildMachineCtl(
|
||||
{ ...h, calc: h.calc, change: (fn) => h.change((d) => fn(d)), readOnly: !h.machine?.scope }, // prettier-ignore
|
||||
h.doc.변수?.find((x) => varId(x) === own)?.연결,
|
||||
line.i,
|
||||
);
|
||||
if (zone) card.append(zone);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* ㉠ 값 칸 = 그 기계 식이 읽는 기계 칸(단위 · 기본 = 사본 값 · 고친 칸 = 「고침」 + [원래 값])
|
||||
* ㉡ 조종원 = 인력 줄(노임 고르기 · 인/일 · 더하기 · 빼기 · 기본 = 기계 값 조종원 · 직종)
|
||||
* ㉢ 잡품 = 재료 줄(재료 · 유가 고르기 · 수량 · 율 % · 기본 = 밑 재료 줄 · 품셈 잡재료율)
|
||||
* ㉣ 시간당 작업량(846-2 · 맨 위) = 일당 / 시간당 식 고르기 · 시간당 변수 칸(`line_stack_machine_work`)
|
||||
* 고침 = 문서 칸 `기계덮음`(캐시 · [저장] 때 서버가 정본으로 다시 셈) · 원본과 같으면 지움(`putCover`)
|
||||
* 기본값 = 덮음 없이 셈한 그 기계 식 답 + 기계 줄 인력 · 잡품 목록(`defaultsOf` · `rowEntry` · 842-3) ·
|
||||
* 읽기(M01 기초) = 칸 잠김 · 단추 없음
|
||||
@@ -32,8 +33,11 @@ import {
|
||||
import { subInputs, type StackDoc } from "../common_util/common_util_line_stack";
|
||||
import type { Picked } from "../common_util/common_util_line_stack_pick";
|
||||
import { machineUnit } from "../common_util/common_util_line_stack_unit";
|
||||
import { workAt, workOfMachine } from "../common_util/common_util_line_stack_work";
|
||||
import { sites, type WorkCover, type WorkSpec } from "../common_util/common_util_formula_doc_work";
|
||||
import { machineCellName } from "../common_util/common_util_machine_symbol_name";
|
||||
import { openFindPicker } from "../M02_MasterTemplete/M02_MasterTemplete_Find_Pick";
|
||||
import { buildWorkGroup } from "./line_stack_machine_work";
|
||||
import { openStackPick } from "./line_stack_pick";
|
||||
import { lt } from "./line_stack_text";
|
||||
|
||||
@@ -51,6 +55,8 @@ export interface CtlHooks {
|
||||
lists?: (given: Record<string, unknown>) => Record<string, unknown> | null;
|
||||
/** 잠긴 값 칸 */
|
||||
lock?: string[];
|
||||
/** 846-2 그 기계 작업량 줄(기계 값 칸 `작업량`) */
|
||||
work?: (machine: string) => WorkSpec[];
|
||||
}
|
||||
|
||||
const FUELS = ["경유", "휘발유", "실내등유"];
|
||||
@@ -80,7 +86,7 @@ const num = (t: string): number | null => {
|
||||
};
|
||||
|
||||
/** 수 칸 — 빈 글 = 원래대로(null) · 음수 · 글 = 알림 · 단위 = 칸 바로 뒤(공식 묶음 값 칸과 같은 자리 · 845-2 ㉥) */
|
||||
function numBox(
|
||||
export function numBox(
|
||||
label: string,
|
||||
value: unknown,
|
||||
unit: string,
|
||||
@@ -115,8 +121,12 @@ function numBox(
|
||||
|
||||
const kind = (text: string): HTMLElement => el("span", { className: "lstk-kind", text });
|
||||
|
||||
/** 기계 제어 구역 — `link` = 그 기계 줄 단가 기호 연결 */
|
||||
export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElement | null {
|
||||
/** 기계 제어 구역 — `link` = 그 기계 줄 단가 기호 연결 · `at` = 그 금액 줄 차례(작업량) */
|
||||
export function buildMachineCtl(
|
||||
h: CtlHooks,
|
||||
link: Link | undefined,
|
||||
at?: number,
|
||||
): HTMLElement | null {
|
||||
const got = ctlOf(h, link);
|
||||
if (!got) return null;
|
||||
const { machine, sub, vals, base } = got;
|
||||
@@ -269,10 +279,27 @@ export function buildMachineCtl(h: CtlHooks, link: Link | undefined): HTMLElemen
|
||||
]),
|
||||
],
|
||||
});
|
||||
// ㉣ 시간당 작업량(846-2)
|
||||
const info = (at === undefined ? null : workAt(h.answer, at)) ?? workOfMachine(h.answer, machine);
|
||||
const workNow = (entry as MachineCover & { 작업량?: WorkCover }).작업량;
|
||||
const work = info
|
||||
? buildWorkGroup({
|
||||
doc: h.doc,
|
||||
info,
|
||||
kind: info.꼴 === "시간당" ? (sites(h.doc, h.doc.금액?.[at ?? -1]?.수량)[0]?.꼴 ?? "일당") : info.꼴, // prettier-ignore
|
||||
entry: workNow,
|
||||
specs: h.work?.(machine) ?? [],
|
||||
subDoc: h.subDoc,
|
||||
vals: (h.answer.값 ?? {}) as Record<string, unknown>,
|
||||
ro,
|
||||
put: (w) => put({ ...entry, 작업량: w } as MachineCover),
|
||||
})
|
||||
: [];
|
||||
const box = el("div", {
|
||||
className: "lstk-mctl lstk-wide",
|
||||
attrs: { "data-mctl": machine },
|
||||
children: [
|
||||
...work,
|
||||
...(values.length
|
||||
? [el("div", { className: "lstk-fhead", children: [kind(lt("Ctl_Values"))] }), ...values]
|
||||
: []),
|
||||
|
||||
@@ -2,3 +2,12 @@
|
||||
.lstk-msheet-similar .ui-find__list {
|
||||
max-height: 240px;
|
||||
}
|
||||
|
||||
/* 846-2 오른쪽 시간당 작업량 풀이(시간당 경비 아래) */
|
||||
.lstk-msheet-work {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
margin-block-start: var(--spacing-16);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 오른쪽 = 그 기계 시간당 경비 산출근거(`line_stack_out` · 노 · 재 · 경 · 계 합계 상자) — 값을 바꾸면 브라우저 계산기로 즉시
|
||||
* 서버(`POST /api/m02/machine-sheet` · `M02_MyMachine_Sheet`) = 식 고름 + 처음 줄 값을 박은 식 문서 + 연결 기록 ·
|
||||
* 고친 값은 덮음(`기계덮음` 꼴)으로 얹어 셈 · 캐시에 없는 연결 · 식이 바뀔 칸만 뒤에서 다시 물음(옛 요청 끊음)
|
||||
* 시간당 작업량(846-2) = 왼쪽 접이 카드(작업량 식 변수 = 기계 값) · 오른쪽 풀이(`line_stack_machine_sheet_work`)
|
||||
* 줄 = 저장 꼴(M01 기계 줄 · 내 기계 줄) — 덮음 ↔ 줄은 `common_util_machine_sheet` · 저장은 부르는 쪽(M01 상세 · 내 기계)
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
import { machineCellName } from "../common_util/common_util_machine_symbol_name";
|
||||
import { docFoot } from "./formula_doc_foot";
|
||||
import { buildMachineCtl } from "./line_stack_machine_ctl";
|
||||
import { sheetWorkCard, sheetWorkOut, type SheetWorkOpts } from "./line_stack_machine_sheet_work";
|
||||
import { buildOut } from "./line_stack_out";
|
||||
import "./line_stack.css";
|
||||
import "./line_stack_machine_sheet.css";
|
||||
@@ -158,6 +160,7 @@ export function createMachineSheet(o: MachineSheetOptions) {
|
||||
const warn = el("div");
|
||||
const ctlBox = el("div");
|
||||
const baseBox = el("div");
|
||||
const workBox = el("div");
|
||||
const root = el("div", {
|
||||
className: "lstk",
|
||||
attrs: { "data-edit": "1", "data-machine-sheet": no },
|
||||
@@ -528,8 +531,24 @@ export function createMachineSheet(o: MachineSheetOptions) {
|
||||
redraw: () => drawRight(),
|
||||
}),
|
||||
);
|
||||
right.append(sheetWorkOut(work()));
|
||||
if (cache.takeMissing()) ask();
|
||||
foot.replaceChildren(docFoot(a));
|
||||
};
|
||||
// 846-2 시간당 작업량 — 지금 줄 `값.작업량`(고침 = 지금 줄 · 저장은 부르는 쪽)
|
||||
const work = (): SheetWorkOpts => ({
|
||||
cur,
|
||||
orig,
|
||||
ro,
|
||||
links: cache.links,
|
||||
miss: () => undefined,
|
||||
changed: () => changed(),
|
||||
});
|
||||
const drawWork = (): void => {
|
||||
const keep = workBox.firstElementChild?.classList.contains("is-collapsed") ?? false;
|
||||
workBox.replaceChildren(sheetWorkCard(work(), keep));
|
||||
if (cache.takeMissing()) ask();
|
||||
};
|
||||
const drawWarn = (): void => {
|
||||
const vals: MachineValues = crewView({ ...engineVals(cur, mine), ...(cur.값 ?? {}) });
|
||||
if (unitOf("손료계수") !== machineUnit("손료계수")) delete vals.손료계수; // 일 단위 손료(임업) = 시간당 범위 아님
|
||||
@@ -541,6 +560,7 @@ export function createMachineSheet(o: MachineSheetOptions) {
|
||||
};
|
||||
const drawLive = (): void => {
|
||||
drawCtl();
|
||||
drawWork();
|
||||
drawRight();
|
||||
drawWarn();
|
||||
};
|
||||
@@ -552,7 +572,7 @@ export function createMachineSheet(o: MachineSheetOptions) {
|
||||
notice.replaceChildren(...(t ? [createText(t, "warning")] : []));
|
||||
};
|
||||
|
||||
left.append(...similarBox(), notice, warn, ctlBox, baseBox);
|
||||
left.append(...similarBox(), notice, warn, ctlBox, workBox, baseBox);
|
||||
drawAll();
|
||||
ask(true);
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/* =============================================================================
|
||||
* line_stack_machine_sheet_work.ts
|
||||
* 기계 보기 시간당 작업량(PLAN 846-2 · 요구 883 「시간당 작업량 = 기계의 능력 → 기계에 둠」)
|
||||
* 왼쪽 = 「시간당 작업량」 접이 카드 — 기계 줄 `값.작업량` 식마다 이름 + 변수 칸(기본 = 기계 값 › 식 문서 기본 ·
|
||||
* 고친 칸 「고침」 + [원래 값] = 처음 줄 값으로) · 고침 = 지금 줄(저장은 부르는 쪽 — M01 기계 상세 · 내 기계)
|
||||
* 오른쪽 = 시간당 경비 풀이 아래 「시간당 작업량」 — 그 식 풀이(값 줄 + 결과 식 · `hourlyRows` 산출근거와 같음)
|
||||
* 식 없는 기계 = 「시간당 작업량 식 없는 기계」 · 작업량 틀 · 표 연결 = 기계 보기 서버(`M02_MyMachine_Sheet._work`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import type { FormulaDoc } from "../common_util/common_util_formula_doc";
|
||||
import { calcDoc, type DocLinks } from "../common_util/common_util_formula_doc_calc";
|
||||
import { specsOf, workDefaults, type WorkSpec } from "../common_util/common_util_formula_doc_work";
|
||||
import type { MachineRow } from "../common_util/common_util_machine_sheet";
|
||||
import { lt } from "./line_stack_text";
|
||||
import { workVarRows } from "./line_stack_machine_work";
|
||||
import { hourlyRows, workTag } from "./line_stack_work";
|
||||
|
||||
export interface SheetWorkOpts {
|
||||
cur: MachineRow;
|
||||
orig: MachineRow;
|
||||
ro: boolean;
|
||||
links: DocLinks;
|
||||
/** 캐시에 없는 연결 — 뒤에서 서버 */
|
||||
miss: () => void;
|
||||
/** 지금 줄을 고친 뒤 */
|
||||
changed: () => void;
|
||||
}
|
||||
|
||||
const specsOfRow = (row: MachineRow): WorkSpec[] => specsOf(row.값?.작업량);
|
||||
|
||||
const docOf = (o: SheetWorkOpts, key: string): FormulaDoc | null => {
|
||||
try {
|
||||
return o.links.doc(key) ?? null;
|
||||
} catch {
|
||||
o.miss();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const note = (text: string): HTMLElement => el("div", { className: "lstk-empty", text });
|
||||
|
||||
/** 왼쪽 카드 — `keep` = 접힘 유지 */
|
||||
export function sheetWorkCard(o: SheetWorkOpts, keep: boolean): HTMLElement {
|
||||
const card = createPanelCard(lt("Work_Head"), keep);
|
||||
card.dataset.machineWork = "1";
|
||||
const specs = specsOfRow(o.cur);
|
||||
if (!specs.length) card.append(note(lt("Work_None")));
|
||||
const was = specsOfRow(o.orig);
|
||||
specs.forEach((spec, i) => {
|
||||
const tpl = docOf(o, spec.식);
|
||||
if (!tpl) return void card.append(note(`${spec.식} — ${lt("Work_Wait")}`));
|
||||
const first = was.find((s) => s.식 === spec.식) ?? null;
|
||||
const box = el("div", {
|
||||
className: "lstk-mctl",
|
||||
children: [
|
||||
el("div", { className: "lstk-fhead", children: [workTag("시간당", spec.식), el("span", { className: "lstk-hint", text: tpl.이름 ?? spec.식 })] }), // prettier-ignore
|
||||
...workVarRows({
|
||||
tpl,
|
||||
now: workDefaults(tpl, spec),
|
||||
base: workDefaults(tpl, first),
|
||||
ro: o.ro,
|
||||
set: (k, v) => {
|
||||
const vars = { ...(spec.변수 ?? {}) };
|
||||
const back = first?.변수?.[k];
|
||||
if (v !== null) vars[k] = v;
|
||||
else if (back !== undefined) vars[k] = back;
|
||||
else delete vars[k];
|
||||
const list = specsOfRow(o.cur).map((s, j) => (j === i ? { ...s, 변수: vars } : s));
|
||||
if (!Object.keys(vars).length) delete list[i].변수;
|
||||
(o.cur.값 ??= {}).작업량 = list;
|
||||
o.changed();
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
card.append(box);
|
||||
});
|
||||
attachCollapsible(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
/** 오른쪽 풀이 — 식마다 머리 + 값 줄 + 결과 식 */
|
||||
export function sheetWorkOut(o: SheetWorkOpts): HTMLElement {
|
||||
const specs = specsOfRow(o.cur);
|
||||
const box = el("div", { className: "lstk-msheet-work", attrs: { "data-machine-work-out": "1" } });
|
||||
box.append(el("strong", { text: lt("Work_Head") }));
|
||||
if (!specs.length) return (box.append(note(lt("Work_None"))), box);
|
||||
for (const spec of specs) {
|
||||
const tpl = docOf(o, spec.식);
|
||||
if (!tpl) {
|
||||
box.append(note(`${spec.식} — ${lt("Work_Wait")}`));
|
||||
continue;
|
||||
}
|
||||
const a = calcDoc(tpl, o.links, workDefaults(tpl, spec), [], new Map(), {});
|
||||
box.append(
|
||||
el("div", { className: "lstk-work", children: [workTag("시간당", spec.식), el("span", { className: "lstk-hint", text: tpl.이름 ?? spec.식 })] }), // prettier-ignore
|
||||
...hourlyRows(tpl, a, (p) => o.links.table(p)).map((r) =>
|
||||
el("div", { className: `lstk-work ${r.cls ?? ""}`.trim(), children: r.h }),
|
||||
),
|
||||
);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
* 기계 · 단가산출 줄 = 비목마다 「노무비 : 59,020 × … = 금액」 — 제목 줄(「굴착기 0.6㎥」) 누름 = 그 문서(기계 시간당 경비 등)를
|
||||
* 같은 계산기로 셈해 같은 꼴로 펼침(손료 식 · 연료 · 운전사 · 노 · 재 · 경 · 계) · 비목 수는 글(링크 꼴 없음 · 요구 687-11) ·
|
||||
* 공식 줄 단가산출 결과 = 수 누름(▸) · 접으면 한 줄 · 출력엔 펼친 것만
|
||||
* 기계 줄 작업량(846-2) = 「시간당 작업량 = …」 줄 먼저 · 비목 줄 「÷ 70 ㎥/시간」(`line_stack_work`)
|
||||
* 값 = 계산기 셈 답 그대로(`calcDoc` · 줄 `answer.줄` · 합 `노무비` …) — 여기서 다시 셈 안 함 · 조판 = KaTeX(`line_stack_tex.ts`)
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -50,9 +51,11 @@ import {
|
||||
unitText,
|
||||
shownUnitOf,
|
||||
} from "../common_util/common_util_line_stack_unit";
|
||||
import { workAt } from "../common_util/common_util_line_stack_work";
|
||||
import { partName } from "../common_util/common_util_machine_symbol_name";
|
||||
import { katexNode, symTex, tex, txt, valTex, type TexCtx } from "./line_stack_tex";
|
||||
import { lt } from "./line_stack_text";
|
||||
import { workQty, workRows } from "./line_stack_work";
|
||||
|
||||
type Cols = Partial<Record<"sum" | (typeof COSTS)[number], number>>;
|
||||
interface Row {
|
||||
@@ -318,18 +321,30 @@ function rows(
|
||||
// 값 · 단위 한 덩이(좁은 폭에서 줄이 바뀌어도 단위가 떨어지지 않게)
|
||||
const glue = (...xs: (string | HTMLElement)[]): HTMLElement =>
|
||||
el("span", { className: "lstk-pack", children: xs.filter((x) => x !== "") });
|
||||
// 846-2 작업량 — 「시간당 작업량」 줄 먼저 · 수량 = 「÷ 시간당 작업량」
|
||||
const w = workAt(a, i);
|
||||
const wq = (): HTMLElement[] | null => workQty(doc, qty, w, scope, alias); // 줄마다 새 요소
|
||||
const byWork = wq() !== null;
|
||||
const workHead = (): void => {
|
||||
if (!w) return;
|
||||
const got = workRows(w, { doc, vals: scope, links: h.cache.links, miss: () => undefined, look }); // prettier-ignore
|
||||
if (h.cache.takeMissing()) h.miss();
|
||||
for (const r of got) push(r);
|
||||
};
|
||||
const qtyNode = (cost?: string): (string | HTMLElement)[] =>
|
||||
qty === null
|
||||
? []
|
||||
: [
|
||||
glue(
|
||||
" × ",
|
||||
mu.wrap && qn
|
||||
? katexNode(`\\left(${tex(qn, { side: "val", vals: scope, inline: true, cost, alias })}\\right)`) // prettier-ignore
|
||||
: exprNode(qty, "val", scope, true, cost, { alias }),
|
||||
mu.qty ? ` ${mu.qty}` : "",
|
||||
),
|
||||
];
|
||||
: byWork
|
||||
? [glue(" ", ...wq()!)]
|
||||
: [
|
||||
glue(
|
||||
" × ",
|
||||
mu.wrap && qn
|
||||
? katexNode(`\\left(${tex(qn, { side: "val", vals: scope, inline: true, cost, alias })}\\right)`) // prettier-ignore
|
||||
: exprNode(qty, "val", scope, true, cost, { alias }),
|
||||
mu.qty ? ` ${mu.qty}` : "",
|
||||
),
|
||||
];
|
||||
const priceUnit = mu.price ? ` ${mu.price}` : "";
|
||||
if (r.까닭 && r.금액 === null) {
|
||||
push({ h: [`${name} : — `, el("span", { className: "lstk-miss", text: r.까닭 })] });
|
||||
@@ -341,6 +356,7 @@ function rows(
|
||||
const cols: Cols = {};
|
||||
const title = name || (s ? (subOf(s)?.이름 ?? s) : "");
|
||||
push({ h: s && subOf(s) ? fold(s, title) : [title], cls: "lname" });
|
||||
workHead();
|
||||
for (const c of COSTS) {
|
||||
const part = r.비목?.[c];
|
||||
if (!isNum(part) || (!part && !bundle[c])) continue;
|
||||
@@ -364,6 +380,7 @@ function rows(
|
||||
: [glue(exprNode(line.단가, "val", scope, true, undefined, { alias }), priceUnit)];
|
||||
const c = String(line.비목);
|
||||
const bare = !!line.합밖 && !line.비목; // 합 밖 소계(810-2) = 글에만 금액 · 비목 칸 빈칸
|
||||
workHead();
|
||||
push({
|
||||
h: [`${name} : `, ...price, ...qtyNode(), ` = ${one(amt)} ${AMOUNT_UNIT}`],
|
||||
cols: bare ? {} : { sum: amt, [c]: amt },
|
||||
|
||||
@@ -56,6 +56,7 @@ import { askRenew } from "./formula_doc_renew";
|
||||
import { buildOptions } from "./formula_doc_options";
|
||||
import { ft } from "./formula_doc_text";
|
||||
import { addTaken, type Taken } from "../common_util/common_util_line_stack_copy";
|
||||
import { machineWork } from "../common_util/common_util_line_stack_work";
|
||||
import type { Layer } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import { buildEdit } from "./line_stack_edit";
|
||||
import { openMachineWindow, type CopyAnswer, type MachineCtx } from "./line_stack_machine";
|
||||
@@ -378,6 +379,7 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
calc: calcSub,
|
||||
lists: (g) => listsOf((q) => cache.links.element(q), g),
|
||||
look: (p) => cache.links.table(p),
|
||||
work: (m) => machineWork((q) => cache.links.element(q), m),
|
||||
before: saved ? lineMap(JSON.parse(saved) as StackDoc) : undefined,
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -90,7 +90,10 @@ const TEXT = {
|
||||
"이 값의 줄은 위 옵션이 고름(다른 줄은 옵션에서 바꿈 · 여기선 칸만)",
|
||||
"Row follows the option above (change it there; pick a column here)",
|
||||
],
|
||||
Ctl_Open: ["기계 값 · 조종원 · 잡품 고치기", "Edit machine values · operator · sundries"],
|
||||
Ctl_Open: [
|
||||
"기계 값 · 조종원 · 잡품 · 작업량 고치기",
|
||||
"Edit machine values · operator · sundries · output",
|
||||
],
|
||||
Ctl_Values: ["기계 값", "Machine values"],
|
||||
Ctl_Crew: ["조종원", "Operator"],
|
||||
Ctl_Misc: ["잡품", "Sundries"],
|
||||
@@ -104,6 +107,18 @@ const TEXT = {
|
||||
Ctl_Rate: ["율", "Rate"],
|
||||
Ctl_Follow: ["(따름)", "(follows)"],
|
||||
Ctl_None: ["없음", "None"],
|
||||
Work_Head: ["시간당 작업량", "Output per hour"],
|
||||
Work_일당: ["품셈 일당", "Daily output"],
|
||||
Work_식: ["문서 식", "Doc formula"],
|
||||
Work_시간당: ["시간당 식", "Hourly formula"],
|
||||
Work_Way: ["작업량 방식", "Output method"],
|
||||
Work_Pick: ["작업량 식", "Output formula"],
|
||||
Work_Day: ["시공량", "Daily output"],
|
||||
Work_Unit: ["대", "unit"],
|
||||
Work_Hours: ["시간", "h"],
|
||||
Work_None: ["시간당 작업량 식 없는 기계", "No hourly output formula for this machine"],
|
||||
Work_Wait: ["작업량 식 불러오는 중", "Loading output formula"],
|
||||
Work_Vars: ["작업량 변수", "Output variables"],
|
||||
Win_Ctl_Note: [
|
||||
"기계 내부 값(가격 · 연료 · 조종원 · 잡품)은 단가산출 왼쪽 기계 줄의 ▸ 에서 고침",
|
||||
"Edit machine internals from the ▸ on the left machine line",
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/* =============================================================================
|
||||
* 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)?.이름 ?? "";
|
||||
@@ -9,6 +9,7 @@
|
||||
- 바탕 = 처음 줄 인력 · 잡품 목록(842-3 · `master_doc_cover.row_entry`) — 덮음(지금 줄 목록)이 그 위에 · 값 박음은
|
||||
목록 → 조종원 수 · 직종 · 잡재료율(`master_machine_crew.view`)
|
||||
- 직종 = 노임 요소 코드 → 인력 원문번호(인력 줄 키 `LB:원문번호`)
|
||||
- 작업량(846-2) = 지금 줄 `값.작업량` 식마다 작업량 틀 문서 · 표를 연결에 더함(오른쪽 풀이)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -59,6 +60,19 @@ def _jobs() -> dict[str, str]:
|
||||
return {str(r["키"]): str(r.get("원문번호")) for r in rows if r.get("키")}
|
||||
|
||||
|
||||
def _work(cur: dict, rec, mc) -> None:
|
||||
"""기계 작업량 줄 → 작업량 틀 문서 · 표 연결(못 읽는 식 = 건너뜀 · 화면이 「불러오는 중」)."""
|
||||
for spec in (cur.get("값") or {}).get("작업량") or []:
|
||||
key = str(spec.get("식") or "") if isinstance(spec, dict) else ""
|
||||
try:
|
||||
tpl = rec.doc(key) if key else None
|
||||
if tpl:
|
||||
rec.prefetch(tpl)
|
||||
mc.calc(tpl, rec, dict(spec.get("변수") or {}))
|
||||
except Exception: # noqa: BLE001 — 그림 연결만(셈 정본 아님)
|
||||
continue
|
||||
|
||||
|
||||
def sheet(row: Any, base: Any = None, cover: Any = None, mine: bool = False) -> dict:
|
||||
import master_doc_calc as mc
|
||||
import master_doc_cover as mcv
|
||||
@@ -91,6 +105,7 @@ def sheet(row: Any, base: Any = None, cover: Any = None, mine: bool = False) ->
|
||||
rec = mr.Rec(links)
|
||||
rec.prefetch(shaped)
|
||||
mc.calc(shaped, rec, given)
|
||||
_work(cur, rec, mc)
|
||||
got = {"키": logic["키"], "이름": logic.get("이름"), "까닭": mh.why(cur, logic), "문서": doc,
|
||||
"값": vals, "연결": rec.out()} # fmt: skip
|
||||
return json.loads(json.dumps(out | got, ensure_ascii=False, default=_plain))
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/* =============================================================================
|
||||
* common_util_line_stack_work.ts
|
||||
* 시간당 작업량 그리기 자료(PLAN 846-2 · 요구 881 · 882 · 883) — 산출근거 · 기계 제어 구역 · 기계 보기가 같이 씀
|
||||
* 셈 답 기계 줄 `줄[i].작업량`(846-1 계산기 `common_util_formula_doc_work`) → 그림 자료
|
||||
* ㉠ 일당 풀이 = 율 식 「D ÷ (8 × 대수…)」 → 시공량 D · 나눔 항(8 = 시간 · 그 밖 = 대수)
|
||||
* ㉡ 비목 줄 = 수량 식의 작업량 자리를 바꾼 글(`swap` — 「나머지 ÷ Q_NEW」) → 나머지(1 이면 없음) ·
|
||||
* 꼴이 다르면 null(지금 꼴 그대로)
|
||||
* ㉢ 단위 — 작업량 「㎥/hr」 → 「㎥/시간」 · 시공량 = 「㎥/일」
|
||||
* ㉣ 시간당 식 풀이 = 작업량 틀 결과 식이 읽는 기호(q · K · f · E · cm — 품셈 기호 그대로)
|
||||
* 그리기만 — 식 · 값 · 금액 불변 · 화면 없음(시험이 Node 로 돌림)
|
||||
* ========================================================================== */
|
||||
|
||||
import { names, type FormulaDoc, type Node } from "./common_util_formula_doc";
|
||||
import type { DocAnswer } from "./common_util_formula_doc_calc";
|
||||
import {
|
||||
Q_NEW,
|
||||
specsOf,
|
||||
swap,
|
||||
WORK,
|
||||
type WorkInfo,
|
||||
type WorkSpec,
|
||||
} from "./common_util_formula_doc_work";
|
||||
import { nodeOf } from "./common_util_line_stack";
|
||||
import { unitText } from "./common_util_line_stack_unit";
|
||||
|
||||
/** 셈 답 i 번째 금액 줄의 작업량(없으면 null) */
|
||||
export function workAt(a: DocAnswer | null | undefined, i: number): WorkInfo | null {
|
||||
const got = (a?.줄?.[i] as Record<string, unknown> | undefined)?.[WORK];
|
||||
return got && typeof got === "object" ? (got as WorkInfo) : null;
|
||||
}
|
||||
|
||||
/** 그 기계의 첫 작업량 줄(같은 기계 두 줄 = 덮음 하나) */
|
||||
export function workOfMachine(a: DocAnswer | null | undefined, machine: string): WorkInfo | null {
|
||||
for (let i = 0; i < (a?.줄?.length ?? 0); i++) {
|
||||
const w = workAt(a, i);
|
||||
if (w && w.기계 === machine) return w;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 기계 작업량 줄(기계 값 칸 `작업량` — 캐시 `links.element`) · 없음 · 못 읽음 = 빈 목록 */
|
||||
export function machineWork(
|
||||
element: (p: Record<string, unknown>) => unknown,
|
||||
machine: string,
|
||||
): WorkSpec[] {
|
||||
try {
|
||||
return specsOf(element({ 키: `EQ:${machine}`, 칸: WORK }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 작업량 단위 → 화면(「/hr」 = 「/시간」) */
|
||||
export const workUnit = (u: unknown): string =>
|
||||
unitText(u).replace(/\/\s*(hr|h|Hr|HR|시간)$/, "/시간");
|
||||
|
||||
/** 시공량(하루) 단위 — 작업량 단위 분자 + 「/일」 */
|
||||
export const dayUnit = (u: unknown): string => {
|
||||
const top = workUnit(u).replace(/\/시간$/, "");
|
||||
return top ? `${top}/일` : "";
|
||||
};
|
||||
|
||||
const factors = (n: Node): Node[] =>
|
||||
n[0] === "bin" && n[1] === "*" ? [...factors(n[2]), ...factors(n[3])] : [n];
|
||||
|
||||
export interface DailyParts {
|
||||
/** 하루 시공량 식 */
|
||||
day: Node;
|
||||
/** 나눔 항 — 8 = 시간 · 그 밖 = 대수 */
|
||||
per: { node: Node; hours: boolean }[];
|
||||
}
|
||||
|
||||
/** ㉠ 일당 율 식 「D ÷ (8 × …)」 → 시공량 · 나눔 항 · 꼴이 다르면 null */
|
||||
export function dailyParts(rate: unknown): DailyParts | null {
|
||||
const n = nodeOf(rate);
|
||||
if (!n || n[0] !== "bin" || n[1] !== "/") return null;
|
||||
const per = factors(n[3]).map((x) => ({ node: x, hours: x[0] === "num" && x[1] === 8 }));
|
||||
if (!per.some((p) => p.hours)) return null;
|
||||
// 대수 먼저 · 8 시간 끝(「1 대 × 8 시간」 — 경쟁사 「대수 × 8hr」 꼴)
|
||||
return { day: n[2], per: [...per.filter((p) => !p.hours), ...per.filter((p) => p.hours)] };
|
||||
}
|
||||
|
||||
/** ㉡ 수량 식 → 「나머지 ÷ Q」 의 나머지(없으면 rest null) · 꼴이 다르면 null */
|
||||
export function restOfQty(doc: FormulaDoc, qty: unknown): { rest: Node | null } | null {
|
||||
let text: string;
|
||||
try {
|
||||
text = swap(doc, qty);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const n = nodeOf(text);
|
||||
if (!n || n[0] !== "bin" || n[1] !== "/" || n[3][0] !== "var" || n[3][1] !== Q_NEW) return null;
|
||||
const rest = n[2];
|
||||
return { rest: rest[0] === "num" && rest[1] === 1 ? null : rest };
|
||||
}
|
||||
|
||||
/** ㉣ 작업량 틀 결과 식 · 그 식이 읽는 기호(차례 그대로 · 겹침 뺌) */
|
||||
export function hourlyParts(tpl: FormulaDoc): { id: string; node: Node; uses: string[] } | null {
|
||||
const id = String(tpl.결과 ?? "");
|
||||
const line = (tpl.식 ?? []).find((l) => String(l.기호 ?? "") === id);
|
||||
const node = line ? nodeOf(line.식) : null;
|
||||
return node ? { id, node, uses: [...new Set(names(node))] } : null;
|
||||
}
|
||||
@@ -41,6 +41,9 @@ class FakeNode {
|
||||
append(...xs) {
|
||||
for (const x of xs) {
|
||||
if (x === null || x === undefined) continue;
|
||||
// 진짜 DOM 처럼 — 이미 다른 곳에 붙은 요소는 옮김(같은 요소를 두 줄에 쓰면 앞 줄에서 빠짐)
|
||||
if (x instanceof FakeNode && x.parentNode) x.parentNode.childNodes = x.parentNode.childNodes.filter((c) => c !== x);
|
||||
if (x instanceof FakeNode) x.parentNode = this;
|
||||
this.childNodes.push(x instanceof FakeNode ? x : new FakeText(String(x)));
|
||||
}
|
||||
}
|
||||
@@ -283,7 +286,7 @@ function left(c) {
|
||||
const rows = [];
|
||||
const walk = (n) => {
|
||||
for (const ch of n.children) {
|
||||
if (ch.cls().includes("lstk-frow")) rows.push({ sym: ch.dataset.sym ?? ch.dataset.mctlField ?? "", t: textOf(ch) });
|
||||
if (ch.cls().includes("lstk-frow") || ch.cls().includes("lstk-work")) rows.push({ sym: ch.dataset.sym ?? ch.dataset.mctlField ?? "", t: textOf(ch) });
|
||||
walk(ch);
|
||||
}
|
||||
};
|
||||
@@ -291,6 +294,14 @@ function left(c) {
|
||||
return [...opts.buildOptions(doc, {}, true, noop).map((x) => ({ sym: `opt:${x.dataset.option}`, t: textOf(x) })), ...rows];
|
||||
}
|
||||
|
||||
/** 셈 답 요약(846-2 — 계 · 줄마다 작업량 · 금액) */
|
||||
function sums(c) {
|
||||
const cache = cacheMod.createDocCache();
|
||||
cache.merge(c.연결 || {});
|
||||
const a = calc.calcDoc(c.문서, cache.links, {});
|
||||
return { 계: a.계 ?? null, 줄: (a.줄 || []).map((r) => ({ 금액: r.금액 ?? null, 작업량: r.작업량 ?? null })) };
|
||||
}
|
||||
|
||||
/** 전수 지킴 — 줄 글에서 규칙 어긋남만 */
|
||||
const MONEY = /^(.+?) : (?!—)(.*\S)$/;
|
||||
function guard(c, machine) {
|
||||
@@ -319,7 +330,7 @@ let res;
|
||||
if (input.할일 === "전수") {
|
||||
res = input.문서들.map((c) => {
|
||||
const g = guard(c, machineNames);
|
||||
return { 키: c.키, ...g, ...(show.has(c.키) ? { 오른쪽: right(c), 왼쪽: left(c) } : {}) };
|
||||
return { 키: c.키, ...g, ...(show.has(c.키) ? { 오른쪽: right(c), 왼쪽: left(c), 셈: sums(c) } : {}) };
|
||||
});
|
||||
} else if (input.할일 === "단위") {
|
||||
res = {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
① 전수 1,520 — 산출근거(펼침 모두)를 진짜 코드로 차린 줄 글:
|
||||
금액 · 소계 · 합계 = 「원」 · 수량이 식이면 괄호로 식 전체에 단위(「÷ 560 시간」 꼴 없음) ·
|
||||
메타 단위(「… 따름」 · 「수(…)」) 글 0 · 기계경비식(맨 위 · 펼침) 분수 한 층 · 문서 자료 안 바뀜
|
||||
② GC000088 — 사용자 그림 자리: 「59,020 원/시간 × (1 × 8 ÷ 560) 시간 = 843.1 원」 · 펼친 굴착기 = 상여 · 휴지 · 손료계수 중간 줄
|
||||
② GC000088 — 사용자 그림 자리: 「59,020 원/시간 ÷ 70 ㎥/시간 = 843.1 원」(846-2) · 펼친 굴착기 = 상여 · 휴지 · 손료계수 중간 줄
|
||||
③ 왼쪽 — 기계 제어 구역 값 칸 단위 = 오른쪽 펼침과 같은 글(천원 · 10⁻⁷/시간 · ℓ/hr) · 단위는 칸 밖 바로 뒤
|
||||
④ 단위 규칙 낱개 — 메타 · 설명 괄호 · 10⁻⁷ · 기계 칸 연결 · 금액 줄 자리
|
||||
"""
|
||||
@@ -71,11 +71,9 @@ def test_전수_규칙_어긋남_0(world):
|
||||
|
||||
def test_GC000088_비목_줄_단위_자리(world):
|
||||
rows = [r["t"] for r in world["GC000088"]["오른쪽"]]
|
||||
labor = next(t for t in rows if t.startswith("노무비 : ") and "×" in t)
|
||||
assert labor.startswith(
|
||||
"노무비 : 59,020 원/시간 × ⟦\\left(1 \\times 8 \\div 560\\right)⟧ 시간 = "
|
||||
)
|
||||
assert labor.endswith(" 원")
|
||||
# 846-2 기계 줄 = 「÷ 시간당 작업량」(시간 수량 식 「× (1 × 8 ÷ 560) 시간」 대신)
|
||||
labor = next(t for t in rows if t.startswith("노무비 : 59,020"))
|
||||
assert labor == "노무비 : 59,020 원/시간 ⟦{}\\div 70\\ \\text{㎥/시간}⟧ = 843.1 원"
|
||||
assert any(t.startswith("소 계 : ") and t.endswith(" 원") for t in rows)
|
||||
assert any(t.startswith("계 : ") and t.endswith(" 원") for t in rows)
|
||||
# 펼친 굴착기 시간당 경비 — 겹분수 대신 중간 줄
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 846-2(요구 879 · 881 · 882 · 883) — 기계 줄 시간당 작업량 그리기 · 제어 구역 · 기계 보기.
|
||||
|
||||
① 일당(GC000088) — 「시간당 작업량 = 시공량 560 ㎥/일 ÷ (1 대 × 8 시간) = 70 ㎥/시간」 줄 먼저 ·
|
||||
비목 줄 「59,020 원/시간 ÷ 70 ㎥/시간 = 843.1 원」 · 화면 셈 = 서버 셈(금액 그대로)
|
||||
② 시간당 식 덮음 — 작업량 틀 풀이(q · K · f · E · cm 값 줄 + 한 층 분수 Q 식) · 비목 ÷ Q ·
|
||||
변수 E 고침 → Q 그만큼 · 화면 셈 = 서버 셈
|
||||
③ 왼쪽 제어 구역 — 방식 고르기 · 시간당 = 그 식 변수 칸 · 식 없는 기계 = 잠김 글
|
||||
④ 전수 — 기계 작업량 식이 있는 문서를 모두 시간당 식으로 바꿔 그려도 안 멈춤 ·
|
||||
겹분수 · 단위 규칙 어긋남 0 · 화면 셈 = 서버 셈
|
||||
⑤ 기계 보기 서버 — 작업량 틀 문서 · 표 연결을 실음
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: F401 (scripts 길)
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
|
||||
mr = importlib.import_module("master_doc_record")
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
WORK = "작업량"
|
||||
|
||||
|
||||
def _js(payload: dict):
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_845_2_unit_show.cjs")],
|
||||
input=json.dumps(dc.ml._norm(payload), ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr[-3000:]
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def base():
|
||||
docs = dc.folder()
|
||||
return docs, dc.ml.Links(dc.skc.whole_(), docs)
|
||||
|
||||
|
||||
def _case(base, key: str, doc: dict):
|
||||
rec = mr.Rec(base[1])
|
||||
rec.prefetch(doc)
|
||||
ans = dc.mc.calc(doc, rec)
|
||||
return {"키": key, "문서": doc, "연결": rec.out(), "기계": False}, ans
|
||||
|
||||
|
||||
def _hourly(doc: dict, machine: str, key: str, vals: dict | None = None) -> dict:
|
||||
out = copy.deepcopy(doc)
|
||||
w = {"방식": "시간당", "식": key, **({"변수": vals} if vals else {})}
|
||||
out["기계덮음"] = {machine: {WORK: w}}
|
||||
return out
|
||||
|
||||
|
||||
def _num(v):
|
||||
return None if v is None else float(Decimal(str(v)))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def gc88(base):
|
||||
d = base[0]["GC000088"]
|
||||
cases, py = [], {}
|
||||
for key, doc in [
|
||||
("일당", d),
|
||||
("시간당", _hourly(d, "0201-0100", "GC001147")),
|
||||
("E", _hourly(d, "0201-0100", "GC001147", {"E입력": 0.8})),
|
||||
]:
|
||||
c, a = _case(base, key, doc)
|
||||
cases.append(c)
|
||||
py[key] = a
|
||||
got = _js({"할일": "전수", "문서들": cases, "보기": [c["키"] for c in cases]})
|
||||
return {g["키"]: g for g in got}, py
|
||||
|
||||
|
||||
def test_일당_시간당_작업량_줄_먼저_비목_나눔(gc88):
|
||||
g, py = gc88
|
||||
rows = [r["t"] for r in g["일당"]["오른쪽"]]
|
||||
work = (
|
||||
r"품셈 일당⟦\text{시간당 작업량}⟧ = ⟦\text{시공량}\ 560\ \text{㎥/일} \div "
|
||||
r"\left(1\ \text{대} \times 8\ \text{시간}\right)⟧ = ⟦70\ \text{㎥/시간}⟧"
|
||||
)
|
||||
assert work in rows
|
||||
labor = "노무비 : 59,020 원/시간 ⟦{}\\div 70\\ \\text{㎥/시간}⟧ = 843.1 원"
|
||||
assert labor in rows
|
||||
assert rows.index(work) < rows.index(labor) # 작업량 줄 먼저
|
||||
assert g["일당"]["bad"] == []
|
||||
assert _num(py["일당"]["계"]) == g["일당"]["셈"]["계"] == 1893 # 금액 그대로 · 화면 = 서버
|
||||
|
||||
|
||||
def test_시간당_식_풀이_비목_나눔_금액(gc88):
|
||||
g, py = gc88
|
||||
rows = [r["t"] for r in g["시간당"]["오른쪽"]]
|
||||
text = "\n".join(rows)
|
||||
assert (
|
||||
"⟦q = 1\\ \\text{㎥}⟧⟦K = 0.9⟧⟦f = 0.7⟧⟦E = 0.75⟧⟦\\mathit{cm} = 21\\ \\text{sec}⟧" in text
|
||||
)
|
||||
assert (
|
||||
r"⟦Q⟧ = ⟦\dfrac{3{,}600 \times q \times K \times f \times E}{\mathit{cm}}⟧ = "
|
||||
r"⟦\dfrac{3{,}600 \times 1 \times 0.9 \times 0.7 \times 0.75}{21}⟧ = ⟦81\ \text{㎥/시간}⟧"
|
||||
) in text
|
||||
assert "노무비 : 59,020 원/시간 ⟦{}\\div 81\\ \\text{㎥/시간}⟧ = 728.6 원" in rows
|
||||
assert any(t.startswith("시간당 식") for t in rows) # 머리 작은 글 = 방식
|
||||
assert g["시간당"]["bad"] == []
|
||||
assert _num(py["시간당"]["계"]) == g["시간당"]["셈"]["계"] != g["일당"]["셈"]["계"]
|
||||
|
||||
|
||||
def test_시간당_변수_E_고침_그만큼(gc88):
|
||||
g, py = gc88
|
||||
text = "\n".join(r["t"] for r in g["E"]["오른쪽"])
|
||||
assert "⟦E = 0.8⟧" in text and r"= ⟦86.4\ \text{㎥/시간}⟧" in text # 81 × 0.8 / 0.75
|
||||
assert "노무비 : 59,020 원/시간 ⟦{}\\div 86.4\\ \\text{㎥/시간}⟧ = 683.1 원" in text
|
||||
assert _num(py["E"]["계"]) == g["E"]["셈"]["계"]
|
||||
w = next(r["작업량"] for r in g["E"]["셈"]["줄"] if r["작업량"])
|
||||
assert w["꼴"] == "시간당" and w["값"] == 86.4 and w["변수"]["E입력"] == 0.8
|
||||
|
||||
|
||||
def test_왼쪽_제어_구역_방식_변수_칸(gc88):
|
||||
g, _ = gc88
|
||||
day = [r["t"] for r in g["일당"]["왼쪽"]]
|
||||
hour = [r["t"] for r in g["E"]["왼쪽"]]
|
||||
assert "품셈 일당시간당 식" in [t.strip() for t in day] # 고르기 두 칸
|
||||
assert not any(t.strip().startswith("버킷용량") for t in day) # 일당 = 변수 칸 없음
|
||||
assert "버킷용량 q=[1]㎥" in [t.strip() for t in hour]
|
||||
assert "체적환산계수 f=[0.7]" in [t.strip() for t in hour]
|
||||
e = next(t.strip() for t in hour if t.strip().startswith("E입력"))
|
||||
assert e.startswith("E입력=[0.8]고침") # 고친 칸 표지
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def every(base):
|
||||
"""작업량 자리가 있는 문서 전수 — 일당 그대로 + 기계 식이 있으면 시간당 식으로."""
|
||||
docs = base[0]
|
||||
plain, hourly, kinds, nofit = [], [], {}, []
|
||||
for key, doc in sorted(docs.items()):
|
||||
c, a = _case(base, key, doc)
|
||||
works = [r[WORK] for r in a.get("줄") or [] if isinstance(r, dict) and r.get(WORK)]
|
||||
if not works:
|
||||
continue
|
||||
plain.append(c)
|
||||
for w in works:
|
||||
kinds.setdefault(w["꼴"], set()).add(key)
|
||||
cover = {
|
||||
w["기계"]: {WORK: {"방식": "시간당", "식": w["식들"][0]}} for w in works if w["식들"]
|
||||
}
|
||||
if not cover:
|
||||
nofit.append(key)
|
||||
continue
|
||||
h = copy.deepcopy(doc)
|
||||
h["기계덮음"] = cover
|
||||
hc, ha = _case(base, f"{key}~시간당", h)
|
||||
hc["py계"] = _num(ha.get("계"))
|
||||
hourly.append(hc)
|
||||
return plain, hourly, kinds, nofit
|
||||
|
||||
|
||||
def test_전수_일당_시간당_차림_어긋남_0(every):
|
||||
plain, hourly, kinds, nofit = every
|
||||
assert len(kinds.get("일당", ())) >= 200 and len(kinds.get("식", ())) >= 40
|
||||
want = {c["키"]: c.get("py계") for c in hourly}
|
||||
got = _js({"할일": "전수", "문서들": plain + hourly, "보기": list(want)})
|
||||
assert len(got) == len(plain) + len(hourly)
|
||||
bad = {g["키"]: g["bad"][:2] for g in got if g["bad"]}
|
||||
assert bad == {}
|
||||
assert all(g["같음"] for g in got)
|
||||
off = {
|
||||
g["키"]: (want[g["키"]], g["셈"]["계"])
|
||||
for g in got
|
||||
if g["키"] in want and want[g["키"]] != g["셈"]["계"]
|
||||
}
|
||||
assert off == {} # 시간당 식 화면 셈 = 서버 셈
|
||||
|
||||
|
||||
def test_식_없는_기계_잠김_글(base, every):
|
||||
_, _, _, nofit = every
|
||||
assert nofit
|
||||
c, _ = _case(base, nofit[0], base[0][nofit[0]])
|
||||
g = _js({"할일": "전수", "문서들": [c], "보기": [c["키"]]})[0]
|
||||
assert any("시간당 작업량 식 없는 기계" in r["t"] for r in g["왼쪽"])
|
||||
|
||||
|
||||
def test_기계_보기_서버_작업량_연결():
|
||||
from M02_MasterTemplete import M02_MyMachine_Sheet as sheet
|
||||
|
||||
folder = os.path.join(HERE, "..", "master_data", "기계")
|
||||
rows = (json.load(open(os.path.join(folder, f), encoding="utf-8")) for f in os.listdir(folder))
|
||||
row = next(r for r in rows if r.get("원문번호") == "0201-0100")
|
||||
got = sheet.sheet(row)
|
||||
keys = [s["식"] for s in row["값"][WORK]]
|
||||
assert "GC001147" in keys
|
||||
assert all(k in got["연결"]["문서"] for k in keys)
|
||||
assert got["연결"]["표"] # 작업량 틀이 읽는 표(버킷계수 · 작업효율 · 싸이클)
|
||||
Reference in New Issue
Block a user