feat(B09 단가 덮개): 노임 · 기계 단가 덮개 labor_cover · machine_cover — 셈 짝 · 저장 다듬기 · 거울 시험 (PLAN 707-3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 15:39:16 +09:00
co-authored by Claude Opus 5.5
parent 4f4348c80e
commit 39e0ee6d85
4 changed files with 96 additions and 9 deletions
+32 -5
View File
@@ -12,6 +12,9 @@
* 재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0) · 하나라도 빈칸이면 빈칸 · 수량 = 입력
* material_qty(183-1 · 서버 역전개 총수량 · 없는 키 빈칸) ?? 입력 줄 수량(B08 총괄 합계 열)
* 중기사용료 = 손료 + 운전사 + 연료 + 잡품 · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 → 중기목록표
* 노임 · 기계 단가 덮개(707-3) = 사용자 값 labor_cover {노무 키: 일당} · machine_cover
* {기계 키: {labor, material, expense}}(셋 다 수일 때만) — 굳힌 값 대신 · covered · book(굳힌 값) ·
* 덮은 기계 = 중기사용료 줄 그대로 · 비목 합 · 합 = 덮개 · 그 기계 「연결 없음」 뺌
* 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)에 남김
* ========================================================================== */
@@ -41,6 +44,7 @@ export const SOURCES = [
/** 자재 줄 사용자 표시 */
export const SUPPLY = ["관급", "별도"] as const;
const PARTS = ["labor", "material", "expense"] as const;
function scaled(value: number, places: number, fn: (x: number) => number): number {
const a = Math.abs(value);
@@ -101,14 +105,28 @@ const head = (row: Row, no: number): Row => ({
source: row.source || "",
});
function laborSheet(labor: Row[], gaps: Gap[]): Row[] {
function laborSheet(labor: Row[], gaps: Gap[], cover?: Row): Row[] {
return labor.map((row, i) => {
const price = num(row.price);
const book = num(row.price);
const mine = num((cover || {})[row.key]);
const price = mine === null ? book : mine;
if (price === null) gaps.push(gap("labor", row.key, row.name, "노임 단가 없음"));
return { ...head(row, i + 1), code: row.code ?? null, unit: row.unit || "인", price };
const extra = mine === null ? {} : { covered: true, book };
return { ...head(row, i + 1), code: row.code ?? null, unit: row.unit || "인", price, ...extra };
});
}
/** 기계 덮개 — 노 · 재 · 경 셋 다 수인 줄만 */
export function machineCover(cover: unknown): Row {
const out: Row = {};
for (const [key, cell] of Object.entries(cover && typeof cover === "object" ? cover : {})) {
const got: Row = {};
for (const p of PARTS) got[p] = cell && typeof cell === "object" ? num(cell[p]) : null;
if (PARTS.every((p) => got[p] !== null)) out[key] = got;
}
return out;
}
function expenseSheet(machines: Row[], gaps: Gap[]): Row[] {
return machines.map((m, i) => {
const price = num(m.price);
@@ -276,8 +294,8 @@ function basisSheet(inputs: Row, laborRows: Row[]): Row {
/** 144 탭 일곱 + 「연결 없음」 목록 */
export function computeBase(inputs: Row, user: Row): Row {
const gaps: Gap[] = [];
const laborRows = laborSheet(inputs.labor || [], gaps);
let gaps: Gap[] = [];
const laborRows = laborSheet(inputs.labor || [], gaps, user.labor_cover);
const expense = expenseSheet(inputs.machines || [], gaps);
const compare = compareSheet(inputs.materials || [], user.materials || {}, gaps);
const basis = basisSheet(inputs, laborRows);
@@ -287,6 +305,14 @@ export function computeBase(inputs: Row, user: Row): Row {
for (const [kind, key] of Object.entries(inputs.fuel_keys || {}))
if ((key as string) in byKey) fuels[kind] = byKey[key as string];
const blocks = (inputs.machines || []).map((m: Row) => machineBlock(m, drivers, fuels, gaps));
const covers = machineCover(user.machine_cover);
for (const b of blocks) {
if (!(b.key in covers)) continue; // 707-3 덮개 = 비목 합 · 합(줄은 그대로)
b.book = { labor: b.labor, material: b.material, expense: b.expense, sum: b.sum };
Object.assign(b, covers[b.key], { covered: true });
b.sum = total(PARTS.map((p) => b[p]));
}
gaps = gaps.filter((g) => !(g.tab === "machine_blocks" && g.key in covers));
const machine = blocks.map((b: Row, i: number) => ({
no: i + 1,
key: b.key,
@@ -297,6 +323,7 @@ export function computeBase(inputs: Row, user: Row): Row {
labor: b.labor,
material: b.material,
expense: b.expense,
...(b.covered ? { covered: true } : {}),
}));
return {
labor: laborRows,