/* ============================================================================= * common_util_machine_sheet.ts * 기계 보기 셈 꼴(PLAN 840-2 · 요구 853 · 862) — 기계 줄(저장 꼴) ↔ 식이 읽는 값 · 덮음(836-1 `기계덮음` 꼴) ↔ 줄 * 셈 꼴(`engineVals`) = M01 = 값 그대로 · 내 기계 = 바로 → 상각비계수 ← 손료계수 · 정비 · 관리 0 · * 직접 → 노무비 · 재료비 · 경비 · 빈 연료 · 조종원 칸 = 0(서버 `M02_MyMachine.engine` 과 같은 꼴) * 덮음 ← 줄 차이(`coverFrom` — 처음 줄 대 지금 줄) · 지금 줄 ← 덮음(`applyCover` — 제어 구역이 맡은 칸만 · * 덮음이 없는 칸 = 처음 줄 값 그대로 = 안 고치고 닫으면 줄 바이트 같음) * 제어 구역이 맡은 칸(`ownedCols`) = 식이 읽는 값 칸 + 조종원 · 조종원직종 + (잡품 줄 있는 식) 잡재료율 * 값 박음(`bindDoc`) = 식 문서 요소 `EQ:{S_기계}` 칸 → 숫자(처음 줄 값) — 셈 차례 = 덮음 얹음(`coverDoc`) → 값 박음 * 파이썬 · TS 짝(`bindDoc`) — 짝 = M02_MasterTemplete/M02_MyMachine_Sheet.py `bind` · 거울 시험 test_840_2_machine_sheet.py * · 화면 · DOM · fetch 없음 * ========================================================================== */ import type { FormulaDoc, MoneyLine } from "./common_util_formula_doc"; import type { CoverParts, MachineCover } from "./common_util_formula_doc_cover"; import { MACHINE_INPUTS } from "./common_util_machine_coef"; export type MachineValues = Record; /** 고르기 목록 한 칸 — 연료(값 = 종류) · 직종(값 = 인력 원문번호) */ export interface MachinePick { value: string; text: string; price: number | null; } export type MachineMode = "바로" | "품셈" | "직접"; /** 기계 줄(M01 기계 줄 · 내 기계 줄 꼴) — 이름 · 규격 · 값 · (내 기계) 방식 · 시간당 */ export interface MachineRow { 이름?: unknown; 규격?: unknown; 값?: MachineValues; 방식?: MachineMode; 시간당?: MachineValues; [k: string]: unknown; } export const HOURLY = ["노무비", "재료비", "경비"] as const; const CREW = ["조종원", "조종원직종"]; const MISC = "잡재료율"; /** 보통 범위(옛 기계 양식과 같음) — 밖이면 경고만 */ export const RANGE: Record = { 가격: [0, 5_000_000], 손료계수: [1, 100_000], 내용시간: [100, 100_000], 연간표준가동시간: [100, 8_760], 상각비율: [0, 1], 정비비율: [0, 3], 연간관리비율: [0, 1], 주연료: [0, 2_000], 잡재료율: [0, 100], 조종원: [0, 20], 전력: [0, 10_000], }; // prettier-ignore const clone = (v: T): T => JSON.parse(JSON.stringify(v ?? null)) as T; const blank = (v: unknown): boolean => v === undefined || v === null || v === ""; /** 식 문서 → 기계 값 칸을 숫자로 박은 새 문서(그 칸이 없는 줄은 요소 그대로) */ export function bindDoc(doc: FormulaDoc, vals: MachineValues): FormulaDoc { const out = clone(doc); for (const v of out.변수 ?? []) { const l = v.연결 ?? {}; const key = String(l.키 ?? ""); const col = l.칸; if ( l.갈래 === "요소" && key.startsWith("EQ:") && key.includes("{S_기계}") && col && col in vals ) v.연결 = { 갈래: "숫자", 값: vals[col] as number | string }; } return out; } /** 같은 값 — 수는 작은 차이 무시 · 글은 글로 */ export function sameVal(a: unknown, b: unknown): boolean { if (blank(a) && blank(b)) return true; if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b)); return String(a ?? "") === String(b ?? ""); } export function modeOf(row: MachineRow, mine: boolean): MachineMode { if (mine) return row.방식 ?? "바로"; return MACHINE_INPUTS.some((k) => typeof row.값?.[k] === "number") ? "품셈" : "바로"; } /** 식이 읽는 값 묶음(셈 꼴) */ export function engineVals(row: MachineRow, mine: boolean): MachineValues { const v: MachineValues = { ...(row.값 ?? {}) }; if (!mine) return v; const how = row.방식 ?? "바로"; if (how === "직접") return Object.fromEntries(HOURLY.map((k) => [k, row.시간당?.[k] ?? 0])); if (how === "바로") Object.assign(v, { 상각비계수: v.손료계수 || 0, 정비비계수: 0, 관리비계수: 0 }); return { 주연료: 0, 잡재료율: 0, 조종원: 0, 주연료종류: "", 조종원직종: "", ...v }; } /** 셈 꼴 칸 → 저장 꼴 자리 · null = 저장 안 함(내 기계 바로의 정비 · 관리 = 늘 0) */ export function slotOf( col: string, mode: MachineMode, mine: boolean, ): ["값" | "시간당", string] | null { if (!mine) return ["값", col]; if (mode === "직접") return (HOURLY as readonly string[]).includes(col) ? ["시간당", col] : null; if (mode === "바로" && col === "상각비계수") return ["값", "손료계수"]; if (mode === "바로" && (col === "정비비계수" || col === "관리비계수")) return null; return ["값", col]; } /** 제어 구역이 맡은 칸(셈 꼴 이름) */ export const ownedCols = (parts: CoverParts): string[] => [ ...parts.fields.map((f) => f.칸), ...CREW, ...(parts.misc !== null ? [MISC] : []), ]; /** 처음 줄 대 지금 줄 → 덮음(조종원 = 「LB:직종」 한 줄 · 잡품 = 밑 줄마다 같은 율) — 정리는 `putCover` 몫 */ export function coverFrom( parts: CoverParts, lines: MoneyLine[], orig: MachineRow, cur: MachineRow, mine: boolean, ): MachineCover { const o = engineVals(orig, mine); const c = engineVals(cur, mine); const out: MachineCover = {}; const 값: Record = {}; for (const { 칸 } of parts.fields) if (!blank(c[칸]) && !sameVal(c[칸], o[칸])) 값[칸] = c[칸] as number | string; if (Object.keys(값).length) out.값 = 값; if (CREW.some((k) => !sameVal(c[k], o[k]))) { const n = Number(c.조종원) || 0; out.조종원 = n > 0 && !blank(c.조종원직종) ? [{ 키: `LB:${String(c.조종원직종)}`, 인: n }] : []; } if (parts.misc !== null && !sameVal(c[MISC], o[MISC])) { const r = Number(c[MISC]) || 0; out.잡품 = r ? parts.base.map((i) => ({ 줄: String(lines[i]?.이름 ?? ""), 율: r })) : []; } return out; } /** 노임 요소 키 → 인력 원문번호(`LB:1048` → 1048 · `LB000048` → 직종 표) */ export const jobCode = (key: string, jobs: Record): string => key.startsWith("LB:") ? key.slice(3) : (jobs[key] ?? key); /** 덮음 → 지금 줄(제어 구역이 맡은 칸만 · 덮음 없는 칸 = 처음 줄 저장 값으로) — 새 줄 */ export function applyCover( parts: CoverParts, orig: MachineRow, cur: MachineRow, entry: MachineCover, mine: boolean, jobs: Record, ): MachineRow { const mode = modeOf(cur, mine); const next = clone(cur); const put = (col: string, v: unknown): void => { const at = slotOf(col, mode, mine); if (!at) return; const [part, key] = at; const box = (next[part] ??= {}) as MachineValues; if (v === undefined) { const was = (orig[part] ?? {}) as MachineValues; if (key in was) box[key] = clone(was[key]); else delete box[key]; } else if (!(key in box) || !sameVal(box[key], v)) box[key] = v; }; for (const { 칸 } of parts.fields) put(칸, entry.값?.[칸]); if (entry.조종원 === undefined) CREW.forEach((k) => put(k, undefined)); else { const r = entry.조종원[0]; put("조종원", r ? r.인 : 0); put("조종원직종", r ? jobCode(r.키, jobs) : undefined); } if (parts.misc !== null) put(MISC, entry.잡품 === undefined ? undefined : (entry.잡품[0]?.율 ?? 0)); return next; } /** 범위 밖 칸 — [칸, 범위 글] */ export function rangeWarnings(values: MachineValues): [string, string][] { return Object.entries(RANGE).flatMap(([k, [lo, hi]]) => { const v = values[k]; return typeof v === "number" && (v < lo || v > hi) ? [[k, `${lo}~${hi}`] as [string, string]] : []; }); }