- 빌더 build_기계_인력줄_842_3.py(규칙 = master_machine_crew.from_old_row · 다시 돌려도 같음) - 인력 줄 301 대(1 인 282 · 2 인 18 · 7 인 1 · 직종 빈 줄 42 = 빈 키 = 식 입력 직종 고르기 그대로) - 잡품 줄 328 대(잡재료율 > 0 = 주연료 · 전력 줄마다 그 율 · 「73.7+」 글 연료도 줄) · 범위 글 「13∼18」 2 대 = 잡품 없음(값원문 그대로) - 선원류 칸 79 대 · 비활성 표지 79 대 그대로 · 옛 칸 남음 0 - 바탕 덮음 = 식에 그 줄이 있을 때만(손료만 식을 일부러 부른 단가산출 GC000324 → GC000273 크롤러드릴 = 조종원 안 더함) - 식 고름 = 그 줄 인력도 조종원 있음으로 봄 · 로직 검사 · 값 모으기 = 기계 칸을 목록 읽기로(master_formula_check) - 시험 고침: 821_2 · 822_2 미리보기 = 목록 읽은 값으로 · 822_1 노임 키 = 인력 줄 · 822_2 범위 까닭 글 = 「잡재료율 없음」(범위 글은 값원문) - 전후: 655 대 시간당(hourly · brief · 문서 셈) 653 같음 · 다름 2 = 펌프 준설선 EQ000552 · 553(비활성 · 범위 글) 까닭 글만 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
412 lines
18 KiB
TypeScript
412 lines
18 KiB
TypeScript
/* =============================================================================
|
||
* common_util_formula_doc_cover.ts
|
||
* 기계 덮음(PLAN 836-1 · 요구 846 · 849 · 850 · 853) — 단가산출 안 기계 내부 값 고침(사본 줄은 그대로 · 고친 것만 따로)
|
||
* 자리 = 문서 칸 `기계덮음` {기계 입력 글: {값?: {칸: 값}, 조종원?: [{키, 인}], 잡품?: [{줄? | 키?, 이름?, 수량?, 율}]}}
|
||
* 기계 입력 글 = 부름 입력 `기계` 값(따옴표 뗌 · 원문번호 · 개인 EQP…) — 같은 기계 줄 여럿 = 한 벌
|
||
* 값 = 기계 식이 읽는 기계 칸(요소 `EQ:{S_기계}` 칸 — 조종원 · 조종원직종 · 잡재료율 빼고) · 조종원 = 인력 줄(노임 키 · 인/일) ·
|
||
* 잡품 = 재료 줄(율 % · 줄 = 그 금액 줄 단가 · 수량 따름(기본) · 키 = 고른 재료 요소) · 원본과 같은 칸 = 없음(`prune`)
|
||
* 셈 규칙(`coverDoc`) = 계산기가 기계 식을 부를 때 그 기계 덮음을 얹은 문서로 셈 — 덮음 없음 = 문서 그대로(금액 전후 같음)
|
||
* 기계 줄 목록(842-3 · `common_util_machine_crew`) = 바탕 덮음(`rowEntry` — 식 한 줄로 안 풀리는 부분만) ·
|
||
* 단가산출 덮음이 그 위에 부분별로 덮음(`mergeCover` — 값 = 칸별 · 조종원 · 잡품 = 목록 통째)
|
||
* 파이썬 · TS 짝 — 짝 = resources/master_data/scripts/master_doc_cover.py · 거울 시험 test_836_1_machine_cover.py ·
|
||
* test_842_3_machine_crew.py
|
||
* · 화면 · DOM · fetch 없음
|
||
* ========================================================================== */
|
||
|
||
import {
|
||
names,
|
||
parse,
|
||
symId,
|
||
varId,
|
||
type DocVar,
|
||
type FormulaDoc,
|
||
type MoneyLine,
|
||
} from "./common_util_formula_doc";
|
||
import { CREW, MISC, code as jobOf } from "./common_util_machine_crew";
|
||
|
||
export const COVER = "기계덮음";
|
||
/** 값 칸에서 빼는 기계 칸 — 줄(조종원 · 잡품)로 다룸 */
|
||
export const NOT_VALUE = ["조종원", "조종원직종", "잡재료율"];
|
||
const HOUR = "Q_시간환산";
|
||
/** 조종원 줄을 새로 넣을 때(조종원 없는 식) 시간환산 — GC000267 과 같은 꼴(1/8 × 16/12 × 25/20 · 건협기술-2230) */
|
||
const HOUR_VARS: DocVar[] = [
|
||
{
|
||
열쇠: "Q_분자^표", 기호: "Q", 아래: "분자", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분자", 단위: "",
|
||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'시간환산'" }, 칸: "분자" },
|
||
},
|
||
{
|
||
열쇠: "Q_분모^표", 기호: "Q", 아래: "분모", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분모", 단위: "",
|
||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'시간환산'" }, 칸: "분모" },
|
||
},
|
||
{
|
||
열쇠: "Q_분자2^표", 기호: "Q", 아래: "분자2", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분자", 단위: "",
|
||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'상여계수'" }, 칸: "분자" },
|
||
},
|
||
{
|
||
열쇠: "Q_분모2^표", 기호: "Q", 아래: "분모2", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분모", 단위: "",
|
||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'상여계수'" }, 칸: "분모" },
|
||
},
|
||
{
|
||
열쇠: "Q_분자3^표", 기호: "Q", 아래: "분자3", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분자", 단위: "",
|
||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'휴지계수'" }, 칸: "분자" },
|
||
},
|
||
{
|
||
열쇠: "Q_분모3^표", 기호: "Q", 아래: "분모3", 위: "표", 이름: "건설기계 조종원 노임 시간 환산 계수.분모", 단위: "",
|
||
연결: { 갈래: "표", 표: "CC000336", 조건: { 구분: "'휴지계수'" }, 칸: "분모" },
|
||
},
|
||
{ 열쇠: "시간환산", 기호: "Q", 아래: "시간환산", 위: "", 이름: "시간환산", 단위: "", 연결: { 갈래: "식" } },
|
||
]; // prettier-ignore
|
||
const HOUR_LINE = {
|
||
기호: HOUR,
|
||
식: "Q_분자^표 ÷ Q_분모^표 × Q_분자2^표 ÷ Q_분모2^표 × Q_분자3^표 ÷ Q_분모3^표",
|
||
};
|
||
|
||
export interface CrewRow {
|
||
키: string;
|
||
인: number;
|
||
/** 화면 이름(직종) — 셈 · 원본 대조 안 봄 */
|
||
이름?: string;
|
||
}
|
||
export interface MiscRow {
|
||
줄?: string;
|
||
키?: string;
|
||
이름?: string;
|
||
단위?: string;
|
||
수량?: number;
|
||
율: number;
|
||
}
|
||
export interface MachineCover {
|
||
값?: Record<string, number | string>;
|
||
조종원?: CrewRow[];
|
||
잡품?: MiscRow[];
|
||
}
|
||
export type Cover = Record<string, MachineCover>;
|
||
type Holder = FormulaDoc & { [COVER]?: Cover };
|
||
|
||
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
||
const plain = (n: number): string => String(Number(n.toPrecision(15)));
|
||
|
||
export const coverOf = (doc: FormulaDoc | null | undefined): Cover => {
|
||
const got = (doc as Holder | null | undefined)?.[COVER];
|
||
return got && typeof got === "object" && !Array.isArray(got) ? got : {};
|
||
};
|
||
|
||
/** 부름 입력 → 기계 열쇠(따옴표 뗌) · 기계 입력 없음 = "" */
|
||
export function machineKey(given: Record<string, unknown> | null | undefined): string {
|
||
const v = given?.기계;
|
||
if (v === undefined || v === null) return "";
|
||
const t = String(v).trim();
|
||
return /^'.*'$/.test(t) ? t.slice(1, -1) : t;
|
||
}
|
||
|
||
const namesOf = (text: unknown): string[] => {
|
||
if (text === null || text === undefined || String(text).trim() === "") return [];
|
||
try {
|
||
return names(parse(text));
|
||
} catch {
|
||
return [];
|
||
}
|
||
};
|
||
|
||
export interface CoverParts {
|
||
/** 값 칸 — 칸 이름 · 그 칸을 읽는 기호(차례 = 문서 변수 차례) */
|
||
fields: { 칸: string; ids: string[] }[];
|
||
/** 조종원 금액 줄 차례 · 사람 수 기호 */
|
||
crew: number[];
|
||
crewQty: Record<number, string>;
|
||
/** 잡품(잡재료비) 금액 줄 차례 · 율 기호 · 밑 재료 줄 차례 */
|
||
misc: number | null;
|
||
rate: string | null;
|
||
base: number[];
|
||
}
|
||
|
||
/** 기계 식 짜임 — 기계 칸 요소(`EQ:…{S_기계}…`) · 조종원 줄(노무비 · 수량이 칸 「조종원」) · 잡품 줄(재료비 누계 × 율) */
|
||
export function partsOf(doc: FormulaDoc): CoverParts {
|
||
const eq = new Map<string, string>();
|
||
for (const v of doc.변수 ?? []) {
|
||
const l = v.연결 ?? {};
|
||
const k = String(l.키 ?? "");
|
||
if (l.갈래 === "요소" && k.startsWith("EQ:") && k.includes("{S_기계}") && l.칸)
|
||
eq.set(varId(v), l.칸);
|
||
}
|
||
const fields: CoverParts["fields"] = [];
|
||
for (const [id, col] of eq) {
|
||
if (NOT_VALUE.includes(col)) continue;
|
||
const at = fields.find((f) => f.칸 === col);
|
||
if (at) at.ids.push(id);
|
||
else fields.push({ 칸: col, ids: [id] });
|
||
}
|
||
const lines = doc.금액 ?? [];
|
||
const crew: number[] = [];
|
||
const crewQty: Record<number, string> = {};
|
||
let misc: number | null = null;
|
||
let rate: string | null = null;
|
||
lines.forEach((l, i) => {
|
||
const q = namesOf(l.수량).find((n) => eq.get(n) === "조종원");
|
||
if (l.비목 === "노무비" && q) {
|
||
crew.push(i);
|
||
crewQty[i] = q;
|
||
}
|
||
const p = namesOf(l.단가);
|
||
const other = p.filter((n) => n !== "재료비");
|
||
if (misc === null && l.비목 === "재료비" && p.includes("재료비") && other.length === 1) {
|
||
misc = i;
|
||
rate = other[0];
|
||
}
|
||
});
|
||
const base =
|
||
misc === null
|
||
? []
|
||
: lines.flatMap((l, i) => (i < misc! && l.비목 === "재료비" && !l.합밖 ? [i] : []));
|
||
return { fields, crew, crewQty, misc, rate, base };
|
||
}
|
||
|
||
type Vals = Record<string, unknown>;
|
||
const num = (v: unknown): number | null => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
||
/** 요소 키 글의 구멍을 값으로 채움(`LB:{S_직종}` → `LB:1048`) */
|
||
const fillKey = (text: string, vals: Vals): string =>
|
||
text.replace(/\{([^{}]+)\}/g, (_, h: string) => {
|
||
const v = vals[h];
|
||
return typeof v === "number" ? plain(v) : String(v ?? "");
|
||
});
|
||
|
||
/** 원본(단가산출 덮음 없음) 기본값 — 셈 답 값(`vals` = 그 기계로 셈한 원본 답 `값`) ·
|
||
* `base` = 기계 줄 바탕 덮음(`rowEntry`) — 있는 부분은 그 목록(빈 직종 키 = 식이 고른 직종) */
|
||
export function defaultsOf(
|
||
doc: FormulaDoc,
|
||
vals: Vals,
|
||
base?: MachineCover | null,
|
||
): Required<MachineCover> {
|
||
const parts = partsOf(doc);
|
||
const lines = doc.금액 ?? [];
|
||
const vars = new Map((doc.변수 ?? []).map((v) => [varId(v), v]));
|
||
const 값: Record<string, number | string> = {};
|
||
for (const f of parts.fields) {
|
||
const v = vals[f.ids[0]];
|
||
if (typeof v === "number" || typeof v === "string") 값[f.칸] = v;
|
||
}
|
||
const 조종원 = parts.crew.flatMap((i) => {
|
||
const n = num(vals[parts.crewQty[i]]);
|
||
const sym = namesOf(lines[i].단가)[0];
|
||
const key = String(vars.get(sym ?? "")?.연결?.키 ?? "");
|
||
return n && key ? [{ 키: fillKey(key, vals), 인: n }] : [];
|
||
});
|
||
const r = parts.rate ? num(vals[parts.rate]) : null;
|
||
let 잡품: MiscRow[] = r
|
||
? parts.base.map((i) => ({ 줄: String(lines[i].이름 ?? ""), 율: r }))
|
||
: [];
|
||
let crew: CrewRow[] = 조종원;
|
||
if (base?.조종원) {
|
||
const own = 조종원[0]?.키 ?? "";
|
||
crew = base.조종원.filter((x) => x.키 || own).map((x) => ({ ...x, 키: x.키 || own }));
|
||
}
|
||
if (base?.잡품) 잡품 = base.잡품;
|
||
return { 값, 조종원: crew, 잡품 };
|
||
}
|
||
|
||
/** 원본 대조 꼴 — 이름 · 단위(화면 글) · 빈 칸 뺌 · 수 12 자리 */
|
||
const LABEL = ["이름", "단위"];
|
||
const canon = (v: unknown): string =>
|
||
JSON.stringify(v, (_, x: unknown) =>
|
||
x && typeof x === "object" && !Array.isArray(x)
|
||
? Object.fromEntries(
|
||
Object.entries(x as Record<string, unknown>)
|
||
.filter(([k, y]) => y !== undefined && y !== null && y !== "" && !LABEL.includes(k))
|
||
.sort(([a], [b]) => (a < b ? -1 : 1)),
|
||
)
|
||
: typeof x === "number"
|
||
? Number(x.toPrecision(12))
|
||
: x,
|
||
);
|
||
const sameVal = (a: unknown, b: unknown): boolean =>
|
||
typeof a === "number" && typeof b === "number"
|
||
? Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b))
|
||
: String(a) === String(b);
|
||
|
||
/** 원본과 같은 칸 · 줄을 뺀 덮음(바꿨다 되돌림 = 안 고침) · 다 빠지면 null */
|
||
export function prune(entry: MachineCover, base: Required<MachineCover>): MachineCover | null {
|
||
const out: MachineCover = {};
|
||
const vals = Object.entries(entry.값 ?? {}).filter(
|
||
([k, v]) =>
|
||
v !== null && v !== undefined && v !== "" && !(k in base.값 && sameVal(v, base.값[k])),
|
||
);
|
||
if (vals.length) out.값 = Object.fromEntries(vals);
|
||
if (entry.조종원 && canon(entry.조종원) !== canon(base.조종원)) out.조종원 = entry.조종원;
|
||
if (entry.잡품 && canon(entry.잡품) !== canon(base.잡품)) out.잡품 = entry.잡품;
|
||
return Object.keys(out).length ? out : null;
|
||
}
|
||
|
||
/** 덮음 한 기계 고침 → 문서 칸(원본 대조로 정리 · 비면 칸 지움) */
|
||
export function putCover(
|
||
doc: FormulaDoc,
|
||
machine: string,
|
||
entry: MachineCover,
|
||
base: Required<MachineCover>,
|
||
): void {
|
||
const box = doc as Holder;
|
||
const all = { ...coverOf(doc) };
|
||
const kept = prune(entry, base);
|
||
if (kept) all[machine] = kept;
|
||
else delete all[machine];
|
||
if (Object.keys(all).length) box[COVER] = all;
|
||
else delete box[COVER];
|
||
}
|
||
|
||
// ── 셈 규칙 ───────────────────────────────────────────────────────────
|
||
/** 새 요소 기호(`w_조종원1` · `P_잡품1` — 문서에 없는 첫 번호) */
|
||
function addVar(doc: FormulaDoc, sign: string, low: string, name: string, key: string): string {
|
||
const have = new Set([
|
||
...(doc.변수 ?? []).map(varId),
|
||
...(doc.식 ?? []).map((l) => String(l.기호 ?? "")),
|
||
]);
|
||
let n = 1;
|
||
while (have.has(symId(sign, `${low}${n}`))) n++;
|
||
const 아래 = `${low}${n}`;
|
||
const id = symId(sign, 아래);
|
||
(doc.변수 ??= []).push({
|
||
열쇠: id, 기호: sign, 아래, 위: "", 이름: name, 단위: "", 연결: { 갈래: "요소", 키: key },
|
||
}); // prettier-ignore
|
||
return id;
|
||
}
|
||
|
||
/** 조종원 줄 — 빈 키 줄 = 식의 조종원 단가 기호(`own` · 식 입력 직종) · 없으면 뺌 */
|
||
function crewLines(doc: FormulaDoc, all: CrewRow[], own = ""): MoneyLine[] {
|
||
const rows = all.filter((r) => r.키 || own);
|
||
const have = (doc.변수 ?? []).some((v) => varId(v) === HOUR) || (doc.식 ?? []).some((l) => l.기호 === HOUR); // prettier-ignore
|
||
if (rows.length && !have) {
|
||
const ids = new Set((doc.변수 ?? []).map(varId));
|
||
for (const v of HOUR_VARS) if (!ids.has(varId(v))) (doc.변수 ??= []).push(clone(v));
|
||
(doc.식 ??= []).push({ ...HOUR_LINE });
|
||
}
|
||
return rows.map((r) => ({
|
||
이름: "조종원",
|
||
...(r.이름 ? { 규격: r.이름 } : {}),
|
||
단위: "인",
|
||
비목: "노무비",
|
||
수량: `${plain(r.인)} × ${HOUR}`,
|
||
단가: r.키 ? addVar(doc, "w", "조종원", "조종원 노임", r.키) : own,
|
||
}));
|
||
}
|
||
|
||
function miscLines(doc: FormulaDoc, rows: MiscRow[], lines: MoneyLine[]): MoneyLine[] {
|
||
return rows.flatMap((r) => {
|
||
let price: string;
|
||
let qty: string;
|
||
let label = r.이름 ?? "";
|
||
if (r.키) {
|
||
price = addVar(doc, "P", "잡품", label || r.키, r.키);
|
||
qty = plain(r.수량 ?? 1);
|
||
} else {
|
||
const b = lines.find((l) => String(l.이름 ?? "") === r.줄);
|
||
if (!b || !b.단가) return [];
|
||
price = String(b.단가);
|
||
qty = r.수량 !== undefined && r.수량 !== null ? plain(r.수량) : `(${b.수량 ?? 1})`;
|
||
label ||= String(b.이름 ?? "");
|
||
}
|
||
return [{ 이름: "잡품", 규격: label, 단위: "", 비목: "재료비", 수량: `${qty} × ${plain(r.율)} ÷ 100`, 단가: price } as MoneyLine]; // prettier-ignore
|
||
});
|
||
}
|
||
|
||
/** 기계 식 문서에 그 기계 덮음을 얹음(새 문서) — `base` = 기계 줄 바탕 덮음(`rowEntry`) · 덮음 없음 = 그대로(같은 물건) */
|
||
export function coverDoc(
|
||
doc: FormulaDoc,
|
||
given: Record<string, unknown> | null | undefined,
|
||
cover: Cover | null | undefined,
|
||
base?: MachineCover | null,
|
||
): FormulaDoc {
|
||
const key = machineKey(given);
|
||
const entry = mergeCover(base, key ? cover?.[key] : undefined);
|
||
if (!entry) return doc;
|
||
const out = clone(doc) as FormulaDoc & { 쌓기?: unknown };
|
||
delete out.쌓기; // 금액 차례가 바뀜 — 기본 차림
|
||
const parts = partsOf(out);
|
||
for (const f of parts.fields)
|
||
if (entry.값 && f.칸 in entry.값)
|
||
for (const v of out.변수 ?? [])
|
||
if (f.ids.includes(varId(v))) v.연결 = { 갈래: "숫자", 값: entry.값[f.칸] };
|
||
const lines = out.금액 ?? [];
|
||
const own = parts.crew.length ? String(lines[parts.crew[0]].단가 ?? "") : "";
|
||
const crew = entry.조종원 ? crewLines(out, entry.조종원, own) : null;
|
||
const misc = entry.잡품 && (parts.misc !== null || entry.잡품.length) ? miscLines(out, entry.잡품, lines) : null; // prettier-ignore
|
||
const gone = new Set([...(crew ? parts.crew : []), ...(misc && parts.misc !== null ? [parts.misc] : [])]); // prettier-ignore
|
||
const tail = lines.findIndex((l) => l.비목 === "경비");
|
||
const end = tail < 0 ? lines.length : tail;
|
||
const crewAt = parts.crew.length ? parts.crew[0] : end;
|
||
const miscAt = parts.misc ?? end;
|
||
const next: MoneyLine[] = [];
|
||
for (let i = 0; i <= lines.length; i++) {
|
||
if (crew && i === crewAt) next.push(...crew);
|
||
if (misc && i === miscAt) next.push(...misc);
|
||
if (i < lines.length && !gone.has(i)) next.push(lines[i]);
|
||
}
|
||
out.금액 = next;
|
||
return out;
|
||
}
|
||
|
||
// ── 기계 줄 목록 → 바탕 덮음(842-3) ─────────────────────────────────────
|
||
/** 기계 시간당 식 — 결과 원/시간 · 요소 `EQ:{S_기계}` 칸을 읽음 */
|
||
export const isMachine = (doc: FormulaDoc | null | undefined): boolean =>
|
||
!!doc &&
|
||
doc.결과단위 === "원/시간" &&
|
||
(doc.변수 ?? []).some(
|
||
(v) => v.연결?.갈래 === "요소" && String(v.연결?.키 ?? "").includes("{S_기계}"),
|
||
);
|
||
|
||
/** 기계 줄 목록(`인력` · `잡품`) 가운데 식 한 줄로 안 풀리는 부분 = 바탕 덮음 · 다 풀림 = null ·
|
||
* 식에 그 줄이 없음 = 안 얹음(손료만 식을 일부러 부른 단가산출) */
|
||
export function rowEntry(doc: FormulaDoc, vals: Vals | null | undefined): MachineCover | null {
|
||
if (!vals || !isMachine(doc)) return null;
|
||
const parts = partsOf(doc);
|
||
const lines = doc.금액 ?? [];
|
||
const out: MachineCover = {};
|
||
const crew = vals[CREW];
|
||
if (Array.isArray(crew) && parts.crew.length) {
|
||
const rows = crew as CrewRow[];
|
||
const jobs = new Set(rows.map((r) => jobOf(r?.키)));
|
||
if (!(rows.length && jobs.size === 1)) out.조종원 = rows;
|
||
}
|
||
const misc = vals[MISC];
|
||
if (Array.isArray(misc) && misc.length && parts.misc !== null) {
|
||
const rows = misc as MiscRow[];
|
||
const names = parts.base.map((i) => String(lines[i].이름 ?? "")).sort();
|
||
const plainMisc =
|
||
rows.every((r) => r && r.줄 && !r.키) &&
|
||
rows.every((r) => r.수량 === undefined || r.수량 === null) &&
|
||
new Set(rows.map((r) => canon(r.율))).size === 1 &&
|
||
JSON.stringify(rows.map((r) => String(r.줄)).sort()) === JSON.stringify(names);
|
||
if (!plainMisc) out.잡품 = rows;
|
||
}
|
||
return Object.keys(out).length ? out : null;
|
||
}
|
||
|
||
/** 바탕 덮음 위에 단가산출 덮음 — 값 = 칸별 · 조종원 · 잡품 = 목록 통째(있는 부분만) */
|
||
export function mergeCover(
|
||
base: MachineCover | null | undefined,
|
||
top: MachineCover | null | undefined,
|
||
): MachineCover | null {
|
||
if (!base || !Object.keys(base).length) return top && Object.keys(top).length ? top : null;
|
||
if (!top || !Object.keys(top).length) return base;
|
||
const out: MachineCover = { ...base, ...top };
|
||
const 값 = { ...(base.값 ?? {}), ...(top.값 ?? {}) };
|
||
if (Object.keys(값).length) out.값 = 값;
|
||
else delete out.값;
|
||
return out;
|
||
}
|
||
|
||
/** 셈 연결로 그 기계 줄 목록 읽기 — {인력?, 잡품?} · 못 읽음 = 빈 묶음(요소 `EQ:<기계>` 칸) */
|
||
export function listsOf(
|
||
element: (p: { 키: string; 칸: string }) => unknown,
|
||
given: Record<string, unknown> | null | undefined,
|
||
): Vals {
|
||
const key = machineKey(given);
|
||
const out: Vals = {};
|
||
if (key)
|
||
for (const col of [CREW, MISC])
|
||
try {
|
||
out[col] = element({ 키: `EQ:${key}`, 칸: col });
|
||
} catch {
|
||
/* 못 읽음 = 그 목록 없음 */
|
||
}
|
||
return out;
|
||
}
|