Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts
T
eomsangdonandClaude Opus 5.5 2789afb102 feat(M02 실무자료 빠짐 0): 확인 안 한 줄 · 노임 · % = 고정 줄 · [모두 고정으로 들여오기] · [품셈으로 바꾸기] (PLAN 705-3)
- 만들기 — 체크 안 한 줄 · 노임 · 「우리에 없음」 = 고정 줄(엑셀 숫자 그대로 · 서버가 파일에서) · % 줄 = 비율 고정 줄(밑수 = 다른 줄 합일 때 · 아니면 금액 그대로) · 합계만 = 재료비 · 같은 이름 = 차례 가름 · 하위 호표 = 확인된 사전조합 참조
- 이음 = 이름 · 규격 · 단위 · 단가 다 같은 노임 · 자재만 저절로 · 다른 단가 = 이음 후보 수
- 시트 통째 — /hopyo-build-all(하위 먼저 · 확인된 호표 그대로) · 알림 「호표 · 줄 · 품셈 줄 · 고정 줄 · 빠짐 · 엑셀 합계 = 우리 합계」 · 비교 = 엑셀 끝수 규칙(EST 내림 · STmate 반올림)
- 비율 고정 줄 셈 — M02 settle 짝(파이썬 · TS ratioLines) · 사전조합 셈 · B09 pct 줄
- 확인 창 — 만든 사전조합 품셈 줄 = 첫 체크 · [품셈으로 바꾸기] · [모두 고정으로] · 생성 물음에 전후 금액 · 품 다른 줄 수
- 낡은 시험 고침 — 빈 몸 400 · 빠진 줄(확인 안 함 · %) 단정 = 고정 줄로 바뀐 규칙

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

