From 083f320ac49d5d073087961d3d1b086268cf1f98 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 19:52:01 +0900 Subject: [PATCH] =?UTF-8?q?fix(M02-=ED=9D=90=EB=A6=84=EB=8F=84):=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EB=82=B4=EC=9A=A9=20=ED=8C=90=EC=9D=84=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EB=AA=B8=ED=86=B5=EC=97=90=20=EC=9D=B4?= =?UTF-8?q?=EC=9D=8C=20-=20=EB=B9=88=20=ED=95=A8=EC=88=98=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20traceBack=C2=B7breaks=C2=B7arrived=20=EB=A5=BC=20?= =?UTF-8?q?=EC=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c --- M02_MasterTemplete/DataFlow/M02_DF_Detail.ts | 238 +++++------------- .../DataFlow/M02_DF_LinkPanel.ts | 19 +- .../DataFlow/M02_DF_PillPanel.ts | 24 +- 3 files changed, 78 insertions(+), 203 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts b/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts index 6ab1911b7..30b1312f8 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts @@ -1,14 +1,22 @@ /* ============================================================================= * M02_DF_Detail.ts - * 상세 내용 판(선 · 알약) 공용 — 값 읽기 · 규칙 미리 보기 · 겹침/안 맞음 · 흐름 줄 · 끊김 까닭 · DOM 없음 - * 모델의 traceBack · traceForward · breaks · applyRule · ruleLabel 이 아직 준비 중이면 같은 뜻의 작은 셈으로 메움 + * 상세 내용 판(선 · 알약) 공용 — 값 읽기 · 규칙 미리 보기 · 끊김 글 · 흐름 줄 · DOM 없음 + * 셈은 모델 것 그대로(Eval 의 evalLogic · breaks · Model 의 traceBack · traceForward · Rule 의 ruleLabel) * 글 [한, 영] 짝은 아래 TEXT * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; -import { breaks, evalLogic, type CfEvalResult, type CfValue, type CfValues } from "./M02_DF_Eval"; import { - portsIn, + breaks, + evalLogic, + type CfBreak, + type CfEvalOptions, + type CfEvalResult, + type CfValue, + type CfValues, +} from "./M02_DF_Eval"; +import { + portDir, portsOut, traceBack, traceForward, @@ -17,26 +25,15 @@ import { type CfLink, type CfLogic, type CfPort, - type CfRound, type CfRule, type CfStep, } from "./M02_DF_Model"; -import { applyRule, ruleLabel } from "./M02_DF_Rule"; +import { ruleLabel } from "./M02_DF_Rule"; const TEXT = { Table: ["표 {n}줄", "table, {n} rows"], Empty: ["값 없음", "no value"], - LabelLookup: ["표에서 「{col}」 찾기", "Look up “{col}” in the table"], - LabelGate: ["「{pill}」 {op} {value} 일 때만 보냄", "Send only when “{pill}” {op} {value}"], - LabelScale: ["값 {op} {value}", "value {op} {value}"], - LabelRound: ["소수 {places}자리 {method}", "{places} decimals, {method}"], NoRule: ["규칙 없음 — 값이 그대로 감", "No rule — the value goes as is"], - Blocked: ["통과 안 됨", "does not pass"], - NoRowLater: ["표에서 맞는 줄은 계산 뒤에 보임", "The matching row shows after calculation"], - EmptyIn: ["빈 받는 자리 — 선이 없음", "Empty receiving spot — no line"], - NoCell: ["받은 알약을 칸에 놓지 않음", "The received pill is not placed in a cell"], - Dead: ["아직 어디로도 안 감", "Goes nowhere yet"], - NoValue: ["값이 아직 없음", "No value yet"], } as const; export const td = (key: keyof typeof TEXT, fill: Record = {}): string => @@ -48,16 +45,22 @@ export const td = (key: keyof typeof TEXT, fill: Record export const GATE_OPS: CfGateOp[] = ["=", "≠", "≥", "≤", ">", "<"]; export const SCALE_OPS: ("×" | "÷" | "+%" | "-%")[] = ["×", "÷", "+%", "-%"]; +/** 셈에 넘길 것 — 값 묶음 · 다른 로직들 */ +export interface DetailOptions { + values?: CfValues; + logics?: CfEvalOptions["logics"]; +} + export const boxOf = (lg: CfLogic, id: string): CfBox | undefined => lg.boxes.find((b) => b.id === id); export const portText = (lg: CfLogic, p: CfPort): string => `${boxOf(lg, p.box)?.name ?? "?"} › ${p.name}`; -/** 계산 한 번 — 못 하면 null */ -export function evalSafe(lg: CfLogic, values?: CfValues): CfEvalResult | null { +/** 계산 한 번 — 한창 만드는 중 막히면 null */ +export function evalSafe(lg: CfLogic, o: DetailOptions = {}): CfEvalResult | null { try { - return evalLogic(lg, values); + return evalLogic(lg, o.values, { logics: o.logics }); } catch { return null; } @@ -79,176 +82,55 @@ export function unitOf(lg: CfLogic, p: CfPort): string { } /** 알약이 나가는(out) 쪽인가 */ -export const isOut = (box: CfBox, name: string): boolean => - portsOut(box).some((p) => p.name === name); +export const isOut = (box: CfBox, name: string): boolean => portDir(box, name) === "out"; -/* ── 규칙 ─────────────────────────────────────────────────────────────── */ +/** 선 위 규칙 글 */ +export const labelOf = (rule: CfRule | undefined): string => + rule ? ruleLabel(rule) : td("NoRule"); -const num = (v: unknown): number | null => { - const n = typeof v === "number" ? v : Number(v); - return Number.isFinite(n) && v !== "" && v !== null ? n : null; -}; - -/** 조건 통과 — 숫자면 숫자끼리 · 아니면 글끼리 */ -export function gatePass(op: CfGateOp, v: unknown, value: number | string): boolean { - const a = num(v); - const b = num(value); - const [x, y] = a !== null && b !== null ? [a, b] : [String(v ?? ""), String(value)]; - switch (op) { - case "=": - return x === y; - case "≠": - return x !== y; - case "≥": - return x >= y; - case "≤": - return x <= y; - case ">": - return x > y; - default: - return x < y; - } +/** 지나가기 전 값 → 지난 뒤 값 — 지난 뒤 값은 셈이 받는 알약에 실제로 들여보낸 것 · 못 들어갔으면 끊긴 까닭 */ +export function preview(link: CfLink, ev: CfEvalResult | null): { before: string; after: string } { + const before = showValue(valueOf(ev, link.from)); + if (!link.rule) return { before, after: before }; + const arrived = ev?.arrived?.[link.to.box]?.[link.to.name]; + if (arrived !== undefined && arrived !== null) return { before, after: showValue(arrived) }; + const why = ev?.breaks?.find((b) => b.link === link.id)?.text; + return { before, after: why ?? td("Empty") }; } -export function roundValue(r: CfRound, v: number): number { - const k = 10 ** r.places; - const f = r.method === "올림" ? Math.ceil : r.method === "버림" ? Math.floor : Math.round; - return f(v * k) / k; +/** 이 선에서 알릴 것 — 이 선이 막힌 까닭 · 같은 알약에서 나간 조건 선의 겹침 · 안 맞음 */ +export function linkNotes(link: CfLink, ev: CfEvalResult | null): string[] { + return (ev?.breaks ?? []) + .filter( + (b) => + b.link === link.id || + ((b.kind === "조건 겹침" || b.kind === "조건 안 맞음") && + b.box === link.from.box && + b.port === link.from.name), + ) + .map((b) => b.text); } -/** 선 위 규칙 글 — 모델이 준비되면 그것, 아니면 같은 뜻의 글 */ -export function labelOf(lg: CfLogic, rule: CfRule | undefined): string { - if (!rule) return td("NoRule"); - try { - return ruleLabel(rule); - } catch { - if (rule.kind === "lookup") return td("LabelLookup", { col: rule.col }); - if (rule.kind === "gate") - return td("LabelGate", { pill: portText(lg, rule.from), op: rule.op, value: rule.value }); - if (rule.kind === "scale") return td("LabelScale", { op: rule.op, value: rule.value }); - return td("LabelRound", { places: rule.places, method: rule.method }); - } -} - -/** 지나가기 전 값 → 지난 뒤 값 미리 보기 */ -export function preview( - link: CfLink, - rule: CfRule | undefined, - ev: CfEvalResult | null, -): { before: string; after: string } { - const raw = valueOf(ev, link.from); - const before = showValue(raw); - if (!rule) return { before, after: before }; - const conds: Record = {}; - if (rule.kind === "lookup") for (const c of rule.conds) conds[c.col] = valueOf(ev, c.from); - try { - const r = applyRule(rule, raw, conds); - return { - before, - after: r.value === null ? (r.why ?? td("Empty")) : showValue(r.value as CfValue), - }; - } catch { - const n = num(raw); - if (rule.kind === "gate") - return { - before, - after: gatePass(rule.op, valueOf(ev, rule.from), rule.value) ? before : td("Blocked"), - }; - if (rule.kind === "lookup" || n === null) return { before, after: td("NoRowLater") }; - if (rule.kind === "round") return { before, after: showValue(roundValue(rule, n)) }; - const v = rule.value; - const out = - rule.op === "×" - ? n * v - : rule.op === "÷" - ? v - ? n / v - : NaN - : rule.op === "+%" - ? n * (1 + v / 100) - : n * (1 - v / 100); - return { before, after: Number.isFinite(out) ? showValue(out) : td("Empty") }; - } -} - -/** 같은 알약에서 나간 조건 선 — 지금 값으로 통과하는 수 */ -export function gateReport( - lg: CfLogic, - link: CfLink, - ev: CfEvalResult | null, -): { total: number; pass: string[] } { - const same = lg.links.filter( - (l) => - l.from.box === link.from.box && l.from.name === link.from.name && l.rule?.kind === "gate", - ); - const pass = same - .filter((l) => { - const r = l.rule as Extract; - return gatePass(r.op, valueOf(ev, r.from), r.value); - }) - .map((l) => l.id); - return { total: same.length, pass }; -} - -/* ── 흐름 · 끊김 ─────────────────────────────────────────────────────── */ - -/** 거슬러 오르는 길(dir "in" = 온 길) · 따라가는 길(dir "out") — 모델 것, 없으면 작은 셈 */ +/** 온 길(dir "in") · 가는 길(dir "out") — 모델 것에서 자기 자신 줄은 뺌 */ export function trace(lg: CfLogic, p: CfPort, dir: "in" | "out"): CfStep[] { - try { - return dir === "in" ? traceBack(lg, p) : traceForward(lg, p); - } catch { - const out: CfStep[] = []; - const seen = new Set(); - const walk = (port: CfPort, back: boolean): void => { - const key = `${port.box}/${port.name}`; - if (seen.has(key)) return; - seen.add(key); - const box = boxOf(lg, port.box); - if (!box) return; - if (back) { - const into = lg.links.filter( - (l) => l.to.box === port.box && (isOut(box, port.name) || l.to.name === port.name), - ); - for (const l of into) { - out.push({ box: l.from.box, port: l.from.name, dir: "out", link: l.id, rule: l.rule }); - walk(l.from, true); - } - } else { - const outs = lg.links.filter((l) => l.from.box === port.box && l.from.name === port.name); - for (const l of outs) { - out.push({ box: l.to.box, port: l.to.name, dir: "in", link: l.id, rule: l.rule }); - const next = boxOf(lg, l.to.box); - for (const o of next ? portsOut(next) : []) walk({ box: next!.id, name: o.name }, false); - } - } - }; - walk(p, dir === "in"); - return out; - } + const steps = dir === "in" ? traceBack(lg, p) : traceForward(lg, p); + return steps.filter((s) => !(s.box === p.box && s.port === p.name)); } /** 그 상자(알약)에서 끊긴 곳 — 쉬운 말 */ -export function breakLines(lg: CfLogic, boxId: string, name?: string): string[] { +export function breakLines( + lg: CfLogic, + boxId: string, + name?: string, + o: DetailOptions = {}, +): string[] { + let list: CfBreak[] = []; try { - return breaks(lg) - .filter((b) => b.box === boxId && (!name || !b.port || b.port === name)) - .map((b) => b.text); + list = breaks(lg, o.values, { logics: o.logics }); } catch { - // 모델의 끊긴 곳 목록이 준비되기 전 — 같은 뜻의 작은 셈 + // 만드는 중 셈이 막히면 끊긴 곳 없음으로 둠 } - const box = boxOf(lg, boxId); - if (!box) return []; - const out: string[] = []; - for (const p of portsIn(box)) { - if (name && p.name !== name) continue; - if (!lg.links.some((l) => l.to.box === boxId && l.to.name === p.name)) out.push(td("EmptyIn")); - else if (box.func?.inputs.some((i) => i.port === p.name && !i.cell)) out.push(td("NoCell")); - } - for (const p of portsOut(box)) { - if (name && p.name !== name) continue; - if (!lg.links.some((l) => l.from.box === boxId && l.from.name === p.name)) out.push(td("Dead")); - } - return out; + return list + .filter((b) => b.box === boxId && (!name || !b.port || b.port === name)) + .map((b) => b.text); } - -export const noValue = (): string => td("NoValue"); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts index 32f1d9fb9..1d7c4043c 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts @@ -8,7 +8,6 @@ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, el } from "@ui/ui_template_elements"; -import type { CfValues } from "./M02_DF_Eval"; import { portsOut, reaches, @@ -22,12 +21,13 @@ import { GATE_OPS, SCALE_OPS, evalSafe, - gateReport, labelOf, + linkNotes, portText, preview, unitOf, valueOf, + type DetailOptions, } from "./M02_DF_Detail"; import { suggestPlaces } from "./M02_DF_UnitPick"; import "./M02_DF_LinkPanel.css"; @@ -317,12 +317,12 @@ export function renderLinkPanel( lg: CfLogic, linkId: string, on: { change(lg: CfLogic): void }, - opts: { values?: CfValues; readOnly?: boolean } = {}, + opts: DetailOptions & { readOnly?: boolean } = {}, ): void { const link = lg.links.find((l) => l.id === linkId); host.classList.add("m02-dflink"); if (!link) return host.replaceChildren(); - const ev = evalSafe(lg, opts.values); + const ev = evalSafe(lg, opts); const table = valueOf(ev, link.from); const headers = Array.isArray(table) && Array.isArray(table[0]) @@ -358,7 +358,7 @@ export function renderLinkPanel( if (rule?.kind === "scale") parts.push(...scaleEditor(ctx, rule)); if (rule?.kind === "round") parts.push(...roundEditor(ctx, rule, unitOf(lg, link.from))); - const pv = preview(link, rule, ev); + const pv = preview(link, ev); parts.push( row( el("span", { className: "m02-dflink-label", text: tk("Before") }), @@ -369,16 +369,11 @@ export function renderLinkPanel( ), row( el("span", { className: "m02-dflink-label", text: tk("Label") }), - el("span", { text: labelOf(lg, rule) }), + el("span", { text: labelOf(rule) }), ), ); - const warns: string[] = []; - if (rule?.kind === "gate") { - const rep = gateReport(lg, link, ev); - if (rep.pass.length > 1) warns.push(tk("WarnBoth", rep.pass.length)); - if (rep.total > 0 && rep.pass.length === 0) warns.push(tk("WarnNone", rep.total)); - } + const warns: string[] = linkNotes(link, ev); if (rule?.kind === "lookup") { if (!rule.conds.length) warns.push(tk("WarnLookup")); if (!rule.col) warns.push(tk("WarnTake")); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts index 3c92bea06..dcd3a4aa3 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts @@ -7,7 +7,6 @@ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, el } from "@ui/ui_template_elements"; -import type { CfValues } from "./M02_DF_Eval"; import type { CfLogic, CfStep } from "./M02_DF_Model"; import { boxOf, @@ -20,6 +19,7 @@ import { trace, unitOf, valueOf, + type DetailOptions, } from "./M02_DF_Detail"; import "./M02_DF_LinkPanel.css"; @@ -50,6 +50,7 @@ const line = (label: string, ...value: (HTMLElement | string)[]): HTMLElement => function steps( lg: CfLogic, list: CfStep[], + arrow: string, empty: string, onBox?: (id: string) => void, ): HTMLElement { @@ -58,7 +59,7 @@ function steps( className: "m02-dfpill-list", children: list.map((s) => { const parts: (HTMLElement | string)[] = [ - s.dir === "out" ? "◀" : "▶", + arrow, onBox ? createButton({ label: portText(lg, { box: s.box, name: s.port }), @@ -68,8 +69,7 @@ function steps( }) : portText(lg, { box: s.box, name: s.port }), ]; - if (s.rule) - parts.push(el("span", { className: "m02-dfpill-rule", text: labelOf(lg, s.rule) })); + if (s.rule) parts.push(el("span", { className: "m02-dfpill-rule", text: labelOf(s.rule) })); return el("li", { className: "m02-dfpill-step", children: parts }); }), }); @@ -82,20 +82,18 @@ export function renderPillPanel( boxId: string, port: string, on: { pick?(boxId: string): void } = {}, - opts: { values?: CfValues } = {}, + opts: DetailOptions = {}, ): void { host.classList.add("m02-dfpill"); const box = boxOf(lg, boxId); if (!box) return host.replaceChildren(); - const ev = evalSafe(lg, opts.values); + const ev = evalSafe(lg, opts); const out = isOut(box, port); - // 받는 알약의 값 = 그 자리로 들어오는 선의 보낸 알약 값(규칙 지나기 전) + // 받는 알약의 값 = 선 규칙을 지나 실제로 들어온 값(없으면 보낸 알약 값) const incoming = lg.links.find((l) => l.to.box === boxId && l.to.name === port); const value = out ? valueOf(ev, { box: boxId, name: port }) - : incoming - ? valueOf(ev, incoming.from) - : undefined; + : (ev?.arrived?.[boxId]?.[port] ?? (incoming ? valueOf(ev, incoming.from) : undefined)); const unit = out ? unitOf(lg, { box: boxId, name: port }) : incoming @@ -103,7 +101,7 @@ export function renderPillPanel( : box.unit; const back = trace(lg, { box: boxId, name: port }, "in"); const forward = trace(lg, { box: boxId, name: port }, "out"); - const breaks = breakLines(lg, boxId, port); + const breaks = breakLines(lg, boxId, port, opts); host.replaceChildren( el("div", { className: "m02-dflink-head", @@ -118,13 +116,13 @@ export function renderPillPanel( el("div", { children: [ el("div", { className: "m02-dflink-label", text: tp("From") }), - steps(lg, back, tp("Start"), on.pick), + steps(lg, back, "◀", tp("Start"), on.pick), ], }), el("div", { children: [ el("div", { className: "m02-dflink-label", text: tp("To") }), - steps(lg, forward, tp("End"), on.pick), + steps(lg, forward, "▶", tp("End"), on.pick), ], }), el("div", {