feat(778-4): 율 줄을 단가산출(SK)에 담음 — 고정 비율 더한줄 · 기준 묶음 · 줄 목록 · 율 위 율

- SK 더한줄 율 줄 허용(일위대가 비율 줄과 같은 꼴 `고정 비율 {기준, 비목}` · 수량 = 율)
- 기준 = 비목(노무비 · 재료비 · 경비) · 묶음(인력 · 재료 · 장비 = 그 줄 종류만 · 기계 노무 섞인 블록도 인력 줄만) ·
  줄 목록(cid · add:#k · 앞 율 줄 = 율 위 율) · 검사 = 자기 · 뒤 율 줄 막음
- 밑 공용 함수 `M02_Labor_Fixed.rate_base` ↔ 짝 `Labor_Calc.ts` `rateBase`(일위대가 비율 줄 · Expand 같이 씀)
- SK 셈 `_rated` = 엔진 한 번 돈 줄로 밑 → 율 줄 값묶음 → 엔진 다시(율 줄 없는 문서 = 한 번 · 값 그대로)
- B09 pct 밑수 `bundle:` · `rows:`(파이썬 · TS 짝) · SK 블록 율 줄 = pct
- 굳힘 — 바탕 없는 SK 율 줄은 한 줄로 안 펼침(참조 그대로) · 겹침 때 기준 열쇠 새 차례
- 화면 — [율 줄 추가] 창(새 `M02_MasterTemplete_Sangeun_Rate.ts` · 부품 상자만) · 율 줄 = % 줄로 보임 ·
  더한줄 지움 · 되살림 때 기준 열쇠 옮김
- 시험 — 새 test_778_4_rate_line(25) · 거울 헬퍼에 rateBase 길 · 전후 대조 SK 16 · 일위대가 118 · B09 대가 입력 14 같음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 12:59:48 +09:00
co-authored by Claude Opus 5.5
parent 2f93dd8b11
commit d9a581659f
16 changed files with 825 additions and 53 deletions
@@ -8,7 +8,9 @@
* 별도계상 자재(222-1 · 빈 칸 = 기본 별도계상) = 반영은 보이되 합에 안 더함(짝 `apart`).
* 참조 줄(단가승격 · 사전조합 · 50-3)은 `lineId` = 「참조|층|주인|키」(짝 `M02_Labor_Ref.ref_id`) ·
* 고정 줄(705-1)은 「고정|이름|규격|단위」(짝 `M02_Labor_Fixed.fixed_id`) · 기준 = 서버가 낸 줄 단가.
* 규격 열(704-4) = 문서 `규격` · 그 열 수량이 있는 줄 = `{손}` 자리(`atSpec` · 짝 `M02_Labor_Spec.at`). */
* 규격 열(704-4) = 문서 `규격` · 그 열 수량이 있는 줄 = `{손}` 자리(`atSpec` · 짝 `M02_Labor_Spec.at`).
* 율 줄 밑(778-4) `rateBase` = 짝 `M02_Labor_Fixed.rate_base` — 일위대가 비율 줄 · 단가산출(SK) 율 더한줄
* (서버 `M02_Sangeun_Calc._rated`) 한 규칙 · 거울 시험 `resources/tester/test_778_4_rate_line.py`. */
import type { CalcLine as LogicLine } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
import type { LogicMat } from "./M02_Labor_LogicMats_Model";
@@ -36,10 +38,25 @@ export interface LaborFixed {
단가: Record<"노무비" | "재료비" | "경비", number>;
이음?: Record<"노임" | "자재" | "기계", string | null>;
출처?: Record<string, string | number>;
/** 비율 줄(705-3 · 엑셀 % 줄) — 수량 = 율 · 반영[비목] = 다른 줄 반영합[기준] × 율 ÷ 100 */
비율?: { 기준: CostKey; 비목: CostKey };
/** 비율 줄(705-3 · 엑셀 % 줄) — 수량 = 율 · 반영[비목] = 밑(`rateBase`) × 율 ÷ 100 */
비율?: { 기준: RateBasis; 비목: CostKey };
}
type CostKey = "노무비" | "재료비" | "경비";
/** 율 기준(778-4) — 비목 · 묶음(인력 · 재료 · 장비) · 줄 열쇠 목록 */
export type RateBasis = CostKey | RateGroup | string[];
export type RateGroup = "인력" | "재료" | "장비";
/** 율 밑 줄 — 열쇠(SC 줄 가름 · SK cid) · 묶음 · 비목 · 율 줄인가 */
export interface RateRow {
키: string | null;
묶음: string | null;
비목: Partial<Record<CostKey, number | null>> | null;
율?: boolean;
}
const GROUPS: Record<"노임" | "자재" | "기계", RateGroup> = {
노임: "인력",
자재: "재료",
기계: "장비",
};
export interface CalcLine {
/** 참조 줄은 서버 답에 없음(빈 글로 다룸) */
@@ -234,27 +251,69 @@ export function settle(
if (cost !== null && !aside.has(one.코드)) add(done, { ...zero(), 재료비: cost, 계: cost });
return { ...one, 수량: qty, 수량출처: source, 반영: cost };
});
const lines = ratioLines(줄, done, base);
const lines = ratioLines(
줄,
자재.filter((m) => !aside.has(m.코드)),
done,
base,
);
return { 줄: lines, 자재, 기준합: base, 반영합: cutSums(done) };
}
/** 비율 고정 줄(705-3 · 짝 `M02_Labor_Fixed.ratios`) — 밑 = 비율 줄 뺀 반영합 · 기준[비목] = 밑[기준] ·
* 배 100 · 반영 = 기준 × 율 ÷ 100 · `done` 에 더함 · 기준합(`base`)엔 안 셈 */
function ratioLines(lines: CalcLine[], done: Money, base: Money): CalcLine[] {
/** 고정 줄 묶음 — 첫 이음(노임 · 자재 · 기계 · 짝 `M02_Labor_Fixed.group_of`) */
export function groupOf(fx: LaborFixed | undefined): RateGroup | null {
const link = fx?.이음;
const hit = (Object.keys(GROUPS) as (keyof typeof GROUPS)[]).find((k) => link?.[k]);
return hit ? GROUPS[hit] : null;
}
const all = (m: RateRow["비목"]): number => COSTS.reduce((s, k) => s + (m?.[k] ?? 0), 0);
/** 율 줄 밑(778-4 · 짝 `M02_Labor_Fixed.rate_base`) — 비목 = 율 줄 뺀 그 비목 합 ·
* 묶음 = 율 줄 뺀 그 묶음 줄 셋 합 · 목록 = 그 열쇠 줄 셋 합(앞 율 줄 됨) */
export function rateBase(basis: RateBasis, rows: RateRow[]): number {
if (Array.isArray(basis)) {
const keep = new Set(basis);
return rows.reduce((s, r) => s + (r.키 !== null && keep.has(r.키) ? all(r.비목) : 0), 0);
}
const plain = rows.filter((r) => !r.율);
if ((COSTS as readonly string[]).includes(basis))
return plain.reduce((s, r) => s + (r.비목?.[basis as CostKey] ?? 0), 0);
return plain.reduce((s, r) => s + (r.묶음 === basis ? all(r.비목) : 0), 0);
}
/** 비율 고정 줄(705-3 · 짝 `M02_Labor_Fixed.ratios`) — 밑 = `rateBase`(율 줄 뺀 반영 줄 · 자재 = 묶음 재료) ·
* 기준[비목] = 밑 · 배 100 · 반영 = 기준 × 율 ÷ 100 · `done` 에 더함 · 기준합(`base`)엔 안 셈 */
function ratioLines(
lines: CalcLine[],
mats: { 반영?: number | null }[],
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 };
const rows: RateRow[] = [
...lines
.filter((l) => !l.고정?.비율 && l.반영)
.map((l) => ({ 키: lineId(l) ?? null, 묶음: groupOf(l.고정), 비목: l.반영 ?? null })),
...mats
.filter((m) => m.반영 != null)
.map((m) => ({ 키: null, 묶음: "재료", 비목: { 재료비: m.반영 } })),
];
return lines.map((line) => {
const rate = line.고정?.비율;
if (!rate) return line;
const own = zero();
own[rate.비목] = own.계 = under[rate.기준];
own[rate.비목] = own.계 = rateBase(rate.기준, rows);
const got = reflect(own, line.수량 ?? null, 100);
if (got) for (const k of MONEY_KEYS) done[k] += got[k];
if (got) {
for (const k of MONEY_KEYS) done[k] += got[k];
rows.push({ 키: lineId(line) ?? null, 묶음: groupOf(line.고정), 비목: got, 율: true });
}
return { ...line, 기준: own, 배: 100, 반영: got };
});
}