Files
Aislo/B09_Estimation/B09_Estimation_Calc_Base.ts
T
eomsangdonandClaude Opus 5.5 5d8d9a4fd3 test(841-1): B09 = M02 값 — 산근 · 대가 · 고정 줄 셈 다듬기 · 낡은 시험 정리 — C 걸음
코드
- 고정 줄 단가 = M02_Labor_Fixed.line(사본 묶음 · 이은 노임 · 자재 = 묶음 값) — B09 단가 책으로 다시 안 덮음
- 대가 · 산근 고친 칸 = 품만 실제로(단가 칸 덮기 줄 남던 흠 · 품 고친 뒤 줄 금액 다시 셈 흠 py · ts 짝)
- 산근 글 = 엔진 단가 셈 그대로 · 별도계상 재료 줄 끝 「· 별도계상 = 0.0」 · 계 줄 = 블록 합
- 노무비 · 중기목록 이름에서 규격 겹침 뗌 · TS 바탕 셈 이름 PARTS → MACHINE_PARTS(짝 시험이 TS 를 이어 붙일 때 겹침)

시험(고친 까닭 = 단가 원천이 B09 책 → M02 엔진 · 노임 · 기계 덮개 · 단가 고친 칸 · 중기사용료 걷음)
- 144 바탕 · 뼈대: 운전사 · 중기사용료 · 경비 · 덮개 시험 걷음 → 노무 · 중기 나열 · 사급 표시 · 자재만 책
- 145-1 · 145-2 · 155 · 157-1 · 166-2 · 166-6 · 719-3 · 719-4 · 723-4 · 778-4 · 792-3: 줄 단가 = own(엔진) · 산근 = Solver() · collect 새 부름 꼴
- 702 · 705-1 · 719-2: 단가 칸 덮개 = 안 읽음 · 고정 줄 = M02 셈(노임 새 판 따라가기 시험 걷음)
- 203-1 · 296 · 703-3 · 795-4: 단가 책 = 자재 줄로
- 707 · 707-6 · 707-7: 단가표 불러오기 = 자재만 · 거창 이은 안내판 줄 = M02 사본 셈(엑셀 −50,000)
- 201-1: 환율 · 중기사용료 표 걷음 · b09 저장 속도: 산근 앞 풀이 그대로(단가 표 같음 견줌 걷음)
- 대상 113 파일 764 통과 · 25 실패 = 원래 실패(formatMoney · formatPrice 없음 · 단위 글 ㎜ · sec · 719-1 · 796-2e · 717-4 · 721 · 311 · 243 · 246)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 16:57:01 +09:00

189 lines
6.7 KiB
TypeScript

