/* ============================================================================= * common_util_machine_sheet.ts * 기계 보기 셈 꼴(PLAN 840-2 · 요구 853 · 862) — 기계 줄(저장 꼴) ↔ 식이 읽는 값 · 덮음(836-1 `기계덮음` 꼴) ↔ 줄 * 셈 꼴(`engineVals`) = M01 = 값 그대로 · 내 기계 = 바로 → 상각비계수 ← 손료계수 · 정비 · 관리 0 · * 직접 → 노무비 · 재료비 · 경비 · 빈 연료 = 0 · 인력 · 잡품 = 빈 목록 · 옛 꼴 = 목록으로(서버 `M02_MyMachine.engine` 과 같은 꼴) * 덮음 ← 줄 차이(`coverFrom` — 처음 줄 대 지금 줄) · 지금 줄 ← 덮음(`applyCover` — 제어 구역이 맡은 칸만 · * 덮음이 없는 칸 = 처음 줄 값 그대로 = 안 고치고 닫으면 줄 바이트 같음) * 제어 구역이 맡은 칸(`ownedCols`) = 식이 읽는 값 칸 + 인력 · 잡품 목록(842-3 — 덮음 조종원 · 잡품 = 줄 목록 그대로) * 값 박음(`bindDoc`) = 식 문서 요소 `EQ:{S_기계}` 칸 → 숫자(처음 줄 값 · 목록 → 조종원 수 · 직종 · 잡재료율 `crewView`) — * 셈 차례 = 덮음 얹음(`coverDoc` · 바탕 = 처음 줄 목록 `rowEntry`) → 값 박음 * 파이썬 · TS 짝(`bindDoc`) — 짝 = M02_MasterTemplete/M02_MyMachine_Sheet.py `bind` · 거울 시험 test_840_2_machine_sheet.py * · 화면 · DOM · fetch 없음 * ========================================================================== */ import type { FormulaDoc } from "./common_util_formula_doc"; import type { CoverParts, CrewRow, MachineCover, MiscRow } from "./common_util_formula_doc_cover"; import { MACHINE_INPUTS } from "./common_util_machine_coef"; import { CREW, MISC, crewView, fromOld } from "./common_util_machine_crew"; 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; /** 보통 범위(옛 기계 양식과 같음) — 밖이면 경고만 */ 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, raw: MachineValues): FormulaDoc { const out = clone(doc); const vals = crewView(raw); 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 (Array.isArray(a) || Array.isArray(b)) return JSON.stringify(a) === JSON.stringify(b); 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, 주연료종류: "", [CREW]: [], [MISC]: [], ...fromOld(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, MISC, ]; const listText = (v: unknown): string => JSON.stringify(v ?? null); /** 처음 줄 대 지금 줄 → 덮음(값 = 고친 칸 · 조종원 · 잡품 = 지금 줄 목록 통째) — 정리는 `putCover` 몫 */ export function coverFrom( parts: CoverParts, 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 (listText(c[CREW]) !== listText(o[CREW])) out.조종원 = clone((c[CREW] ?? []) as CrewRow[]); if (listText(c[MISC]) !== listText(o[MISC])) out.잡품 = clone((c[MISC] ?? []) as MiscRow[]); return out; } /** 노임 요소 키 → 인력 원문번호(`LB:1048` → 1048 · `LB000048` → 직종 표 · 빈 글 = 직종 고르기) */ export const jobCode = (key: string, jobs: Record): string => key.startsWith("LB:") ? key.slice(3) : (jobs[key] ?? key); /** 인력 줄 키 = `LB:원문번호`(빈 키 그대로) */ export const crewKey = (key: string, jobs: Record): string => key ? `LB:${jobCode(key, jobs)}` : ""; /** 덮음 → 지금 줄(제어 구역이 맡은 칸만 · 덮음 없는 칸 = 처음 줄 저장 값으로) — 새 줄 */ 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.값?.[칸]); put( CREW, entry.조종원?.map((r) => ({ ...r, 키: crewKey(r.키, jobs) })), ); put(MISC, entry.잡품 && clone(entry.잡품)); 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]] : []; }); }