- 기계 자료: 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>
185 lines
8.2 KiB
TypeScript
185 lines
8.2 KiB
TypeScript
/* =============================================================================
|
|
* 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";
|
|
import { WORK } from "./common_util_formula_doc_work";
|
|
|
|
export type MachineValues = Record<string, unknown>;
|
|
/** 고르기 목록 한 칸 — 연료(값 = 종류) · 직종(값 = 인력 원문번호) */
|
|
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<string, [number, number]> = {
|
|
가격: [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 = <T>(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];
|
|
}
|
|
|
|
/** 제어 구역이 맡은 칸(셈 꼴 이름) — 작업량(846-1 · 목록) = 바탕 칸 숫자 입력으로 안 그림 */
|
|
export const ownedCols = (parts: CoverParts): string[] => [
|
|
...parts.fields.map((f) => f.칸),
|
|
CREW,
|
|
MISC,
|
|
WORK,
|
|
];
|
|
|
|
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<string, number | string> = {};
|
|
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, string>): string =>
|
|
key.startsWith("LB:") ? key.slice(3) : (jobs[key] ?? key);
|
|
/** 인력 줄 키 = `LB:원문번호`(빈 키 그대로) */
|
|
export const crewKey = (key: string, jobs: Record<string, string>): string =>
|
|
key ? `LB:${jobCode(key, jobs)}` : "";
|
|
|
|
/** 덮음 → 지금 줄(제어 구역이 맡은 칸만 · 덮음 없는 칸 = 처음 줄 저장 값으로) — 새 줄 */
|
|
export function applyCover(
|
|
parts: CoverParts,
|
|
orig: MachineRow,
|
|
cur: MachineRow,
|
|
entry: MachineCover,
|
|
mine: boolean,
|
|
jobs: Record<string, string>,
|
|
): 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]]
|
|
: [];
|
|
});
|
|
}
|