/* =============================================================================
* B09_Estimation_Calc_Base.ts
* B09 원가 바탕 단가 셈(PLAN 144) — 화면 없음 · 저장 안 함 · import 없음(시험이 node 로 돌림).
*
* ⚠ 짝 = `B09_Estimation_Calc_Base.py`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
* 거울 시험 `resources/tester/test_144_b09_base_calc.py`.
* 조작 중은 이 파일이 바로 셈 · [저장] 때 서버가 파이썬 짝으로 정본을 다시 셈.
*
* 노무비목록표 · 중기목록표 = 대가 · 산근 줄 엔진 단가(사본 값 · 서버가 inputs.labor · machines 로 줌) 나열 ·
* 경비목록표 · 중기사용료 · 환율및기초자료 = 걷음(841-1 · 값 = M02 단가산출 그대로) — 빈 표
* 자재단가대비표 = 출처 중 최저 자동(견적 포함) · 적용 = 고른 출처 › 사용자 단가 › 최저 ·
* 관급 · 별도 표시 → 재료비목록표
* 재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0) · 하나라도 빈칸이면 빈칸 · 수량 = 입력
* material_qty(183-1 · 서버 역전개 총수량 · 없는 키 빈칸) ?? 입력 줄 수량(B08 총괄 합계 열)
* 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)에 남김
* ========================================================================== */
export type Num = number | null;
export type Row = Record<string, any>;
export interface Gap {
tab: string;
key: string;
name: string;
why: string;
}
/** 떠도는 끝수(부동소수) — 이만큼은 자리 올림 · 버림에서 무시 */
const EPS = 1e-7;
/** 자재단가대비표 출처 칸 차례 — 엑셀 조달 · 물가정보 · 물가자료(같은 값이면 앞 칸) */
export const SOURCES = [
"관급",
"물가정보",
"물가자료",
"유통물가",
"거래가격",
"유가",
"견적",
] as const;
/** 자재 줄 사용자 표시 */
export const SUPPLY = ["관급", "별도"] as const;
const MACHINE_PARTS = ["labor", "material", "expense"] as const;
function scaled(value: number, places: number, fn: (x: number) => number): number {
const a = Math.abs(value);
let out: number;
if (places >= 0) {
const scale = 10 ** places;
out = fn(a * scale) / scale;
} else {
const unit = 10 ** -places;
out = fn(a / unit) * unit;
}
return value < 0 ? -out : out;
}
/** 엑셀 ROUNDDOWN */
export const rounddown = (value: number, places = 0): number =>
scaled(value, places, (x) => Math.floor(x + EPS));
/** 엑셀 ROUNDUP */
export const roundup = (value: number, places = 0): number =>
scaled(value, places, (x) => Math.ceil(x - EPS));
/** 엑셀 ROUND(반 올림) */
export const roundHalf = (value: number, places = 0): number =>
scaled(value, places, (x) => Math.floor(x + 0.5 + EPS));
export function num(value: unknown): Num {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
/** 합 — 하나라도 빈칸이면 빈칸 */
export function total(values: Num[]): Num {
let out = 0;
for (const v of values) {
if (v === null) return null;
out += v;
}
return out;
}
export const gap = (tab: string, key: string, name: string, why: string): Gap => ({
tab,
key,
name,
why,
});
const head = (row: Row, no: number): Row => ({
no,
key: row.key,
name: row.name || "",
spec: row.spec || "",
source: row.source || "",
});
function laborSheet(labor: Row[], gaps: Gap[]): Row[] {
return labor.map((row, i) => {
const price = num(row.price);
if (price === null) gaps.push(gap("labor", row.key, row.name, "노임 단가 없음"));
return { ...head(row, i + 1), code: row.code ?? null, unit: row.unit || "인", price };
});
}
export function lowest(prices: Row): [Num, string | null] {
let best: [Num, string | null] = [null, null];
for (const source of SOURCES) {
const v = num(prices[source]);
if (v !== null && v > 0 && (best[0] === null || v < best[0])) best = [v, source];
}
return best;
}
function compareSheet(materials: Row[], user: Row, gaps: Gap[]): Row[] {
return materials.map((m, i) => {
const mine: Row = user[m.key] || {};
const prices: Row = { ...(m.prices || {}), 견적: mine.quote };
const pages: Row = { ...(m.pages || {}), 견적: mine.quote_page };
const [low, lowFrom] = lowest(prices);
const pick = (SOURCES as readonly string[]).includes(mine.pick) ? mine.pick : null;
let applied: Num;
let appliedFrom: string | null;
if (pick) [applied, appliedFrom] = [num(prices[pick]), pick];
else if ("applied" in mine) [applied, appliedFrom] = [num(mine.applied), "사용자"];
else [applied, appliedFrom] = [low, lowFrom];
if (applied === null) gaps.push(gap("compare", m.key, m.name, "적용 단가 없음(출처 없음)"));
const supply =
mine.supply === "사급" // 841-1 사용자가 기본 표시(M02 자재표시)를 끔
? null
: ([mine.supply, m.supply].find((v) => (SUPPLY as readonly string[]).includes(v)) ?? null);
return {
...head(m, i + 1),
unit: m.unit || "",
origin: m.origin || "",
prices: Object.fromEntries(SOURCES.map((s) => [s, num(prices[s])])),
pages: Object.fromEntries(SOURCES.map((s) => [s, pages[s] ?? null])),
lowest: low,
lowest_from: lowFrom,
applied,
applied_from: applied !== null ? appliedFrom : null,
pick,
supply,
qty: num(m.qty),
};
});
}
function materialSheet(compare: Row[], qtyOf?: Row | null): Row[] {
const keep = ["no", "key", "name", "spec", "unit", "supply", "origin"];
return compare.map((row) => {
const price: Num = row.applied;
const q: Num = qtyOf == null ? row.qty : num(qtyOf[row.key]);
return {
...Object.fromEntries(keep.map((k) => [k, row[k]])),
qty: q,
price,
amount: price === null || q === null ? null : roundHalf(q * price, 0),
};
});
}
/** 144 탭 + 「연결 없음」 목록 — 841-1 경비목록표 · 중기사용료 · 환율및기초자료 = 빈 표(짝 `compute_base`) */
export function computeBase(inputs: Row, user: Row): Row {
const gaps: Gap[] = [];
const compare = compareSheet(inputs.materials || [], user.materials || {}, gaps);
const machine = (inputs.machines || []).map((m: Row, i: number) => {
const parts: Row = Object.fromEntries(MACHINE_PARTS.map((p) => [p, num(m[p])]));
return {
...head(m, i + 1),
unit: "시간",
...parts,
sum: total(MACHINE_PARTS.map((p) => parts[p])),
};
});
return {
labor: laborSheet(inputs.labor || [], gaps),
expense: [],
basis: { fx: [], drivers: [] },
compare,
material: materialSheet(compare, inputs.material_qty),
machine_blocks: [],
machine,
gaps,
};
}