Files
Aislo/B09_Estimation/B09_Estimation_Calc_Base.ts
T
eomsangdonandClaude Opus 5.5 193ce92806 refactor(841-1): B09 단가 책 · 바탕 셈 걷음 — B 걸음
- 단가 책 = 자재만(자재단가대비표 · 4 자재대 · 관급자재대) · 노무 · 기계 · 운전사 계수(CC000336 세 줄) · 연료 · 환율 줄 걷음
- 바탕 셈(py · ts 짝) 운전사 시간 노임 · 중기사용료(손료 · 운전사 · 연료 · 잡품 재현) · 경비목록표 · 노임 · 기계 덮개 걷음
- 노무비목록표 · 중기목록표 = 대가 · 산근 줄 엔진 단가(프로젝트 사본 M02 값) 나열 · 다시 안 셈(price_lists)
- 경비목록표 · 중기사용료 · 환율및기초자료 탭 = 「—」 한 줄 + 까닭 한 줄(탭은 둠)
- [단가 갱신] = 프로젝트 사본 자원 단가 줄(단가.json LB · MT · EQ · MO · FX)을 지금 M01 로 + 자재 단가 책(새 파일 B09_Estimation_Copy_Prices.py · 사용자가 누를 때만) · 미리 보기 = 덮개로 같은 셈(파일 안 씀)
- 산근 · 대가 단가 고친 칸 걷음(품만) · 단가표 불러오기(707-3) 노임 · 기계 덮개 걷음(자재만)
- 낡은 시험은 C 걸음에서 정리

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

184 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 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(PARTS.map((p) => [p, num(m[p])]));
return { ...head(m, i + 1), unit: "시간", ...parts, sum: total(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,
};
}