fix(M01): 흐름도 패널 글 · 왼쪽 고침 → 오른쪽 따라감 · 시험판 길 지움

- 패널 위 글 「이 로직의 계산 방법을 흐름도로 보여 줌」(「옛 식」 · 「정본」 말 지움)
- 왼쪽 편집기로 고친 뒤 잠시 쉬면 고친 줄을 서버 변환(POST /api/m01/logic/flow {file, key, row})으로 다시 받아 오른쪽에 넣음 · 마지막 물음만 받음 · 길이 아직 없으면 아무 일 없음
- 다른 로직 받기에서 흐름도 시험판(DF-) cfGet 길 지움

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 07:43:09 +09:00
co-authored by Claude Opus 5.5
parent d202a3a6d8
commit 45b614ca55
2 changed files with 51 additions and 10 deletions
+25 -5
View File
@@ -43,6 +43,7 @@ import {
emptyFlow,
examplesOf,
fetchFlow,
fetchFlowFor,
sameShape,
withExamples,
mountLogicFlow,
@@ -78,9 +79,11 @@ interface Opened extends Draft {
values: Record<string, string>;
/** 흐름 로직의 왼쪽 「지금 내용」 자리 — 흐름도 왼쪽 보기가 그림 */
flowHost: HTMLElement;
/** 오른쪽 흐름도의 기준(예시값만 다른지 견줄 때) */
flowBase?: CfLogic;
}
/** 줄의 흐름(있으면 흐름도가 정본) */
/** 줄의 흐름(있으면 흐름도로 셈) */
const flowOf = (row: LogicRow | null): CfLogic | null => (row?.흐름 as CfLogic | undefined) ?? null;
const OWN_FILE = "로직_자체.json";
@@ -165,6 +168,7 @@ export async function mountM01LogicLab(
if (opened.origKey !== null && now === opened.original) delete drafts[opened.id];
else drafts[opened.id] = pick(opened, clone(opened.row));
persist();
if (!flowOf(opened.row)) scheduleFlow(opened); // 왼쪽 편집기로 고침 — 오른쪽 흐름도도 따라감
const inputs = JSON.stringify(opened.row.입력 ?? []);
if (inputs !== calcInputs) drawCalc();
if (isOwnLogic(opened.row)) {
@@ -241,6 +245,22 @@ export async function mountM01LogicLab(
flowNow?.redrawLeft();
};
/** 왼쪽 고침 뒤 잠시 쉬면 고친 줄을 서버가 흐름도로 바꿔 줌 → 오른쪽에 넣음 · 마지막 물음만 받음 */
let flowTimer: number | undefined;
let flowSeq = 0;
const scheduleFlow = (o: Opened): void => {
window.clearTimeout(flowTimer);
flowTimer = window.setTimeout(() => {
const seq = ++flowSeq;
if (!o.row) return;
void fetchFlowFor(o.row, o.origKey ?? "", o.file).then((got) => {
if (!got || seq !== flowSeq || opened !== o || !flowNow || flowOf(o.row)) return;
o.flowBase = withExamples(got.flow, o.values);
flowNow.set(clone(o.flowBase));
});
}, 700);
};
/** 오른쪽 흐름도 — 줄의 흐름 · 없으면 서버 변환(흐름 받기) · 그것도 없으면 빈 흐름 */
const startFlow = async (o: Opened): Promise<void> => {
const row = o.row;
@@ -249,7 +269,7 @@ export async function mountM01LogicLab(
const own = flowOf(row);
const got = own || o.origKey === null ? null : await fetchFlow(o.origKey, o.file);
// 시험 입력 한 벌 — 열 때 왼쪽 시험 계산 입력을 흐름 변수 예시값으로
let base = withExamples(
o.flowBase = withExamples(
own ?? got?.flow ?? (await emptyFlow(row.이름, row.결과단위)),
o.values,
);
@@ -257,7 +277,7 @@ export async function mountM01LogicLab(
try {
const made = await mountLogicFlow({
split,
flow: clone(base),
flow: clone(o.flowBase),
title: row.이름 || row.키,
readOnly: !admin,
notes: got?.notes ?? [],
@@ -266,9 +286,9 @@ export async function mountM01LogicLab(
onChange: (lg) => {
if (!o.row || opened !== o) return;
const was = flowOf(o.row);
if (sameShape(lg, was ?? base)) {
if (sameShape(lg, was ?? o.flowBase ?? lg)) {
// 예시값만 바뀜 — 고친 것 아님 · 왼쪽 시험 계산 입력에 같은 이름으로 넣고 다시 셈
base = lg;
o.flowBase = lg;
syncLeft(o, examplesOf(lg));
return;
}
@@ -18,8 +18,8 @@ const TEXT = {
New: ["신규 로직", "New logic"],
Waiting: ["흐름도 불러오는 중", "Loading the flow diagram"],
Converted: [
"옛 식을 흐름도로 바꿔 보임 — 저장하면 흐름도가 정본",
"Converted from the old formula — saving makes the flow the master",
"이 로직의 계산 방법을 흐름도로 보여 줌",
"Shows how this logic is calculated as a flow diagram",
],
Failed: ["흐름도를 못 띄움 — {v}", "Couldn't open the flow diagram — {v}"],
} as const;
@@ -122,7 +122,7 @@ export interface FlowAnswer {
notes: string[];
}
/** 마스터 로직의 흐름 — 줄에 흐름이 있으면 그대로 · 없으면 서버가 옛 식을 바꿔 줌 · 길이 없거나 못 받으면 null */
/** 마스터 로직의 흐름 — 줄에 흐름이 있으면 그대로 · 없으면 서버가 계산 방법을 흐름도로 바꿔 줌 · 못 받으면 null */
export async function fetchFlow(key: string, file = ""): Promise<FlowAnswer | null> {
try {
const q = new URLSearchParams({ key, ...(file ? { file } : {}) });
@@ -135,6 +135,27 @@ export async function fetchFlow(key: string, file = ""): Promise<FlowAnswer | nu
}
}
/** 고치는 중인 줄의 흐름 — 왼쪽 편집기로 고친 줄(저장 전)을 서버가 흐름도로 바꿔 줌 · 못 받으면 null */
export async function fetchFlowFor(
row: unknown,
key: string,
file: string,
): Promise<FlowAnswer | null> {
try {
const r = await fetch(`${API_BASE_URL}/m01/logic/flow`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file, key, row }),
});
if (!r.ok) return null;
const got = (await r.json()) as Partial<FlowAnswer>;
return got.flow ? { flow: got.flow, converted: !!got.converted, notes: got.notes ?? [] } : null;
} catch {
return null;
}
}
const loadFlowMods = () =>
Promise.all([
import("../M02_MasterTemplete/DataFlow/M02_DF_Panel"),
@@ -223,9 +244,9 @@ export async function mountLogicFlow(opts: LogicFlowOpts): Promise<LogicFlow> {
let logicSig = "";
let gone = false;
/** 다른 로직 — 흐름도 저장본(DF-) · 마스터 로직(흐름 받기) */
/** 다른 로직 — 마스터 로직(흐름 받기) */
const getLogic = async (key: string): Promise<CfLogic | null> =>
key.startsWith("DF-") ? M.api.cfGet(key) : ((await fetchFlow(key))?.flow ?? null);
(await fetchFlow(key))?.flow ?? null;
const left = (): void => {
const at = opts.leftHost();