refactor(794-1b): M01 흐름도 패널 걷음 — 로직 상세 오른쪽 = 원문만 · [신규 로직] = 빈 시트 줄 · 흐름 서버 길 · DataFlow 화면 지움
요구 651 · 653 — 계산 공식 참고용 흐름도(① M01 로직 상세 오른쪽 접는 패널)를 걷음. ② M02 일위대가 다이어그램은 그대로.
지움(64 파일 · 18,666 줄 · 되돌릴 길 = git 이력)
- M01 6 파일 1,064 줄 — Store_Flow.py · UI_LogicLab_Flow.ts/.css · FlowRows.ts · FlowView.ts/.css
- M02_MasterTemplete/DataFlow 43 파일 12,799 줄 — 흐름도 화면 · 셈(Compute · Convert · Lookup · Model · Eval …)
- 시험 15 파일 4,803 줄 — test_m01_logic_flow{view,_engine,_project,_save} · test_m02_df_*(값 찾기 values_old 뺌) — 지운 흐름 코드만 지키던 것
남김(바닥) — DataFlow M02_DF_UnitPick(일위대가 셋) · M02_DF_Values(M02_Item_Prices) · M02_DF_Round · _standard 표 ·
M02_DF_Router = GET /api/m02/df/standards 하나 · M02_DF_Api = cfStandards 하나 · CfRound 꼴은 Round 안으로
고침
- M01 Router — /save = Store_Make.save(개정 확인 · 자체 파일 새 줄 GX · added 그대로) · /calc /text /logic/auto = 옛 셈만 · /logic/flow 둘 지움
- master_formula — 흐름 훅 지움 · 줄에 「흐름」 이 남으면 「흐름도 로직은 걷음」 까닭 오류(저장된 흐름 로직 0 — 마스터 · storage 다시 확인)
- Sangeun_Calc._defaults = 옛 자동값 · M02_Logic_Lib_Read /logic/flow 지움 · main.py 주석
- 화면 — LogicLab(startFlow · scheduleFlow · 예시값 잇기 지움 · [신규 로직] 줄 = 입력 · 중간 · 호표 빈 목록 + 끝수 null) ·
Side(원문 패널 하나) · Source(머리 칸 이름 m01lab-src__top/__title · DF_Panel.css 안 씀) · Detail · View(nowInputs 지움) · Sheet(흐름 갈래 지움) · 글
- 시험 — 지운 파일을 가리키던 목록 넷(spreadsheet_size · 208_1 · 588 · input_prefs) · 717_1 흐름 길 두 줄 · values_old 흐름 셈 부분 지움
- test_713_3_list 기대 — 로직 1,461 → 1,515 · 묶음 307 → 335 · 작업량 21 → 65 · 부르는 506 → 512(782-1 · 2 · 3 건설 기초 로직 54 더함 ·
옛 키만 세면 307 · 21 · 506 그대로)
- 새 지킴 test_794_1b_flow_gone — 흐름 길 404 · 표준 표 남음 · 신규 로직 자체 GX · 흐름 남은 줄 까닭 오류
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,261 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_Rule.ts
|
||||
* 선 규칙 · 끝수 → 엑셀 수식 — 우리 스프레드시트 엔진 한 벌로 셈 · 선 위 규칙 글(쉬운 말) · DOM 없음
|
||||
*
|
||||
* 한 번 셈 = 시트 하나: A1 부터 보낸 값(표면 머리 줄 포함 덩어리) · 그 오른쪽에 조건 값 · 맨 끝 칸에 규칙 수식
|
||||
* lookup = 줄마다 「맞음」 칸(같음 · 범위는 「이름·아래」 < 값 ≤ 「이름·위」 · 빈 끝은 열림 · 아래 = 위면 값 == 그 수)
|
||||
* → 맞은 줄 수 · INDEX/MATCH
|
||||
* gate = IF(조건, 보낸 값, NA()) · scale = × ÷ (1 ± %) · round = ROUND · ROUNDUP · ROUNDDOWN
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { colName } from "../../A00_Common/spreadsheet/spreadsheet_address";
|
||||
import { recalcWorkbook } from "../../A00_Common/spreadsheet/spreadsheet_recalc";
|
||||
import type { CalcValue, Cell } from "../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import { gateConds, type CfGateOp, type CfRound, type CfRule } from "./M02_DF_Model";
|
||||
|
||||
type Scalar = number | string | boolean | null;
|
||||
|
||||
/** 규칙을 지난 값 — 통과 안 됨 · 표에 없는 조건 · 둘 이상 맞음은 value null + why */
|
||||
export interface CfRuleResult {
|
||||
value: Scalar;
|
||||
why: "통과 안 됨" | "표에 없는 조건" | "표에 여럿 맞음" | "오류" | null;
|
||||
/** 받는 칸에 들어갈 엑셀 수식(보이기 · 시험) */
|
||||
formula: string;
|
||||
}
|
||||
|
||||
const TEXT = {
|
||||
Lookup: ["표에서 찾기", "Look up in table"],
|
||||
Gate: ["일 때만", "only when"],
|
||||
And: ["그리고", "and"],
|
||||
Or: ["또는", "or"],
|
||||
Places: ["소수 {n}자리", "{n} decimals"],
|
||||
Round: ["반올림", "round"],
|
||||
Up: ["올림", "round up"],
|
||||
Down: ["버림", "round down"],
|
||||
} as const;
|
||||
|
||||
const tl = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
|
||||
|
||||
/** 엑셀 비교 기호 */
|
||||
const OP: Record<CfGateOp, string> = {
|
||||
"=": "=",
|
||||
"≠": "<>",
|
||||
"≥": ">=",
|
||||
"≤": "<=",
|
||||
">": ">",
|
||||
"<": "<",
|
||||
};
|
||||
const ROUND_FN: Record<CfRound["method"], string> = {
|
||||
반올림: "ROUND",
|
||||
올림: "ROUNDUP",
|
||||
버림: "ROUNDDOWN",
|
||||
};
|
||||
|
||||
/** 선 위에 늘 보일 규칙 글 */
|
||||
export function ruleLabel(rule: CfRule): string {
|
||||
switch (rule.kind) {
|
||||
case "lookup": {
|
||||
const conds = rule.conds.map((c) => `${c.col} = ${c.from.name}`).join(" · ");
|
||||
return `${tl("Lookup")}${conds ? ` (${conds})` : ""} → ${rule.col}`;
|
||||
}
|
||||
case "gate": {
|
||||
const g = gateConds(rule);
|
||||
const join = g.join === "또는" ? tl("Or") : tl("And");
|
||||
return `${g.conds.map((c) => `${c.from.name} ${c.op} ${c.value}`).join(` ${join} `)} ${tl("Gate")}`;
|
||||
}
|
||||
case "scale":
|
||||
return rule.op === "+%" || rule.op === "-%"
|
||||
? `${rule.op[0]}${rule.value}%`
|
||||
: `${rule.op} ${rule.value}`;
|
||||
case "round":
|
||||
return `${tl("Places").replace("{n}", String(rule.places))} ${roundWord(rule.method)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const roundWord = (m: CfRound["method"]): string =>
|
||||
m === "반올림" ? tl("Round") : m === "올림" ? tl("Up") : tl("Down");
|
||||
|
||||
/* ── 엔진 한 번 ─────────────────────────────────────────────────────────── */
|
||||
|
||||
const fromCalc = (v: CalcValue | undefined): Scalar => {
|
||||
if (!v) return null;
|
||||
if ("수" in v) return Number(v.수);
|
||||
if ("글" in v) return v.글;
|
||||
if ("참" in v) return v.참;
|
||||
return v.오류;
|
||||
};
|
||||
|
||||
const cellOf = (v: unknown): Cell | null =>
|
||||
v === null || v === undefined || v === ""
|
||||
? null
|
||||
: typeof v === "number" || typeof v === "string" || typeof v === "boolean"
|
||||
? { 값: v }
|
||||
: { 값: String(v) };
|
||||
|
||||
/** 칸들을 시트 하나로 셈 → 읽을 칸 값 */
|
||||
function run(칸: Record<string, Cell>, read: string[]): Scalar[] {
|
||||
const id = "rule";
|
||||
const values = recalcWorkbook({
|
||||
종류: "통합문서",
|
||||
판: 1,
|
||||
열: "DF",
|
||||
서식: [{}],
|
||||
시트: [{ id, 이름: "규칙", 칸 }],
|
||||
});
|
||||
return read.map((a) =>
|
||||
칸[a]?.식 !== undefined ? fromCalc(values[id]?.[a]) : (칸[a]?.값 ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
const isError = (v: Scalar): boolean => typeof v === "string" && /^#[A-Z/0!?]+$/.test(v);
|
||||
const lit = (v: number | string): string =>
|
||||
typeof v === "number" ? String(v) : `"${String(v).replace(/"/g, '""')}"`;
|
||||
|
||||
/* ── 규칙 ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** 조건 알약 값 — 열쇠 「상자|알약」 이 먼저 · 없으면 알약 이름 */
|
||||
const condOf = (conds: Record<string, unknown>, p: { box: string; name: string }): unknown =>
|
||||
`${p.box}|${p.name}` in conds ? conds[`${p.box}|${p.name}`] : conds[p.name];
|
||||
|
||||
/** 규칙 적용 — value = 보낸 알약 값(표면 머리 줄 포함 칸 덩어리) · conds = 조건 알약 이름 → 값(lookup · gate) */
|
||||
export function applyRule(
|
||||
rule: CfRule,
|
||||
value: unknown,
|
||||
conds: Record<string, unknown> = {},
|
||||
): CfRuleResult {
|
||||
if (rule.kind === "lookup") return lookup(rule, value, conds);
|
||||
const 칸: Record<string, Cell> = {};
|
||||
const a1 = cellOf(Array.isArray(value) ? null : value);
|
||||
if (a1) 칸.A1 = a1;
|
||||
let formula: string;
|
||||
if (rule.kind === "gate") {
|
||||
// 조건 값 = C1 · D1 · E1 … · 여러 줄은 AND / OR
|
||||
const g = gateConds(rule);
|
||||
const tests = g.conds.map((c, i) => {
|
||||
const at = `${colName(2 + i)}1`;
|
||||
const cv = cellOf(condOf(conds, c.from));
|
||||
if (cv) 칸[at] = cv;
|
||||
return `${at}${OP[c.op]}${lit(c.value)}`;
|
||||
});
|
||||
const test =
|
||||
tests.length > 1 ? `${g.join === "또는" ? "OR" : "AND"}(${tests.join(",")})` : tests[0];
|
||||
formula = `IF(${test},A1,NA())`;
|
||||
} else if (rule.kind === "scale") {
|
||||
const v = rule.value;
|
||||
formula =
|
||||
rule.op === "×"
|
||||
? `A1*${v}`
|
||||
: rule.op === "÷"
|
||||
? `A1/${v}`
|
||||
: rule.op === "+%"
|
||||
? `A1*(1+${v}/100)`
|
||||
: `A1*(1-${v}/100)`;
|
||||
} else formula = `${ROUND_FN[rule.method]}(A1,${rule.places})`;
|
||||
칸.B1 = { 식: formula };
|
||||
const [got] = run(칸, ["B1"]);
|
||||
if (rule.kind === "gate" && got === "#N/A") return { value: null, why: "통과 안 됨", formula };
|
||||
if (isError(got)) return { value: null, why: "오류", formula };
|
||||
return { value: got, why: null, formula };
|
||||
}
|
||||
|
||||
/** 표 블록 범위 경계 규칙 칸(줄마다 「초과·이하」 「이상·이하」 「이상·미만」 「이하」 · Values._table) */
|
||||
export const RANGE_RULE = "·범위규칙";
|
||||
|
||||
/** 표에서 찾기 — 줄마다 맞음 칸 · 맞은 줄 수 · INDEX/MATCH(범위 칸 「이름·아래」 「이름·위」 · 초과 · 이하)
|
||||
* 빈 조건 칸 = 무엇이든 · 꺼낼 칸이 빈 줄은 뺌(옛 엔진 find · 서버 Compute `_lookup` 과 같음) */
|
||||
function lookup(
|
||||
rule: Extract<CfRule, { kind: "lookup" }>,
|
||||
value: unknown,
|
||||
conds: Record<string, unknown>,
|
||||
): CfRuleResult {
|
||||
const block = Array.isArray(value) ? (value as unknown[][]) : [];
|
||||
const head = (block[0] ?? []).map((h) => String(h ?? ""));
|
||||
const rows = block.length - 1;
|
||||
const W = head.length;
|
||||
const 칸: Record<string, Cell> = {};
|
||||
block.forEach((line, r) =>
|
||||
line.forEach((x, c) => {
|
||||
const cell = cellOf(x);
|
||||
if (cell) 칸[`${colName(c)}${r + 1}`] = cell;
|
||||
}),
|
||||
);
|
||||
const at = (name: string): string | null => {
|
||||
const i = head.indexOf(name);
|
||||
return i < 0 ? null : colName(i);
|
||||
};
|
||||
// 값 칸이 범위로 갈림(「이름·아래」 「이름·위」) — 아래 = 위면 그 값
|
||||
const two = !at(rule.col) && !!at(`${rule.col}·아래`) && !!at(`${rule.col}·위`);
|
||||
const out = two ? at(`${rule.col}·아래`) : at(rule.col);
|
||||
// 조건 값 = 1행 · 블록 오른쪽 한 칸 띄고
|
||||
const tests: ((r: number) => string)[] = out ? [(r) => `NOT(ISBLANK(${out}${r}))`] : [];
|
||||
const ups: string[] = [];
|
||||
// 범위 경계 — 줄마다 「·범위규칙」 칸(이상 … = 아래 끝 포함 · … 미만 = 위 끝 뺌) · 없으면 초과 · 이하
|
||||
const rc = at(RANGE_RULE);
|
||||
const lowCmp = (c: string, r: number, x: string): string =>
|
||||
rc ? `IF(LEFT(${rc}${r},2)="이상",${c}${r}<=${x},${c}${r}<${x})` : `${c}${r}<${x}`;
|
||||
const highCmp = (c: string, r: number, x: string): string =>
|
||||
rc ? `IF(RIGHT(${rc}${r},2)="미만",${x}<${c}${r},${x}<=${c}${r})` : `${x}<=${c}${r}`;
|
||||
let bad = !out || rows < 1;
|
||||
rule.conds.forEach((c, i) => {
|
||||
const ref = `$${colName(W + 1 + i)}$1`;
|
||||
const cv = cellOf(condOf(conds, c.from));
|
||||
if (cv) 칸[`${colName(W + 1 + i)}1`] = cv;
|
||||
const same = at(c.col);
|
||||
const low = at(`${c.col}·아래`);
|
||||
const high = at(`${c.col}·위`);
|
||||
// 글끼리는 대소문자까지 같아야 맞음(옛 find · 「t_2」 ≠ 「T_2」) — FIND 는 대소문자를 가림
|
||||
if (same)
|
||||
tests.push(
|
||||
(r) =>
|
||||
`OR(ISBLANK(${same}${r}),IF(AND(ISTEXT(${same}${r}),ISTEXT(${ref})),` +
|
||||
`AND(LEN(${same}${r})=LEN(${ref}),ISNUMBER(FIND(${ref},${same}${r}))),${same}${r}=${ref}))`,
|
||||
);
|
||||
else if (low && high) {
|
||||
ups.push(high);
|
||||
tests.push(
|
||||
(r) =>
|
||||
`IF(AND(NOT(ISBLANK(${low}${r})),${low}${r}=${high}${r}),${ref}=${low}${r},` +
|
||||
`AND(OR(ISBLANK(${low}${r}),${lowCmp(low, r, ref)}),OR(ISBLANK(${high}${r}),${highCmp(high, r, ref)})))`,
|
||||
);
|
||||
} else bad = true;
|
||||
});
|
||||
const hc = colName(W + 1 + rule.conds.length);
|
||||
const last = rows + 1;
|
||||
for (let r = 2; r <= last; r++)
|
||||
칸[`${hc}${r}`] = {
|
||||
식: tests.length ? `IF(AND(${tests.map((t) => t(r)).join(",")}),1,0)` : "1",
|
||||
};
|
||||
// 여럿이면 범위 위 끝이 가장 작은 줄(옛 find · 「20 이하」 「40 이하」 …) — 둘째 맞음 칸
|
||||
const hc2 = colName(W + 3 + rule.conds.length);
|
||||
const span = (c: string): string => `${c}2:${c}${Math.max(2, last)}`;
|
||||
for (let r = 2; r <= last; r++) {
|
||||
const top = ups.map((h) => `OR(ISBLANK(${h}${r}),${h}${r}=MINIFS(${span(h)},${span(hc)},1))`);
|
||||
칸[`${hc2}${r}`] = {
|
||||
식: top.length ? `IF(AND(${hc}${r}=1,${top.join(",")}),1,0)` : `${hc}${r}`,
|
||||
};
|
||||
}
|
||||
const range = span(hc2);
|
||||
const count = `${colName(W + 2 + rule.conds.length)}1`;
|
||||
const formula = out ? `INDEX(${out}2:${out}${Math.max(2, last)},MATCH(1,${range},0))` : "NA()";
|
||||
칸[count] = { 식: `SUM(${range})` };
|
||||
칸.ZZ1 = { 식: formula };
|
||||
const up = at(`${rule.col}·위`);
|
||||
if (two && up) 칸.ZZ2 = { 식: formula.replace(`INDEX(${out}2:${out}`, `INDEX(${up}2:${up}`) };
|
||||
if (bad) return { value: null, why: "오류", formula };
|
||||
const [n, got, top] = run(칸, [count, "ZZ1", "ZZ2"]);
|
||||
if (two && n === 1 && got !== top) return { value: null, why: "오류", formula };
|
||||
if (n === 0) return { value: null, why: "표에 없는 조건", formula };
|
||||
if (typeof n === "number" && n > 1) return { value: null, why: "표에 여럿 맞음", formula };
|
||||
if (isError(got)) return { value: null, why: "오류", formula };
|
||||
return { value: got, why: null, formula };
|
||||
}
|
||||
|
||||
/** 끝수 적용 — 엔진 ROUND · ROUNDUP · ROUNDDOWN */
|
||||
export function applyRound(round: CfRound, value: number): number {
|
||||
const [got] = run(
|
||||
{ A1: { 값: value }, B1: { 식: `${ROUND_FN[round.method]}(A1,${round.places})` } },
|
||||
["B1"],
|
||||
);
|
||||
return typeof got === "number" ? got : value;
|
||||
}
|
||||
Reference in New Issue
Block a user