Merge origin/sub_laptop_7 (50-3 ①②⑤ 서고 서버) into sub_laptop_2

- 충돌 M02_Labor_Doc.calc — sub7 run_line 을 취하고 50-4 「줄」 실음을 run_line 으로 옮김
- 단가승격 펼침 · 참조 줄 답에 원 로직 키 · 「줄」 실음(50-3 ④ 줄 상세용)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
2026-10-05 18:53:01 +09:00
co-authored by Claude Opus 5.5
12 changed files with 970 additions and 56 deletions
@@ -3,7 +3,8 @@
* 짝(CLAUDE.md 5장 ①): `M02_Labor_Doc.py` `settle` 과 같은 규칙 —
* 거울 시험 `resources/tester/test_m02_labor_calc_mirror.py`.
* 기준(결과단위 그대로)은 서버 calc 길이 셈 · 여기는 받은 기준에
* 수량(손이 칸보다 앞섬 · 칸 = 알약 키 값) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 재료비만. */
* 수량(손이 칸보다 앞섬 · 칸 = 알약 키 값) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 재료비만.
* 참조 줄(단가승격 · 사전조합 · 50-3)은 `lineId` = 「참조|층|주인|키」(짝 `M02_Labor_Ref.ref_id`). */
import type { CalcLine as LogicLine } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
@@ -13,8 +14,17 @@ export type QtySource = "손" | "칸" | "없음";
/** 문서의 수량 칸 — null = 면적산출 알약 · {손} = 손 입력 */
export type Qty = { 손: number } | null;
/** 참조 — 단가승격(SP) · 사전조합(SC) 문서(50-3) */
export interface LaborRef {
층: string;
주인: string | null;
키: string;
}
export interface CalcLine {
/** 참조 줄은 서버 답에 없음(빈 글로 다룸) */
로직: string;
참조?: LaborRef;
알약?: string | null;
분모?: string | null;
배: number;
@@ -51,7 +61,7 @@ export interface LaborCalc {
/** 수량만 보는 문서 조각 — 담은 로직 · 자재의 손 값 */
export interface LaborQtyDoc {
담은로직?: { 로직: string; 수량?: Qty }[];
담은로직?: { 로직?: string; 참조?: LaborRef; 수량?: Qty }[];
자재?: { 코드: string; 수량?: Qty }[];
}
@@ -61,6 +71,12 @@ function num(raw: unknown): number | null {
return String(raw).trim() !== "" && Number.isFinite(n) ? n : null;
}
/** 줄 가름 — 로직 키 · 참조 줄은 「참조|층|주인|키」(짝 `M02_Labor_Doc.line_id`) */
export function lineId(one: { 로직?: string; 참조?: LaborRef }): string | undefined {
if (one.참조) return `참조|${one.참조.층}|${one.참조.주인 ?? ""}|${one.참조.키}`;
return one.로직;
}
function zero(): Money {
return { 노무비: 0, 재료비: 0, 경비: 0, 계: 0 };
}
@@ -95,7 +111,7 @@ export function settle(
const v = num(raw);
if (v !== null) have[k] = v;
}
const hands = new Map((doc.담은로직 ?? []).map((one) => [one.로직, one.수량 ?? null]));
const hands = new Map((doc.담은로직 ?? []).map((one) => [lineId(one), one.수량 ?? null]));
const mhands = new Map((doc.자재 ?? []).map((one) => [one.코드, one.수량 ?? null]));
const base = zero();
const done = zero();
@@ -103,7 +119,7 @@ export function settle(
if (m) for (const k of MONEY_KEYS) sum[k] += m[k];
};
const 줄 = result.줄.map((line) => {
const [qty, source] = pick(hands.get(line.로직), line.알약, have);
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);