feat(810-2): 원파일 구조 줄 → 수식 문서 변환 · 원파일 우선 올림 · 전수 대조

- 새 M02_Doc_NativeEst(EST 산근 블록 → 가져온 로직 · 단가 \x03 · 변수 \x11 · 줄참조 \x0c 표지로 정함 · 변수 이름 그대로 · 분수 그림 공식 = 위아래 줄 칸 · 기계 · 산근 · 견적 세 줄 = 로직 값묶음 한 줄 · 소계 61 = 합 밖 · 율 58 · 62 = 덧줄 · 못 읽은 줄 = 보인 값 얼림 + 참고 표지 · 품셈 조건 글 = 추정 표지)
- 새 M02_Doc_FromNative(원파일 읽기 · 둘 꼴 · 블록 번 = 엑셀 길과 같음 · STmate = 원문 그대로 M02_San_Emit.stmate · 같은 공사 판정 · EST 일위대가 → 실무자료 호표 꼴)
- M02_Sangeun_FromHopyo — 산근 자리 칸 원파일 · 원파일 우선 emit · 안쪽 산근 원파일 코드로 · upload_native(같은 공사 엑셀 HP 에 붙음 · 없으면 새 HP)
- M02_Hopyo_Router 올림 — .estx · .stc 이면 upload_native(한 덩이)
- M02_Doc_Import — 원파일 문서 소계 덧줄 비목 칸 비움 · 수 하나 변수 출처 원파일 · 머리 원파일
- 수식 문서 셈 짝(master_doc · master_doc_calc ↔ common_util_formula_doc · _calc) — 합밖 + 비목 비움 = 금액만(소계 보임) · 그 밖 그대로
- M02_San_Estx 구조 줄 색인(Index) 칸 · 어디서 왔나 카드 「EST · 원파일」
- 전수 대조 — EST 산근 1,849 · STC 371 블록 수식 문서 셈 = 원파일 저장값(STC 계약단가 82 = 낙찰률 버림 뒤) · 표지 없는 틀린 줄 0 · 목록 docs/raw/요구_1104/_목록_810-2_원파일변환.xlsx
- 시험 test_810_2_native_doc(전수 · 꼴 · 문서 셈 거울 · 올림 분기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 20:11:03 +09:00
co-authored by Claude Opus 5.5
parent 7a7f545cc4
commit 53210f9a7a
15 changed files with 1168 additions and 13 deletions
+1 -1
View File
@@ -602,7 +602,7 @@ export function check(doc: FormulaDoc): Mark[] {
}
(doc.금액 ?? []).forEach((l, at) => {
const where = `금액 ${l.이름 || at + 1}`;
if (![...COSTS, "묶음"].includes(l.비목 as string))
if (![...COSTS, "묶음"].includes(l.비목 as string) && !(l.합밖 && !l.비목))
out.push(mark("금액 줄", `${where} 비목 「${l.비목}」`));
if (l.수량 !== null && l.수량 !== undefined && l.수량 !== "")
need(l.수량, `${where} 수량`, true);
+9 -1
View File
@@ -464,6 +464,7 @@ function money(run: Run, doc: FormulaDoc, rules: Record<string, Rule>) {
const price = run.expr(line.단가, where, scope);
const kind = line.비목 as string;
let parts: Bundle = {};
let bare: number | null = null; // 비목 없는 합 밖 줄(소계 보임 · 810-2) — 금액만
let why: string | null = null;
const stop = (text: string): void => {
stopped = true;
@@ -474,6 +475,7 @@ function money(run: Run, doc: FormulaDoc, rules: Record<string, Rule>) {
why = (qty instanceof Stop ? qty : (price as Stop)).why;
} else if (qty instanceof Blank) why = qty.why;
else if (!isNum(qty)) stop("수량이 수 아님");
else if (qty === 0 && !kind && line.합밖) bare = 0;
else if (qty === 0) for (const k of kind === "묶음" ? COSTS : [kind]) parts[k] = 0;
else if (price instanceof Blank) why = price.why;
else if (kind === "묶음") {
@@ -482,11 +484,17 @@ function money(run: Run, doc: FormulaDoc, rules: Record<string, Rule>) {
else stop("묶음 줄 단가가 단가산출 묶음 아님");
} else if ((COSTS as readonly string[]).includes(kind) && isNum(price))
parts[kind] = qty * price;
else if (!kind && line.합밖 && isNum(price)) bare = qty * price;
else stop(isObj(price) ? "묶음 값 — 칸(.계 · .노무비 …) 필요" : "단가가 수 아님");
const rule = line.자리 ?? rules.줄;
parts = Object.fromEntries(Object.entries(parts).map(([k, v]) => [k, cut(v, rule) as number]));
const keys = Object.keys(parts);
const amount = keys.length ? keys.reduce((s, k) => s + parts[k], 0) : null;
const amount =
bare !== null
? (cut(bare, rule) as number)
: keys.length
? keys.reduce((s, k) => s + parts[k], 0)
: null;
if (!skip.has(i)) addTo(running, parts);
if (line.기호) syms[String(line.기호)] = amount ?? new Blank(why ?? "빈 줄");
outs.push({