- 기계 자료: M01 기계 221 대 값 칸 `작업량` = [{식 · 단위 · 변수(규격에서 정해지는 칸)}] (건설 공통 8-2 장 작업량 틀 · 빌더 build_기계_작업량_846_1.py · --check 0 · 다른 칸 · 시간당 비용 그대로)
- 계산기 짝 master_doc_work.py ↔ common_util_formula_doc_work.ts: 기계 줄 수량에서 시간당 작업량 자리 읽기(일당 = 시공량 ÷ (8 × 대수) · 식 = …/hr 나눔 변수) · 셈 답 줄 칸 `작업량` · 덮음 `작업량: {방식 시간당, 식, 변수?}` = 그 자리를 1 ÷ Q(기계 작업량 식)로 바꿔 셈 · 부착 기계(브레이커 · 집게) 줄 따로 · 식 없는 기계 · 단위 안 맞음 = 시간당 거절
- 덮음 짝(master_doc_cover · common_util_formula_doc_cover): 작업량은 기계 식 문서에 안 얹음 · prune 이 작업량 정리(일당 · 기본과 같은 변수 = 지움) · 몸 검사 · CoverEntry · CoverBase 꼴
- 저장 길: 사본 부름 묶음에 시간당 식 키(M02_Doc_Copy.calls) · 내 기계 clean 이 작업량 받음 · 연결 읽기 작업량 칸(master_doc_link)
- 기계 보기: 작업량 칸 = 제어 구역 몫(ownedCols — 바탕 칸 숫자 입력으로 안 그림 · 그리기는 846-2)
- 지킴: 1,520 문서 · 기계 655 대 · 견본 SK 금액 전후 다름 0 · 빌더 802-2 --check 1,520 같음(옵션 표본 18,873) · 둘 다 가능 줄 전수 시간당 셈 멈춤 0
- 시험: test_846_1_machine_work(일당 · 시간당 손셈 · E · K 덮음 · 정리 · 거절 · 부착 · 거울 · 저장 길 · 내 기계) 신설 · test_816_1 기대값 고침(까닭 — 4륜 덤프 1톤 기계 줄에 작업량 칸이 생겨 값 칸 비교에서 뺌)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
434 lines
17 KiB
TypeScript
434 lines
17 KiB
TypeScript
/* =============================================================================
|
|
* common_util_formula_doc_work.ts
|
|
* 기계 줄 시간당 작업량(PLAN 846-1 · 요구 881 · 882 · 883) — 기계 줄 수량을 「1 ÷ 시간당 작업량」 으로 읽고
|
|
* 단가산출 덮음으로 그 기계의 작업량 식(시간당 방식)으로 바꿔 셈.
|
|
* 읽기(`sites`) — 기계 줄 수량 식의 시간당 작업량 자리(저장 식 글은 안 바꿈 · 만약 갈래 안도 찾음):
|
|
* 일당 = 곱 · 나눔 줄에 8 과 나눔 하나(시공량 D) → 시간당 작업량 = D ÷ (8 × 대수)
|
|
* (대수 = 곱의 다른 변수 — 단위 % · `r_` 율은 곱에 남김)
|
|
* 식 = 나눔으로만 쓰는 변수 · 단위 「…/hr」(작업량 식 부품 · 표 값 · 빈 단위 = 정의 식에서 짐작)
|
|
* 기계 값 칸 `작업량` = [{식: 작업량 틀 키, 단위, 변수: {기계 규격에서 정해지는 칸}}](빌더 build_기계_작업량_846_1.py)
|
|
* 덮음 = `기계덮음` 기계 칸 `작업량: {방식: '시간당', 식, 변수?}` — 그 기계 줄의 작업량 자리를 `1 ÷ Q` 로 바꿈
|
|
* (Q = 그 식을 식 문서 기본 › 기계 변수 › 덮음 변수로 셈) · 다른 곱(율 등) 그대로 · 못 함 = 그 줄 멈춤(까닭)
|
|
* 부착 기계(브레이커 · 집게 입력) 줄 = 그 부착 기계 열쇠(굴착기 줄과 따로)
|
|
* 셈 답 기계 줄 칸 `작업량` = {기계, 꼴(일당 · 식 · 시간당), 율, 값, 단위, 식들, 변수?, 까닭?}(산출근거 자료)
|
|
* 파이썬 · TS 짝 — 짝 = resources/master_data/scripts/master_doc_work.py · 거울 시험 test_846_1_machine_work.py
|
|
* · 계산기(`common_util_formula_doc_calc.ts` money)가 줄마다 `lineWork` 를 부름 · 화면 · DOM · fetch 없음
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
names,
|
|
parse,
|
|
textOf,
|
|
varId,
|
|
walk,
|
|
type DocVar,
|
|
type FormulaDoc,
|
|
type Mark,
|
|
type Node,
|
|
} from "./common_util_formula_doc";
|
|
import type { Cover } from "./common_util_formula_doc_cover";
|
|
|
|
export const WORK = "작업량";
|
|
export const HOURLY = "시간당";
|
|
export const DAILY = "일당";
|
|
export const Q_NEW = "Q_시간당작업량"; // 셈 범위 이름(문서 기호 아님)
|
|
export const NOT_MACHINE = ["지역", "보정작업", "조종원직종", "계약종별"]; // 기계 식 입력 가운데 기계 아닌 것
|
|
|
|
/** 기계 값 칸 `작업량` 한 줄 */
|
|
export interface WorkSpec {
|
|
식: string;
|
|
단위?: string;
|
|
변수?: Record<string, unknown>;
|
|
}
|
|
/** 덮음 `작업량` */
|
|
export interface WorkCover {
|
|
방식: string;
|
|
식?: string;
|
|
변수?: Record<string, unknown>;
|
|
}
|
|
/** 셈 답 기계 줄 `작업량` */
|
|
export interface WorkInfo {
|
|
기계: string;
|
|
꼴: string;
|
|
율: string;
|
|
값: number | null;
|
|
단위: string;
|
|
식들: string[];
|
|
변수?: Record<string, unknown>;
|
|
까닭?: string;
|
|
}
|
|
interface Site {
|
|
꼴: string;
|
|
노드: Node;
|
|
단위: string;
|
|
율: string;
|
|
뺌?: Set<number>;
|
|
}
|
|
type Fac = [number, Node];
|
|
|
|
export const normUnit = (u: unknown): string =>
|
|
String(u ?? "")
|
|
.trim()
|
|
.replace(/m3/g, "㎥")
|
|
.replace(/m2/g, "㎡")
|
|
.replace(/\/(시간|h|Hr|HR)$/, "/hr");
|
|
|
|
const varOf = (doc: FormulaDoc, name: string): DocVar | undefined =>
|
|
(doc.변수 ?? []).find((v) => varId(v) === name);
|
|
|
|
function parseOr(text: unknown): Node | null {
|
|
try {
|
|
return parse(text === null || text === undefined || text === "" ? "1" : text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** 문서 결과 단위(원/X · 원/100㎡) → X/hr · 모름 = "" */
|
|
function perHour(doc: FormulaDoc): string {
|
|
const t = String(doc.결과단위 ?? "");
|
|
const low = t.includes("/")
|
|
? t
|
|
.split("/")
|
|
.slice(1)
|
|
.join("/")
|
|
.replace(/^[\d.,]+/, "")
|
|
.trim()
|
|
: "";
|
|
return low ? `${normUnit(low)}/hr` : "";
|
|
}
|
|
|
|
/** 변수 단위 — 빈 칸이면 정의 식에서 짐작(…/hr 변수를 씀 · 3600 이 든 식 = 문서 결과 단위/hr) */
|
|
export function unitOf(doc: FormulaDoc, name: string, depth = 0): string {
|
|
const u = normUnit(varOf(doc, name)?.단위);
|
|
const line = (doc.식 ?? []).find((x) => x.기호 === name);
|
|
if (u || depth > 3 || !line) return u;
|
|
const node = parseOr(line.식);
|
|
if (!node) return "";
|
|
for (const n of names(node)) {
|
|
const got = unitOf(doc, n, depth + 1);
|
|
if (got.endsWith("/hr")) return got;
|
|
}
|
|
return [...walk(node)].some((x) => x[0] === "num" && x[1] === 3600) ? perHour(doc) : "";
|
|
}
|
|
|
|
const held = (doc: FormulaDoc, name: string): boolean =>
|
|
name.startsWith("r_") || unitOf(doc, name) === "%";
|
|
|
|
// ── 식 나무 ───────────────────────────────────────────────────────────
|
|
const isChain = (n: Node): boolean => n[0] === "bin" && (n[1] === "*" || n[1] === "/");
|
|
|
|
function factors(node: Node, sign = 1, out: Fac[] = []): Fac[] {
|
|
if (node[0] === "bin" && (node[1] === "*" || node[1] === "/")) {
|
|
factors(node[2], sign, out);
|
|
factors(node[3], node[1] === "*" ? sign : -sign, out);
|
|
} else out.push([sign, node]);
|
|
return out;
|
|
}
|
|
|
|
/** (부호, 노드) 목록 → 곱 · 나눔 나무(차례 그대로 · 곱 먼저) */
|
|
function build(fs: Fac[]): Node {
|
|
const tops = fs.filter(([s]) => s > 0).map(([, n]) => n);
|
|
let node: Node = tops.length ? tops[0] : ["num", 1, "1"];
|
|
for (const n of tops.slice(1)) node = ["bin", "*", node, n];
|
|
for (const [s, n] of fs) if (s < 0) node = ["bin", "/", node, n];
|
|
return node;
|
|
}
|
|
|
|
function kids(node: Node): Node[] {
|
|
switch (node[0]) {
|
|
case "call":
|
|
return node[2];
|
|
case "bin":
|
|
return [node[2], node[3]];
|
|
case "neg":
|
|
case "pct":
|
|
case "field":
|
|
return [node[1]];
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function uses(node: Node, sign: number, out: Map<string, number[]>): void {
|
|
if (isChain(node)) {
|
|
for (const [s, n] of factors(node))
|
|
if (n[0] === "var") out.set(n[1], [...(out.get(n[1]) ?? []), s * sign]);
|
|
else uses(n, 0, out);
|
|
return;
|
|
}
|
|
if (node[0] === "var") {
|
|
out.set(node[1], [...(out.get(node[1]) ?? []), 0]);
|
|
return;
|
|
}
|
|
for (const k of kids(node)) uses(k, 0, out);
|
|
}
|
|
|
|
function rates(doc: FormulaDoc, node: Node): Set<string> {
|
|
const got = new Map<string, number[]>();
|
|
uses(node, 1, got);
|
|
return new Set(
|
|
[...got].filter(([n, signs]) => signs.every((s) => s < 0) && unitOf(doc, n).endsWith("/hr")).map(([n]) => n), // prettier-ignore
|
|
);
|
|
}
|
|
|
|
/** 곱 · 나눔 줄 하나 → 일당 자리 | null */
|
|
function daily(doc: FormulaDoc, node: Node, rs: Set<string>): Site | null {
|
|
const fs = factors(node);
|
|
const eight = fs.flatMap(([s, n], i) => (s > 0 && n[0] === "num" && n[1] === 8 ? [i] : []));
|
|
const den = fs.flatMap(([s, n], i) => (s < 0 && n[0] !== "num" ? [i] : [])); // 수 나눔(÷ 100) = 곱에 남김
|
|
if (!eight.length || den.length !== 1) return null;
|
|
const d = fs[den[0]][1];
|
|
if (d[0] === "var" && rs.has(d[1])) return null;
|
|
const crew = fs.flatMap(([s, n], i) => (s > 0 && n[0] === "var" && !held(doc, n[1]) ? [i] : []));
|
|
let per: Node = ["num", 8, "8"];
|
|
for (const i of crew) per = ["bin", "*", per, fs[i][1]];
|
|
const rate: Node = ["bin", "/", d, per];
|
|
const ns = names(d);
|
|
const u = ns.length === 1 ? unitOf(doc, ns[0]) : "";
|
|
return { 꼴: DAILY, 노드: rate, 단위: u ? `${u}/hr` : perHour(doc), 율: "", 뺌: new Set([eight[0], den[0], ...crew]) }; // prettier-ignore
|
|
}
|
|
|
|
type Test = (cond: Node) => boolean | null;
|
|
|
|
function find(doc: FormulaDoc, node: Node, rs: Set<string>, test: Test | null, out: Site[]): void {
|
|
if (node[0] === "call" && node[1] === "만약" && test && node[2].length === 3) {
|
|
const t = test(node[2][0]);
|
|
if (t !== null) return find(doc, t ? node[2][1] : node[2][2], rs, test, out);
|
|
}
|
|
if (isChain(node)) {
|
|
const site = daily(doc, node, rs);
|
|
if (site) {
|
|
out.push(site);
|
|
return;
|
|
}
|
|
for (const [s, n] of factors(node))
|
|
if (s < 0 && n[0] === "var" && rs.has(n[1]))
|
|
out.push({ 꼴: "식", 노드: n, 단위: unitOf(doc, n[1]), 율: "" });
|
|
else if (n[0] !== "var") find(doc, n, rs, test, out);
|
|
return;
|
|
}
|
|
for (const k of kids(node)) find(doc, k, rs, test, out);
|
|
}
|
|
|
|
/** 수량 글 → 시간당 작업량 자리 [{꼴, 노드, 율(글), 단위}] · test(조건 노드) → 참 · 거짓 · null(두 갈래 다) */
|
|
export function sites(doc: FormulaDoc, text: unknown, test: Test | null = null): Site[] {
|
|
const node = parseOr(text);
|
|
if (!node) return [];
|
|
const out: Site[] = [];
|
|
find(doc, node, rates(doc, node), test, out);
|
|
for (const s of out) s.율 = textOf(s.노드);
|
|
return out;
|
|
}
|
|
|
|
function swapNode(doc: FormulaDoc, node: Node, rs: Set<string>): Node {
|
|
if (isChain(node)) {
|
|
const site = daily(doc, node, rs);
|
|
const fs = factors(node);
|
|
if (site) return build([...fs.filter((_, i) => !site.뺌!.has(i)), [-1, ["var", Q_NEW]]]);
|
|
if (fs.some(([s, n]) => s < 0 && n[0] === "var" && rs.has(n[1])))
|
|
return build(fs.map(([s, n]) => [s, s < 0 && n[0] === "var" && rs.has(n[1]) ? ["var", Q_NEW] : n])); // prettier-ignore
|
|
const next: Fac[] = fs.map(([s, n]) => [s, n[0] === "var" ? n : swapNode(doc, n, rs)]);
|
|
return next.every((x, i) => x[1] === fs[i][1]) ? node : build(next);
|
|
}
|
|
const old = kids(node);
|
|
const next = old.map((k) => swapNode(doc, k, rs));
|
|
if (next.every((x, i) => x === old[i])) return node; // 자리 없음 = 그대로(글 차례 안 바뀜)
|
|
switch (node[0]) {
|
|
case "call":
|
|
return ["call", node[1], next];
|
|
case "field":
|
|
return ["field", next[0], node[2]];
|
|
case "bin":
|
|
return ["bin", node[1], next[0], next[1]];
|
|
case "neg":
|
|
case "pct":
|
|
return [node[0], next[0]];
|
|
default:
|
|
return node;
|
|
}
|
|
}
|
|
|
|
/** 수량 글의 작업량 자리(모든 갈래)를 `1 ÷ Q_NEW` 로 바꾼 글 */
|
|
export function swap(doc: FormulaDoc, text: unknown): string {
|
|
const node = parseOr(text);
|
|
return node ? textOf(swapNode(doc, node, rates(doc, node))) : String(text);
|
|
}
|
|
|
|
// ── 기계 작업량 · 덮음 ──────────────────────────────────────────────────
|
|
/** 기계 값 칸 `작업량` → [{식, 단위, 변수}] (꼴 틀림 = 뺌) */
|
|
export const specsOf = (got: unknown): WorkSpec[] =>
|
|
(Array.isArray(got) ? got : []).filter(
|
|
(x): x is WorkSpec => !!x && typeof x === "object" && typeof x.식 === "string" && !!x.식,
|
|
);
|
|
|
|
/** 시간당 작업량 단위에 맞는 식 키(단위 모름 = 전부) */
|
|
export const fits = (specs: WorkSpec[], unit: string): string[] =>
|
|
specs.filter((s) => !unit || normUnit(s.단위) === unit).map((s) => s.식);
|
|
|
|
/** 식 문서 입력 · 선택 기본 › 기계 변수 */
|
|
export function workDefaults(
|
|
tpl: FormulaDoc | null | undefined,
|
|
spec: WorkSpec | null | undefined,
|
|
): Record<string, unknown> {
|
|
const out: Record<string, unknown> = {};
|
|
for (const v of tpl?.변수 ?? []) {
|
|
const link = v.연결 ?? {};
|
|
if (["입력", "선택", "숫자"].includes(String(link.갈래)) && v.열쇠)
|
|
out[String(v.열쇠)] = (link as { 값?: unknown }).값;
|
|
}
|
|
return { ...out, ...(spec?.변수 ?? {}) };
|
|
}
|
|
|
|
const sameVal = (a: unknown, b: unknown): boolean => {
|
|
const x = Number(a);
|
|
const y = Number(b);
|
|
return a !== "" && b !== "" && Number.isFinite(x) && Number.isFinite(y)
|
|
? Math.abs(x - y) <= 1e-9 * Math.max(1, Math.abs(x))
|
|
: String(a) === String(b);
|
|
};
|
|
|
|
/** 작업량 덮음 정리 — 방식 시간당만 · 기본(`workDefaults`)과 같은 변수 뺌 · 남는 것 없음 = null */
|
|
export function pruneWork(
|
|
want: WorkCover | null | undefined,
|
|
base: Record<string, unknown> | null = null,
|
|
): WorkCover | null {
|
|
if (!want || want.방식 !== HOURLY) return null;
|
|
const got = Object.entries(want.변수 ?? {}).filter(
|
|
([k, v]) =>
|
|
v !== null && v !== undefined && v !== "" && !(base && k in base && sameVal(v, base[k])),
|
|
);
|
|
const out: WorkCover = { 방식: HOURLY };
|
|
if (want.식) out.식 = String(want.식);
|
|
if (got.length) out.변수 = Object.fromEntries(got);
|
|
return out;
|
|
}
|
|
|
|
/** 문서 `기계덮음` 의 시간당 작업량 식 키(파이썬 `cover_calls` 짝) */
|
|
export function coverCalls(doc: FormulaDoc | null | undefined): string[] {
|
|
const got = (doc as { 기계덮음?: Cover } | null | undefined)?.기계덮음;
|
|
return Object.values(got && typeof got === "object" ? got : {}).flatMap((e) => {
|
|
const w = (e as { [WORK]?: WorkCover })?.[WORK];
|
|
return w && w.방식 === HOURLY && w.식 ? [String(w.식)] : [];
|
|
});
|
|
}
|
|
|
|
// ── 셈(계산기 money 가 기계 줄마다 부름) ─────────────────────────────────
|
|
type Scope = Record<string, unknown>;
|
|
/** 계산기 셈 한 벌(`Run`)에서 쓰는 몫 */
|
|
export interface WorkRun {
|
|
doc: FormulaDoc;
|
|
vars: Map<string, DocVar>;
|
|
values: Map<string, unknown>;
|
|
marks: Mark[];
|
|
cover: Cover;
|
|
links: { element(p: Record<string, unknown>): unknown; doc(key: string): FormulaDoc | null | undefined }; // prettier-ignore
|
|
expr(text: unknown, where?: string, scope?: Scope): unknown;
|
|
ev(node: Node, scope: Scope | undefined, where: string): unknown;
|
|
sub(p: Record<string, unknown>, name: string): unknown;
|
|
}
|
|
/** 계산기 몫(계산기 → 이 파일 부름만 · 거꾸로 import 없음) */
|
|
export interface WorkKit {
|
|
Stop: new (why: string, kind?: string, sign?: string) => { why: string };
|
|
Gap: new (...a: never[]) => Error;
|
|
Broken: new (...a: never[]) => Error;
|
|
bad: (v: unknown) => boolean;
|
|
plainText: (v: unknown) => string;
|
|
}
|
|
|
|
function machineOf(run: WorkRun, line: { 단가?: unknown }): string | null {
|
|
const node = parseOr(line.단가);
|
|
for (const name of node ? names(node) : []) {
|
|
const link = (run.vars.get(name)?.연결 ?? {}) as { 갈래?: string; 입력?: Record<string, unknown> }; // prettier-ignore
|
|
const given = link.입력 ?? {};
|
|
if (link.갈래 === "단가산출" && "기계" in given) {
|
|
const tool = Object.entries(given).filter(([k]) => k !== "기계" && !NOT_MACHINE.includes(k));
|
|
return String(tool.length ? tool[tool.length - 1][1] : given.기계);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 기계 줄 하나 → [바꾼 수량 글 | 멈춤(표지는 계산기가 담) | null, 범위 더함, 답 칸 | null] */
|
|
export function lineWork(
|
|
run: WorkRun,
|
|
line: { 이름?: unknown; 수량?: unknown; 단가?: unknown },
|
|
scope: Scope,
|
|
kit: WorkKit,
|
|
): [string | { why: string } | null, Record<string, unknown>, WorkInfo | null] {
|
|
const expr = machineOf(run, line);
|
|
if (expr === null) return [null, {}, null];
|
|
const where = String(line.이름 ?? "");
|
|
/** 흔적 없는 셈 — 표지 · 기억한 값을 되돌림(진짜 셈이 표지를 다시 냄) */
|
|
const quiet = <T>(fn: () => T): T => {
|
|
const n = run.marks.length;
|
|
const seen = new Set(run.values.keys());
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
run.marks.length = n;
|
|
for (const k of [...run.values.keys()]) if (!seen.has(k)) run.values.delete(k);
|
|
}
|
|
};
|
|
const keyVal = quiet(() => run.expr(expr, where, scope));
|
|
if (kit.bad(keyVal)) return [null, {}, null];
|
|
const raw = kit.plainText(keyVal).trim();
|
|
const key = /^'.*'$/.test(raw) ? raw.slice(1, -1) : raw; // 덮음 기계 열쇠(cover machineKey 와 같음)
|
|
const test: Test = (cond) => {
|
|
const v = quiet(() => run.ev(cond, scope, where));
|
|
return kit.bad(v) ? null : !!v;
|
|
};
|
|
const found = sites(run.doc, line.수량, test);
|
|
const want = (run.cover?.[key] as { [WORK]?: WorkCover } | undefined)?.[WORK];
|
|
const hourly = !!want && want.방식 === HOURLY;
|
|
if (!found.length && !hourly) return [null, {}, null];
|
|
let specs: WorkSpec[] = [];
|
|
try {
|
|
specs = specsOf(run.links.element({ 키: `EQ:${key}`, 칸: WORK }));
|
|
} catch (e) {
|
|
if (!(e instanceof kit.Gap) && !(e instanceof kit.Broken)) throw e;
|
|
}
|
|
const unit = found.length ? found[0].단위 : "";
|
|
const info: WorkInfo = {
|
|
기계: key, 꼴: found.length ? found[0].꼴 : "", 율: found.length ? found[0].율 : "",
|
|
값: null, 단위: unit, 식들: fits(specs, unit),
|
|
}; // prettier-ignore
|
|
if (!hourly) {
|
|
const v = quiet(() => run.ev(found[0].노드, scope, where));
|
|
info.값 = typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
return [null, {}, info];
|
|
}
|
|
const pick = String(want!.식 ?? "") || (info.식들[0] ?? "");
|
|
const spec = specs.find((s) => s.식 === pick);
|
|
const why = !specs.length
|
|
? "시간당 작업량 식 없는 기계"
|
|
: !found.length
|
|
? "시간당 작업량 자리 없는 줄"
|
|
: !spec || !info.식들.includes(pick)
|
|
? `단위 ${unit} 맞는 작업량 식 없음`
|
|
: "";
|
|
info.꼴 = HOURLY;
|
|
info.율 = pick;
|
|
if (why) {
|
|
info.까닭 = why;
|
|
return [new kit.Stop(`${where} 시간당 거절 — ${why}`, "시간당 거절", where), {}, info];
|
|
}
|
|
const given = { ...(spec!.변수 ?? {}), ...(want!.변수 ?? {}) };
|
|
let q: unknown;
|
|
let tpl: FormulaDoc | null | undefined = null;
|
|
try {
|
|
q = run.sub({ 키: pick, 입력: given }, where);
|
|
tpl = run.links.doc(pick);
|
|
} catch (e) {
|
|
if (!(e instanceof kit.Gap) && !(e instanceof kit.Broken)) throw e;
|
|
q = new kit.Stop(e.message);
|
|
}
|
|
info.변수 = { ...workDefaults(tpl, spec), ...(want!.변수 ?? {}) };
|
|
if (typeof q !== "number" || !Number.isFinite(q) || q <= 0) {
|
|
info.까닭 = `작업량 식 ${pick} 못 셈${kit.bad(q) ? ` — ${(q as { why: string }).why}` : ""}`;
|
|
return [new kit.Stop(`${where} ${info.까닭}`, "시간당 거절", where), {}, info];
|
|
}
|
|
info.값 = q;
|
|
return [swap(run.doc, line.수량), { [Q_NEW]: q }, info];
|
|
}
|