Files
Aislo/M01_MasterData/M01_MasterData_UI_LogicLab_Flow.ts
T
eomsangdonandClaude Opus 5.5 da510d3cbc fix(M01): 47-3 로직 상세 오른쪽 = 공용 접는 패널 둘(흐름도 · 원문) — 위아래 접기 카드 걷어냄
- 흐름도 · 원문 = 따로 패널 · 접힌 패널 = 세로 글 띠 · 하나만 열림 · 모두 접힘 됨 · 폭 공유(경계 끌기 · 화살표 키)
- 열린 패널 · 폭 = 취향 키 m01-logic-side · 흐름도 패널 자체 접기 단추 · 끌기 경계는 숨김
- 옛 두 칸 비율 막대(buildSplit) 제거 · 부품 칸 숨김([hidden]) 규칙 덧붙임
- 화면 시나리오 ui_checks/47-3_로직상세_오른쪽패널.js

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
2026-10-05 13:41:48 +09:00

274 lines
10 KiB
TypeScript

/* =============================================================================
* M01_MasterData_UI_LogicLab_Flow.ts
* 로직 상세 오른쪽 흐름도 — 칸 · 폭은 오른쪽 접는 패널(LogicLab_Side) 몫
* 잇기(mountLogicFlow) — 패널(M02_DF_Panel) · 왼쪽 보기(LogicLab_FlowView) · 입력 값 · 다른 로직 흐름 받기
* DataFlow 파일은 import() 로 늦게(엔진이 M01 첫 묶음에 안 들어가게). 정함 `docs/raw/M01_쉬운단가/흐름도_적용_정함.md` 2 · 5 · 6 · 7
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { el } from "@ui/ui_template_elements";
import { API_BASE_URL } from "@config/config_frontend";
import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
import type { CfValues } from "../M02_MasterTemplete/DataFlow/M02_DF_Eval";
import "./M01_MasterData_UI_LogicLab_Flow.css";
const TEXT = {
New: ["신규 로직", "New logic"],
Waiting: ["흐름도 불러오는 중", "Loading the flow diagram"],
Converted: [
"이 로직의 계산 방법을 흐름도로 보여 줌",
"Shows how this logic is calculated as a flow diagram",
],
Failed: ["흐름도를 못 띄움 — {v}", "Couldn't open the flow diagram — {v}"],
} as const;
export const tf = (key: keyof typeof TEXT, v = ""): string =>
TEXT[key][currentLanguageIndex === 1 ? 1 : 0].replace("{v}", v);
export interface SplitHandle {
/** 오른쪽 칸 — 흐름도 패널을 붙이는 곳 */
right: HTMLElement;
/** 패널이 알린 너비(px) · null = 패널 내림 */
setRight(px: number | null): void;
}
/* ── 흐름 받기 · 잇기 ─────────────────────────────────────────────────── */
export interface FlowAnswer {
flow: CfLogic;
converted: boolean;
notes: string[];
}
/** 마스터 로직의 흐름 — 줄에 흐름이 있으면 그대로 · 없으면 서버가 계산 방법을 흐름도로 바꿔 줌 · 못 받으면 null */
export async function fetchFlow(key: string, file = ""): Promise<FlowAnswer | null> {
try {
const q = new URLSearchParams({ key, ...(file ? { file } : {}) });
const r = await fetch(`${API_BASE_URL}/m01/logic/flow?${q}`, { credentials: "include" });
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;
}
}
/** 고치는 중인 줄의 흐름 — 왼쪽 편집기로 고친 줄(저장 전)을 서버가 흐름도로 바꿔 줌 · 못 받으면 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"),
import("./M01_MasterData_UI_LogicLab_FlowView"),
import("../M02_MasterTemplete/DataFlow/M02_DF_Model"),
import("../M02_MasterTemplete/DataFlow/M02_DF_Eval"),
import("../M02_MasterTemplete/DataFlow/M02_DF_Api"),
import("../M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail"),
]).then(([panel, view, model, ev, api, detail]) => ({ panel, view, model, ev, api, detail }));
/** 빈 흐름 — 신규 로직 · 흐름을 못 받은 로직 */
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 designFields(
lg: CfLogic,
): { 이름: string; 단위?: string; 고르기?: (string | number)[]; 범위?: [number, number] }[] {
const seen = new Set<string>();
return varsOf(lg).flatMap((v) => {
if (!v.name || seen.has(v.name)) return [];
seen.add(v.name);
return [
{
이름: v.name,
...(v.unit && v.unit !== "-" ? { 단위: v.unit } : {}),
...(v.type === "choice" && v.choices?.length ? { 고르기: v.choices } : {}),
...(v.range ? { 범위: v.range } : {}),
},
];
});
}
/** 예시값만 다른가 — 그 밖이 같으면 고친 것으로 안 셈 */
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;
title: string;
readOnly: boolean;
/** 변환 알림 — 패널 위 한 줄 */
notes: string[];
converted: boolean;
/** 흐름 로직이면 왼쪽 「지금 내용」 자리(LogicLab_Detail 의 flowHost) */
leftHost(): HTMLElement | null;
onChange(lg: CfLogic): void;
/** 패널 접힘 — 부르는 쪽(오른쪽 두 패널)이 정함 */
folded?: boolean;
onFold?(on: boolean): void;
}
export interface LogicFlow {
/** 밖에서 바꿈(고친 것 버리기) — 패널 · 왼쪽 함께 */
set(lg: CfLogic): void;
/** 왼쪽만 다시(왼쪽 화면을 새로 그린 뒤) */
redrawLeft(): void;
/** 왼쪽 시험 계산 입력이 바뀜 — 같은 이름 변수의 예시값으로(고친 것 아님) */
setExamples(vals: Record<string, string>): void;
/** 패널 접기 · 펴기 */
fold(on: boolean): void;
destroy(): void;
}
/** 오른쪽 패널을 띄우고 왼쪽 보기 · 입력 값 · 다른 로직을 이음 */
export async function mountLogicFlow(opts: LogicFlowOpts): Promise<LogicFlow> {
const M = await loadFlowMods();
const host = el("div", { className: "m01lab-flow__panel" });
const note = el("p", { className: "m01lab-flow__note" });
const notes = [...(opts.converted ? [tf("Converted")] : []), ...opts.notes];
note.textContent = notes.join(" · ");
note.hidden = !notes.length;
opts.split.right.replaceChildren(note, host);
let lg = opts.flow;
let values: CfValues = {};
let valueSig = "";
let logics: Record<string, CfLogic> = {};
let logicSig = "";
let gone = false;
/** 다른 로직 — 마스터 로직(흐름 받기) */
const getLogic = async (key: string): Promise<CfLogic | null> =>
(await fetchFlow(key))?.flow ?? null;
const left = (): void => {
const at = opts.leftHost();
if (!at || gone) return;
M.view.buildFlowView(at, lg, values, { logics });
};
/** 입력 값 · 다른 로직 — 물음이 바뀐 때만 다시 받음 · 받으면 왼쪽 다시 */
const refresh = (): void => {
let ports: Record<string, Record<string, unknown>> = {};
try {
ports = M.ev.evalLogic(lg, values, { logics }).ports;
} catch {
/* 셈이 막혀도 물음은 보냄 */
}
const pill = (p: { box: string; name: string }): unknown => ports[p.box]?.[p.name] ?? null;
const asks = M.detail.queriesOf(lg, pill, M.model.elementPort);
const sig = JSON.stringify(asks.map((a) => a.q));
if (sig !== valueSig) {
valueSig = sig;
void M.detail.fetchValues(M.api.cfValues, lg, pill, M.model.elementPort).then(
(got) => {
if (sig !== valueSig || gone) return;
values = got.values;
left();
refresh(); // 받은 값으로 조건 찾기 값이 바뀌면 한 번 더
},
() => undefined,
);
}
const keys = JSON.stringify(lg.boxes.map((b) => b.input?.logic?.key ?? ""));
if (keys !== logicSig) {
logicSig = keys;
void M.detail.loadLogics(lg, getLogic).then((got) => {
if (keys !== logicSig || gone) return;
logics = got.logics;
left();
});
}
};
const panel = M.panel.mountFlowPanel(host, {
logic: lg,
title: opts.title,
readOnly: opts.readOnly,
logics: getLogic,
onResize: (px) => opts.split.setRight(px),
folded: opts.folded,
onFold: opts.onFold,
onChange: (next) => {
lg = next;
opts.onChange(next);
left();
refresh();
},
});
left();
refresh();
return {
set: (next) => {
lg = next;
panel.set(next);
left();
refresh();
},
redrawLeft: left,
fold: (on) => panel.fold(on),
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();
opts.split.setRight(null);
opts.split.right.replaceChildren();
},
};
}