205 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* M02 일위대가 수량 · 반영 · 합(PLAN 39-1a · 설계_정함 1장) — 면적 칸 값이 바뀌면 화면이 바로 곱함.
*
* 짝(CLAUDE.md 5장 ①): `M02_Labor_Doc.py` `settle` 과 같은 규칙 —
* 거울 시험 `resources/tester/test_m02_labor_calc_mirror.py`.
* 기준(결과단위 그대로)은 서버 calc 길이 셈 · 여기는 받은 기준에
* 수량(손이 칸보다 앞섬 · 칸 = 알약 키 값) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 재료비만.
* 별도계상 자재(222-1 · 빈 칸 = 기본 별도계상) = 반영은 보이되 합에 안 더함(짝 `apart`).
* 참조 줄(단가승격 · 사전조합 · 50-3)은 `lineId` = 「참조|층|주인|키」(짝 `M02_Labor_Ref.ref_id`) ·
* 고정 줄(705-1)은 「고정|이름|규격|단위」(짝 `M02_Labor_Fixed.fixed_id`) · 기준 = 서버가 낸 줄 단가. */
import type { CalcLine as LogicLine } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
import type { LogicMat } from "./M02_Labor_LogicMats_Model";
export const MONEY_KEYS = ["노무비", "재료비", "경비", "계"] as const;
export type Money = Record<(typeof MONEY_KEYS)[number], number>;
export type QtySource = "손" | "칸" | "없음";
/** 문서의 수량 칸 — null = 면적산출 알약 · {손} = 손 입력 */
export type Qty = { 손: number } | null;
/** 참조 — 단가승격(SP) · 사전조합(SC) 문서(50-3) */
export interface LaborRef {
층: string;
주인: string | null;
키: string;
}
/** 고정 줄(705-1) — 숫자 그대로 · 이음 = 이 공사 노임 · 자재 · 기계 키(못 이음 = null) */
export interface LaborFixed {
이름: string;
규격?: string;
단위?: string;
단가: Record<"노무비" | "재료비" | "경비", number>;
이음?: Record<"노임" | "자재" | "기계", string | null>;
출처?: Record<string, string | number>;
/** 비율 줄(705-3 · 엑셀 % 줄) — 수량 = 율 · 반영[비목] = 다른 줄 반영합[기준] × 율 ÷ 100 */
비율?: { 기준: CostKey; 비목: CostKey };
}
type CostKey = "노무비" | "재료비" | "경비";
export interface CalcLine {
/** 참조 줄은 서버 답에 없음(빈 글로 다룸) */
로직: string;
참조?: LaborRef;
고정?: LaborFixed;
/** 고정 줄 — 단가 책 값을 쓴 비목 */
이은?: string[];
알약?: string | null;
분모?: string | null;
배: number;
기준: Money | null;
수량?: number | null;
수량출처?: QtySource;
반영?: Money | null;
까닭?: string | null;
결과?: unknown;
/** 그 로직 재료 · 노무 · 경비 줄(M01 시험계산 표와 같은 모양 · 50-4) */
줄?: LogicLine[];
/** 로직 자재(103-1) — 맨 위 층 재료 줄 · 금액은 `기준` 안(여기서 안 더함) */
재료?: LogicMat[];
/** 참조 줄 — 가리킨 문서 이름 · 단가승격의 원 로직 키 · 사전조합 안 줄(한 층 · 50-3 ④ ·
* 참조 줄 = `참조` · 로직 줄(51-11) = `로직` · 자재 줄(175-2) = `자재` 코드) */
이름?: string | null;
원로직?: string | null;
안?: {
참조?: LaborRef;
로직?: string;
자재?: string;
수량: number;
이름?: string | null;
단위?: string | null;
기준: Money | null;
까닭?: string | null;
}[];
}
export interface CalcMaterial {
코드: string;
알약?: string | null;
기준단가: number | null;
/** M01 자재품목 값 열 다섯(물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급 · 빈 값 null · 43-3) */
가격?: Record<string, number | null>;
/** 기준단가가 된 열 이름 — 없으면 null */
기준칸?: string | null;
수량?: number | null;
수량출처?: QtySource;
반영?: number | null;
까닭?: string | null;
}
export interface LaborCalc {
줄: CalcLine[];
자재: CalcMaterial[];
기준합?: Money;
반영합?: Money;
}
/** 수량만 보는 문서 조각 — 담은 로직 · 자재의 손 값 */
export interface LaborQtyDoc {
담은로직?: { 로직?: string; 참조?: LaborRef; 고정?: LaborFixed; 수량?: Qty }[];
자재?: { 코드: string; 수량?: Qty; 별도계상?: string }[];
}
function num(raw: unknown): number | null {
if (raw === null || raw === undefined || typeof raw === "boolean") return null;
const n = Number(String(raw).replace(/,/g, "").trim());
return String(raw).trim() !== "" && Number.isFinite(n) ? n : null;
}
/** 줄 가름 — 로직 키 · 참조 줄은 「참조|층|주인|키」 · 고정 줄은 「고정|이름|규격|단위」
* (짝 `M02_Labor_Doc.line_id`) */
export function lineId(one: {
로직?: string;
참조?: LaborRef;
고정?: LaborFixed;
}): string | undefined {
const fx = one.고정;
if (fx) return `고정|${fx.이름 ?? ""}|${fx.규격 ?? ""}|${fx.단위 ?? ""}`;
if (one.참조) return `참조|${one.참조.층}|${one.참조.주인 ?? ""}|${one.참조.키}`;
return one.로직;
}
function zero(): Money {
return { 노무비: 0, 재료비: 0, 경비: 0, 계: 0 };
}
function pick(
hand: Qty | undefined,
key: string | null | undefined,
given: Record<string, number>,
): [number | null, QtySource] {
const own = hand ? num(hand.손) : null;
if (own !== null) return [own, "손"];
if (key && key in given) return [given[key], "칸"];
return [null, "없음"];
}
/** 반영 = 기준 × 수량 ÷ 배 — 수량이 없으면 null */
export function reflect(base: Money, qty: number | null, times: number): Money | null {
if (qty === null) return null;
const out = zero();
for (const k of MONEY_KEYS) out[k] = (base[k] * qty) / times;
return out;
}
/** 기준이 선 계산 답 + 문서 손 값 + 알약 값(`{키: 값}`) → 수량 · 반영 · 합을 새로 채운 답 */
export function settle(
result: LaborCalc,
doc: LaborQtyDoc,
given: Record<string, unknown>,
): LaborCalc {
const have: Record<string, number> = {};
for (const [k, raw] of Object.entries(given)) {
const v = num(raw);
if (v !== null) have[k] = v;
}
const hands = new Map((doc.담은로직 ?? []).map((one) => [lineId(one), one.수량 ?? null]));
const mhands = new Map((doc.자재 ?? []).map((one) => [one.코드, one.수량 ?? null]));
// 별도계상(222-1) — 「직접계산」 아니면(빈 · 옛 값 = 기본 별도계상) 합에서 뺌(짝 `M02_Labor_Doc.apart`)
const aside = new Set(
(doc.자재 ?? []).filter((m) => m.별도계상 !== "직접계산").map((m) => m.코드),
);
const base = zero();
const done = zero();
const add = (sum: Money, m: Money | null): void => {
if (m) for (const k of MONEY_KEYS) sum[k] += m[k];
};
const 줄 = result.줄.map((line) => {
const [qty, source] = pick(hands.get(lineId(line)), line.알약, have);
const own = line.기준 ? reflect(line.기준, qty, num(line.배) || 1) : null;
add(base, line.기준);
add(done, own);
return { ...line, 수량: qty, 수량출처: source, 반영: own };
});
const 자재 = result.자재.map((one) => {
const [qty, source] = pick(mhands.get(one.코드), one.알약, have);
const price = num(one.기준단가);
const cost = price !== null && qty !== null ? price * qty : null;
if (price !== null && !aside.has(one.코드)) add(base, { ...zero(), 재료비: price, 계: price });
if (cost !== null && !aside.has(one.코드)) add(done, { ...zero(), 재료비: cost, 계: cost });
return { ...one, 수량: qty, 수량출처: source, 반영: cost };
});
return { 줄: ratioLines(줄, done, base), 자재, 기준합: base, 반영합: done };
}
/** 비율 고정 줄(705-3 · 짝 `M02_Labor_Fixed.ratios`) — 밑 = 비율 줄 뺀 반영합 · 기준[비목] = 밑[기준] ·
* 배 100 · 반영 = 기준 × 율 ÷ 100 · `done` 에 더함 · 기준합(`base`)엔 안 셈 */
function ratioLines(lines: CalcLine[], done: Money, base: Money): CalcLine[] {
for (const l of lines.filter((x) => x.고정?.비율)) {
for (const k of MONEY_KEYS) {
done[k] -= l.반영?.[k] ?? 0;
base[k] -= l.기준?.[k] ?? 0;
}
}
const under = { ...done };
return lines.map((line) => {
const rate = line.고정?.비율;
if (!rate) return line;
const own = zero();
own[rate.비목] = own.계 = under[rate.기준];
const got = reflect(own, line.수량 ?? null, 100);
if (got) for (const k of MONEY_KEYS) done[k] += got[k];
return { ...line, 기준: own, 배: 100, 반영: got };
});
}