fix(M01): 시험 입력 한 벌 — 왼쪽 시험 계산 입력 ↔ 흐름 변수 예시값

- 로직을 열 때 왼쪽 시험 계산 입력(자동값 포함)을 이름이 같은 흐름 변수 예시값으로
- 왼쪽 입력을 바꾸면 흐름 예시값도 · 흐름 검증 칸 예시값을 바꾸면 왼쪽 입력도 바꾸고 다시 셈
- 예시값만 바뀐 것은 고친 것(drafts)으로 안 셈(sameShape)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
2026-10-02 01:38:24 +09:00
co-authored by Claude Opus 5.5
parent 2b3ba84fe5
commit f30bc9e82f
2 changed files with 88 additions and 4 deletions
+44 -4
View File
@@ -41,7 +41,10 @@ import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import {
buildSplit,
emptyFlow,
examplesOf,
fetchFlow,
sameShape,
withExamples,
mountLogicFlow,
tf,
type LogicFlow,
@@ -244,12 +247,16 @@ export async function mountM01LogicLab(
split.right.replaceChildren(el("p", { className: "m01lab-flow__wait", text: tf("Waiting") }));
const own = flowOf(row);
const got = own || o.origKey === null ? null : await fetchFlow(o.origKey, o.file);
const flow = own ?? got?.flow ?? (await emptyFlow(row.이름, row.결과단위));
// 시험 입력 한 벌 — 열 때 왼쪽 시험 계산 입력을 흐름 변수 예시값으로
let base = withExamples(
own ?? got?.flow ?? (await emptyFlow(row.이름, row.결과단위)),
o.values,
);
if (opened !== o) return;
try {
const made = await mountLogicFlow({
split,
flow: clone(flow),
flow: clone(base),
title: row.이름 || row.키,
readOnly: !admin,
notes: got?.notes ?? [],
@@ -258,6 +265,12 @@ export async function mountM01LogicLab(
onChange: (lg) => {
if (!o.row || opened !== o) return;
const was = flowOf(o.row);
if (sameShape(lg, was ?? base)) {
// 예시값만 바뀜 — 고친 것 아님 · 왼쪽 시험 계산 입력에 같은 이름으로 넣고 다시 셈
base = lg;
syncLeft(o, examplesOf(lg));
return;
}
o.row.흐름 = lg;
touch();
if (!was) drawEditor(); // 이제 흐름 로직 — 왼쪽을 흐름 보기로
@@ -265,6 +278,7 @@ export async function mountM01LogicLab(
});
if (opened !== o) return made.destroy();
flowNow = made;
made.setExamples(o.values); // 띄우는 사이에 온 자동값까지
} catch (e) {
split.right.replaceChildren(
el("p", { className: "m01-logic__muted", text: tf("Failed", String(e)) }),
@@ -272,6 +286,32 @@ export async function mountM01LogicLab(
}
};
/** 흐름 예시값 → 왼쪽 시험 계산 입력(이름이 있는 칸만) · 바뀌면 왼쪽을 다시 그리고 셈 */
const syncLeft = (o: Opened, ex: Record<string, string>): void => {
const names = new Set((o.row?.입력 ?? []).map((x) => x.이름));
const changed = Object.entries(ex).filter(([k, v]) => names.has(k) && o.values[k] !== v);
if (!changed.length) return;
quiet = true;
for (const [k, v] of changed) o.values[k] = v;
quiet = false;
drawCalc();
calc.querySelector<HTMLButtonElement>("[data-calc-run]")?.click();
};
/** 왼쪽 시험 계산 입력 — 칸이 바뀌면 흐름 예시값으로(이 로직이 열려 있을 때만) */
let quiet = false;
const watchValues = (): Record<string, string> => {
const self: Record<string, string> = new Proxy({} as Record<string, string>, {
set(t, k, v) {
const was = t[k as string];
t[k as string] = v as string;
if (!quiet && was !== v && opened?.values === self)
flowNow?.setExamples({ [k as string]: v as string });
return true;
},
});
return self;
};
const show = (next: Opened | null): void => {
flowNow?.destroy();
flowNow = null;
@@ -294,7 +334,7 @@ export async function mountM01LogicLab(
lines: null,
text: null,
ownHost: ownHost(),
values: {},
values: watchValues(),
flowHost: el("div"),
});
return;
@@ -314,7 +354,7 @@ export async function mountM01LogicLab(
lines: null,
text: null,
ownHost: ownHost(),
values: {},
values: watchValues(),
flowHost: el("div"),
});
} catch (error) {
@@ -150,6 +150,40 @@ export async function emptyFlow(name: string, unit: string): Promise<CfLogic> {
return (await import("../M02_MasterTemplete/DataFlow/M02_DF_Model")).newLogic(name, unit);
}
/* ── 시험 입력 한 벌 — 왼쪽 시험 계산 입력 ↔ 흐름 변수 예시값(이름이 같은 변수끼리) ── */
const varsOf = (lg: CfLogic) =>
lg.boxes.flatMap((b) => [...(b.input?.variables ?? []), ...(b.func?.variables ?? [])]);
/** 흐름 변수 예시값 — 이름 → 글(빈 값은 뺌) */
export function examplesOf(lg: CfLogic): Record<string, string> {
const out: Record<string, string> = {};
for (const v of varsOf(lg))
if (v.example !== null && v.example !== undefined && v.example !== "")
out[v.name] = String(v.example);
return out;
}
/** 예시값을 넣은 새 흐름 — 수처럼 생긴 글은 수로 · 빈 값은 그대로 둠 */
export function withExamples(lg: CfLogic, vals: Record<string, string>): CfLogic {
const next = JSON.parse(JSON.stringify(lg)) as CfLogic;
for (const v of varsOf(next)) {
const raw = (vals[v.name] ?? "").trim();
if (raw) v.example = /^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw;
}
return next;
}
/** 예시값만 다른가 — 그 밖이 같으면 고친 것으로 안 셈 */
export function sameShape(a: CfLogic, b: CfLogic): boolean {
const strip = (lg: CfLogic): string => {
const c = JSON.parse(JSON.stringify(lg)) as CfLogic;
for (const v of varsOf(c)) v.example = null;
return JSON.stringify(c);
};
return strip(a) === strip(b);
}
export interface LogicFlowOpts {
split: SplitHandle;
flow: CfLogic;
@@ -168,6 +202,8 @@ export interface LogicFlow {
set(lg: CfLogic): void;
/** 왼쪽만 다시(왼쪽 화면을 새로 그린 뒤) */
redrawLeft(): void;
/** 왼쪽 시험 계산 입력이 바뀜 — 같은 이름 변수의 예시값으로(고친 것 아님) */
setExamples(vals: Record<string, string>): void;
destroy(): void;
}
@@ -253,6 +289,14 @@ export async function mountLogicFlow(opts: LogicFlowOpts): Promise<LogicFlow> {
refresh();
},
redrawLeft: left,
setExamples: (vals) => {
const next = withExamples(lg, vals);
if (JSON.stringify(next) === JSON.stringify(lg)) return;
lg = next;
panel.set(next);
left();
refresh();
},
destroy: () => {
gone = true;
panel.destroy();