feat(m02-df): 36-2 옛 로직 → 흐름도 변환 · 표 찾기 옛 find 와 같은 뜻

- M02_DF_Convert.convert(row, ctx={key, file, master}) → {flow, notes} — 옛 파서 나무 → 함수 상자 시트(칸 주소 · 결과 하나) · 표 찾기 lookup · 로직 줄 비목 셋 · 덧줄 비목 합 · 줄.이름 · 결과 = 값 출력 · 자동 배치
- {입력} 낀 요소(EQ:{기계} · LB:{직종} · MO:경유:{지역} · 재료 검색어) = 조건 찾기 field 「키」(Model CfMaterialRule · Api slot 타입) · 자리 여럿은 글 잇는 함수
- 표 찾기: 빈 조건 칸 = 무엇이든 · 꺼낼 칸 빈 줄 건너뜀(Rule · Compute) · 함수 칸으로 가는 실패는 #N/A 를 넣어 넘김 · 결과가 오류일 때만 끊긴 곳(Eval · Compute 같음)
- 전수 대조 test_m02_df_convert(사본 폴더 1,472 로직 · 시험입력 92.9% · 기본 입력 94.3%) → tmp/convert_report_36.json

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
This commit is contained in:
2026-10-01 23:57:58 +09:00
co-authored by Claude Opus 5.5
parent 9516116af5
commit 52a085eb0c
8 changed files with 837 additions and 36 deletions
+34 -5
View File
@@ -35,6 +35,10 @@ export type CfValues = Record<string, Record<string, CfValue>>;
type Scalar = number | string | boolean | null;
/** 함수 상자 칸으로 가는 표 찾기 실패 값 — 칸에 NA() 를 넣음(엑셀과 같게 · IF 가 안 고른 갈래면 무시) */
const NA_VALUE = "#N/A";
const isErrorValue = (v: unknown): boolean => typeof v === "string" && /^#[A-Z/0!?]+$/.test(v);
/** 끊긴 곳 하나 — 상자 · 알약 · 선 위에 표시할 것 */
export interface CfBreak {
kind:
@@ -228,7 +232,11 @@ function runFunc(
): Scalar {
const func = box.func!;
const sheet: Sheet = structuredClone(func.sheet);
for (const i of func.inputs) put(sheet.칸, i.cell, arrive[i.port] ?? given[i.port] ?? null);
for (const i of func.inputs) {
const v = arrive[i.port] ?? given[i.port] ?? null;
if (v === NA_VALUE) sheet.칸[i.cell.toUpperCase()] = { 식: "NA()" };
else put(sheet.칸, i.cell, v);
}
for (const v of func.variables)
if (v.cell) put(sheet.칸, v.cell, v.name in given ? given[v.name] : v.example);
const values = recalcWorkbook({ 종류: "통합문서", 판: 1, 열: "DF", 서식: [{}], 시트: [sheet] });
@@ -262,6 +270,8 @@ export function evalLogic(
const blocked = new Set<string>();
/** 조건이 안 맞아 쉬는 상자 — 그 아래로 번짐 */
const resting = new Set<string>();
/** 함수 상자로 가는 표 찾기 실패 — 그 함수 결과가 오류일 때만 끊긴 곳(엑셀과 같게) */
const late = new Map<string, CfBreak[]>();
/** 선 하나로 들어오는 값 — 규칙이 있으면 엔진으로 */
const through = (l: CfLink): CfValue => {
const v = valueOf(l.from.box, l.from.name);
@@ -295,7 +305,22 @@ export function evalLogic(
? "ManyRows"
: "RuleError";
const text = tl(key, fill);
found.push({ kind, level: "문제", box: l.to.box, port: l.to.name, link: l.id, text });
const brk: CfBreak = {
kind,
level: "문제",
box: l.to.box,
port: l.to.name,
link: l.id,
text,
};
if (
l.rule.kind === "lookup" &&
lg.boxes.find((b) => b.id === l.to.box)?.kind === "function"
) {
late.set(l.to.box, [...(late.get(l.to.box) ?? []), brk]);
return NA_VALUE;
}
found.push(brk);
errors.push(text);
}
if (res.why) blocked.add(l.to.box);
@@ -354,9 +379,13 @@ export function evalLogic(
} else if (b.kind === "input") ports[b.id] = otherLogic(b, arrive(b), given, opts, errors);
else if (b.kind === "function") {
const got = arrive(b);
ports[b.id] = {
[b.func!.output.name]: blocked.has(b.id) ? null : runFunc(b, got, given, errors),
};
const out = blocked.has(b.id) ? null : runFunc(b, got, given, errors);
ports[b.id] = { [b.func!.output.name]: out };
if (out === null || isErrorValue(out))
for (const brk of late.get(b.id) ?? []) {
found.push(brk);
errors.push(brk.text);
}
} else {
const v = arrive(b)[OUTPUT_PORT] ?? null;
let scalar: Scalar = Array.isArray(v) ? null : v;