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,716 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_FuncPanel.ts
|
||||
* 단가산출 다이어그램 함수 상자 판 — 상자 하나의 시트를 우리 스프레드시트 편집기로 · 받은 알약 줄(눌러/끌어 칸에 놓기) ·
|
||||
* 변수 만들기 · 출력 칸 하나 · 단위 · 틀 단추(진짜 엑셀 수식) · 한 줄 오류
|
||||
* 모양은 `docs/raw/M01_쉬운단가/원가다이어그램_정함.md` 「상자 · 선 약속」 · M02_DF_Model 타입 그대로
|
||||
* 편집기는 M02 미리보기가 쓰는 그것(늦게 불러옴) · 옛 시험판 코드는 옮겨 고쳐 씀(import 로 기대지 않음)
|
||||
* 글 [한, 영] 짝은 아래 TEXT
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import type { Sheet } from "../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type {
|
||||
CalcValues,
|
||||
SpreadsheetHandle,
|
||||
Workbook,
|
||||
} from "../../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { CfBox, CfFunc, CfLogic, CfRound, CfVariable } from "./M02_DF_Model";
|
||||
import { evalSafe, type DetailOptions } from "./M02_DF_Detail";
|
||||
import {
|
||||
conditionsOf,
|
||||
drawCells,
|
||||
drawFormula,
|
||||
variableFrom,
|
||||
type ExtraContext,
|
||||
} from "./M02_DF_FuncExtra";
|
||||
import { createRoundRow } from "./M02_DF_RoundRow";
|
||||
import { createUnitPick, suggestPlaces } from "./M02_DF_UnitPick";
|
||||
import { ERR_KEY, FRAMES, TEXT, tf } from "./M02_DF_FuncText";
|
||||
import "./M02_DF_FuncPanel.css";
|
||||
import "@ui/ui_template_text.css";
|
||||
import "@ui/ui_template_banner.css";
|
||||
import "@ui/ui_template_badge.css";
|
||||
|
||||
type Handlers = { change(lg: CfLogic): void };
|
||||
|
||||
interface VarForm {
|
||||
name: string;
|
||||
type: CfVariable["type"];
|
||||
unit: string;
|
||||
min: string;
|
||||
max: string;
|
||||
choices: string;
|
||||
example: string;
|
||||
why: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
boxId: string;
|
||||
lg: CfLogic;
|
||||
on: Handlers;
|
||||
readOnly: boolean;
|
||||
/** 셈에 넘길 것 — 값 묶음 · 다른 로직들 */
|
||||
detail: DetailOptions;
|
||||
handle: SpreadsheetHandle | null;
|
||||
/** 수식 칸 계산값 — 바뀐 뒤에만 새로 받음(받으면 격자가 다시 그려져, 표시 쪽에서 부르면 맴돎) */
|
||||
snap: CalcValues;
|
||||
/** 선 규칙을 지나 이 상자 받는 알약에 들어온 값 */
|
||||
arrived: Record<string, unknown>;
|
||||
/** 편집기가 마지막으로 낸 시트 글 — 같으면 다시 안 끼움 */
|
||||
json: string;
|
||||
cell: string;
|
||||
form: VarForm | null;
|
||||
/** 단위 표준이 제안한 출력 소수자리 */
|
||||
suggest: number | null;
|
||||
parts: {
|
||||
head: HTMLElement;
|
||||
formula: HTMLElement;
|
||||
cells: HTMLElement;
|
||||
got: HTMLElement;
|
||||
vars: HTMLElement;
|
||||
bar: HTMLElement;
|
||||
msg: HTMLElement;
|
||||
sheet: HTMLElement;
|
||||
};
|
||||
observer: MutationObserver | null;
|
||||
redraw(): void;
|
||||
}
|
||||
|
||||
const states = new WeakMap<HTMLElement, State>();
|
||||
let sheetModule: Promise<typeof import("../../A00_Common/spreadsheet/spreadsheet")> | null = null;
|
||||
/** 편집기는 M02 처럼 늦게 불러옴 — 정적 import 금지 */
|
||||
const loadSheet = (): Promise<typeof import("../../A00_Common/spreadsheet/spreadsheet")> =>
|
||||
(sheetModule ??= import("../../A00_Common/spreadsheet/spreadsheet"));
|
||||
|
||||
const boxOf = (st: State): CfBox | undefined => st.lg.boxes.find((b) => b.id === st.boxId);
|
||||
const funcOf = (st: State): CfFunc | undefined => boxOf(st)?.func;
|
||||
const bookOf = (sheet: Sheet): Workbook => ({
|
||||
종류: "통합문서",
|
||||
판: 1,
|
||||
열: "cf",
|
||||
서식: [{}],
|
||||
시트: [structuredClone(sheet)],
|
||||
활성: sheet.id,
|
||||
});
|
||||
|
||||
/** 보이는 시트 — 받은 알약 칸에는 선 규칙을 지나 들어온 값(arrived)을 넣어 보임(저장하는 시트에는 안 넣음) */
|
||||
function withArrived(f: CfFunc, base: Sheet, arrived: Record<string, unknown>): Sheet {
|
||||
const sheet = structuredClone(base);
|
||||
for (const i of f.inputs) {
|
||||
const v = arrived[i.port];
|
||||
if (i.cell && (typeof v === "number" || typeof v === "string" || typeof v === "boolean"))
|
||||
sheet.칸[i.cell.toUpperCase()] = { 값: v };
|
||||
}
|
||||
return sheet;
|
||||
}
|
||||
const shownOf = (st: State): Sheet => withArrived(funcOf(st)!, funcOf(st)!.sheet, st.arrived);
|
||||
|
||||
/* ------------------------------ 바꾸기 ------------------------------ */
|
||||
|
||||
function patchBox(st: State, make: (b: CfBox) => CfBox): void {
|
||||
const next: CfLogic = {
|
||||
...st.lg,
|
||||
boxes: st.lg.boxes.map((b) => (b.id === st.boxId ? make(b) : b)),
|
||||
};
|
||||
st.lg = next;
|
||||
st.on.change(next);
|
||||
}
|
||||
|
||||
const patchFunc = (st: State, make: (f: CfFunc) => CfFunc): void =>
|
||||
patchBox(st, (b) => (b.func ? { ...b, func: make(b.func) } : b));
|
||||
|
||||
const usedCells = (f: CfFunc): Map<string, string> => {
|
||||
const m = new Map<string, string>();
|
||||
for (const i of f.inputs) m.set(i.cell, i.port);
|
||||
for (const v of f.variables) if (v.cell) m.set(v.cell, v.name);
|
||||
if (f.output.cell) m.set(f.output.cell, f.output.name);
|
||||
return m;
|
||||
};
|
||||
|
||||
function placePort(st: State, port: string, cell: string): void {
|
||||
const f = funcOf(st);
|
||||
if (!f || st.readOnly) return;
|
||||
const other = [...f.variables.map((v) => v.cell), f.output.cell].includes(cell);
|
||||
if (other) return void showToast(tf("Busy"), "error");
|
||||
patchFunc(st, (x) => ({
|
||||
...x,
|
||||
inputs: [...x.inputs.filter((i) => i.port !== port && i.cell !== cell), { port, cell }],
|
||||
}));
|
||||
}
|
||||
|
||||
function makeVariable(st: State): void {
|
||||
const form = st.form;
|
||||
const f = funcOf(st);
|
||||
if (!form || !f) return;
|
||||
const made = variableFrom(form, f, st.cell, usedCells);
|
||||
if ("why" in made) {
|
||||
form.why = tf(made.why);
|
||||
return drawVars(st);
|
||||
}
|
||||
const { v, cell } = made;
|
||||
st.form = null;
|
||||
patchFunc(st, (x) => {
|
||||
const sheet = structuredClone(x.sheet);
|
||||
if (v.example !== null) sheet.칸[cell] = { 값: v.example };
|
||||
return {
|
||||
...x,
|
||||
sheet,
|
||||
inputs: x.inputs.filter((i) => i.cell !== cell),
|
||||
variables: [...x.variables.filter((o) => o.name !== v.name && o.cell !== cell), v],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 선으로 들어온 알약 — 받는 알약 이름과 보낸 쪽 단위 */
|
||||
function incoming(st: State): { port: string; unit: string }[] {
|
||||
const out = new Map<string, string>();
|
||||
for (const l of st.lg.links) {
|
||||
if (l.to.box !== st.boxId || out.has(l.to.name)) continue;
|
||||
const src = st.lg.boxes.find((b) => b.id === l.from.box);
|
||||
const unit =
|
||||
src?.func && src.func.output.name === l.from.name
|
||||
? src.func.output.unit
|
||||
: (src?.input?.elements.find((e) => e.name === l.from.name)?.unit ??
|
||||
src?.input?.variables.find((v) => v.name === l.from.name)?.unit ??
|
||||
src?.unit ??
|
||||
"-");
|
||||
out.set(l.to.name, unit || "-");
|
||||
}
|
||||
// 선 없이 칸 없는 받는 알약만 있어도 보임(새 함수 상자가 먼저 만든 자리)
|
||||
for (const i of funcOf(st)?.inputs ?? []) if (!out.has(i.port)) out.set(i.port, "-");
|
||||
return [...out].map(([port, unit]) => ({ port, unit }));
|
||||
}
|
||||
|
||||
function problems(st: State): string[] {
|
||||
const f = funcOf(st);
|
||||
if (!f) return [];
|
||||
const out: string[] = [];
|
||||
for (const [addr, v] of Object.entries(st.snap[f.sheet.id] ?? {})) {
|
||||
if (typeof v === "object" && "오류" in v) {
|
||||
out.push(`${addr} ${tf(ERR_KEY[v.오류] ?? "Err_OTHER")}`);
|
||||
}
|
||||
}
|
||||
for (const p of incoming(st)) {
|
||||
if (!f.inputs.some((i) => i.port === p.port && i.cell)) out.push(`${p.port}${tf("Unplaced")}`);
|
||||
}
|
||||
if (!f.output.cell) out.push(tf("OutNone"));
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ------------------------------ 식 보기 · 칸 연결 표 · 조건 만들기 ------------------------------ */
|
||||
|
||||
/** 보조 파일(M02_DF_FuncExtra)에 넘기는 손잡이 */
|
||||
const extra = (st: State): ExtraContext => ({
|
||||
lg: st.lg,
|
||||
boxId: st.boxId,
|
||||
detail: st.detail,
|
||||
arrived: st.arrived,
|
||||
readOnly: st.readOnly,
|
||||
place: (port, cell) => placePort(st, port, cell),
|
||||
setOutput: (cell) => setOutput(st, cell),
|
||||
patch: (make) => patchFunc(st, make),
|
||||
used: (f) => usedCells(f),
|
||||
});
|
||||
|
||||
/* ------------------------------ 칸 위 표시 ------------------------------ */
|
||||
|
||||
function applyMarks(st: State): void {
|
||||
const f = funcOf(st);
|
||||
if (!f) return;
|
||||
const marks = new Map<string, { kind: string; label: string }>();
|
||||
for (const i of f.inputs.filter((x) => x.cell))
|
||||
marks.set(i.cell, { kind: "in", label: `${tf("Got")} ${i.port}` });
|
||||
for (const v of f.variables)
|
||||
if (v.cell) marks.set(v.cell, { kind: "var", label: `${tf("Vars")} ${v.name}` });
|
||||
if (f.output.cell)
|
||||
marks.set(f.output.cell, { kind: "out", label: `${tf("OutNow")} ${f.output.name}` });
|
||||
const locked = new Set(f.inputs.filter((i) => i.cell).map((i) => i.cell));
|
||||
const list = problems(st);
|
||||
const bad = new Set(list.map((p) => p.split(" ")[0]));
|
||||
for (const c of st.parts.sheet.querySelectorAll<HTMLElement>(".aislo-cell")) {
|
||||
const addr = c.dataset.addr ?? "";
|
||||
for (const a of ["cfKind", "cfName", "cfLock", "cfErr"]) delete c.dataset[a];
|
||||
const m = marks.get(addr);
|
||||
if (m) {
|
||||
c.dataset.cfKind = m.kind;
|
||||
c.dataset.cfName = m.label;
|
||||
}
|
||||
if (locked.has(addr)) c.dataset.cfLock = "🔒";
|
||||
if (bad.has(addr)) c.dataset.cfErr = "1";
|
||||
}
|
||||
st.parts.msg.textContent = list.length
|
||||
? `${tf("Problem")}${list[0]}${list.length > 1 ? ` (+${list.length - 1})` : ""}`
|
||||
: "";
|
||||
st.parts.msg.hidden = !list.length;
|
||||
}
|
||||
|
||||
/* ------------------------------ 위 줄들 ------------------------------ */
|
||||
|
||||
function drawHead(st: State): void {
|
||||
const box = boxOf(st);
|
||||
const unit = createUnitPick({
|
||||
value: box?.unit ?? "-",
|
||||
readOnly: st.readOnly,
|
||||
extra: st.lg.resultUnit ? [st.lg.resultUnit] : [],
|
||||
ariaLabel: tf("Unit"),
|
||||
onPick: (v) => {
|
||||
patchBox(st, (x) => ({
|
||||
...x,
|
||||
unit: v,
|
||||
func: x.func ? { ...x.func, output: { ...x.func.output, unit: v } } : x.func,
|
||||
}));
|
||||
void suggestPlaces(v).then((n) => ((st.suggest = n), drawBar(st)));
|
||||
},
|
||||
});
|
||||
st.parts.head.replaceChildren(
|
||||
el("strong", { className: "m02-dfpanel-title", text: box?.name ?? "" }),
|
||||
el("span", {
|
||||
className: "ui-badge ui-badge--pill ui-badge--neutral",
|
||||
attrs: { "data-size": "24" },
|
||||
text: `${tf("Cell")} ${st.cell}`,
|
||||
}),
|
||||
el("label", {
|
||||
className: "m02-dfpanel-unitbox",
|
||||
children: [el("span", { className: "ui-text ui-text--muted", text: tf("Unit") }), unit],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function drawGot(st: State): void {
|
||||
const f = funcOf(st);
|
||||
const items = incoming(st);
|
||||
const chips = items.map(({ port, unit }) => {
|
||||
const at = f?.inputs.find((i) => i.port === port)?.cell;
|
||||
const b = el("button", {
|
||||
className: `ui-badge ui-badge--pill ui-badge--info m02-dfpanel-chip is-in${at ? " is-placed" : ""}`,
|
||||
text: `${port} (${unit}) · ${at ? `${tf("Placed")} ${at}` : tf("Free")}`,
|
||||
attrs: { type: "button", "data-size": "24", draggable: st.readOnly ? "false" : "true" },
|
||||
});
|
||||
b.addEventListener("click", () => placePort(st, port, st.cell));
|
||||
b.addEventListener("dragstart", (e) => e.dataTransfer?.setData("text/plain", port));
|
||||
return b;
|
||||
});
|
||||
st.parts.got.replaceChildren(
|
||||
el("span", { className: "m02-dfpanel-label", text: tf("Got") }),
|
||||
...(chips.length
|
||||
? chips
|
||||
: [el("span", { className: "ui-text ui-text--muted", text: tf("NoGot") })]),
|
||||
...(chips.length && !st.readOnly
|
||||
? [el("span", { className: "ui-text ui-text--muted", text: tf("GotHint") })]
|
||||
: []),
|
||||
);
|
||||
}
|
||||
|
||||
function formField(
|
||||
st: State,
|
||||
label: keyof typeof TEXT,
|
||||
key: "name" | "unit" | "min" | "max" | "choices" | "example",
|
||||
cls = "",
|
||||
): HTMLElement {
|
||||
const form = st.form!;
|
||||
if (key === "unit") {
|
||||
return el("label", {
|
||||
className: "m02-dfpanel-fieldbox",
|
||||
children: [
|
||||
el("span", { className: "ui-text ui-text--muted", text: tf(label) }),
|
||||
createUnitPick({
|
||||
value: form.unit || "-",
|
||||
ariaLabel: tf(label),
|
||||
onPick: (u) => ((form.unit = u), drawVars(st)),
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
const input = el("input", {
|
||||
className: `ui-input ui-input--sm m02-dfpanel-field ${cls}`,
|
||||
attrs: { type: "text", "aria-label": tf(label) },
|
||||
});
|
||||
input.value = form[key];
|
||||
input.addEventListener("input", () => (form[key] = input.value));
|
||||
input.addEventListener("keydown", (e) => e.key === "Enter" && makeVariable(st));
|
||||
return el("label", {
|
||||
className: "m02-dfpanel-fieldbox",
|
||||
children: [el("span", { className: "ui-text ui-text--muted", text: tf(label) }), input],
|
||||
});
|
||||
}
|
||||
|
||||
function drawVars(st: State): void {
|
||||
const f = funcOf(st);
|
||||
const row: HTMLElement[] = [el("span", { className: "m02-dfpanel-label", text: tf("Vars") })];
|
||||
for (const v of f?.variables ?? []) {
|
||||
const chip = el("span", {
|
||||
className: "ui-badge ui-badge--pill ui-badge--warning m02-dfpanel-chip is-var",
|
||||
attrs: { "data-size": "24" },
|
||||
children: [`${v.name} (${v.unit}) · ${v.cell ?? ""}`],
|
||||
});
|
||||
if (!st.readOnly) {
|
||||
chip.append(
|
||||
createButton({
|
||||
label: "×",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
patchFunc(st, (x) => ({
|
||||
...x,
|
||||
variables: x.variables.filter((o) => o.name !== v.name),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
chip.lastElementChild?.setAttribute("aria-label", `${v.name} ${tf("Remove")}`);
|
||||
}
|
||||
row.push(chip);
|
||||
}
|
||||
if (!f?.variables.length)
|
||||
row.push(el("span", { className: "ui-text ui-text--muted", text: tf("NoVars") }));
|
||||
const parts: HTMLElement[] = [el("div", { className: "m02-dfpanel-row", children: row })];
|
||||
if (!st.readOnly) {
|
||||
const form = st.form;
|
||||
if (!form) {
|
||||
parts.push(
|
||||
el("div", {
|
||||
className: "m02-dfpanel-row",
|
||||
children: [
|
||||
createButton({
|
||||
label: tf("MakeVar"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
st.form = {
|
||||
name: "",
|
||||
type: "number",
|
||||
unit: "",
|
||||
min: "",
|
||||
max: "",
|
||||
choices: "",
|
||||
example: "",
|
||||
why: "",
|
||||
};
|
||||
drawVars(st);
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const type = el("select", {
|
||||
className: "ui-select ui-select--sm m02-dfpanel-field",
|
||||
attrs: { "aria-label": tf("VarType") },
|
||||
});
|
||||
for (const [v, k] of [
|
||||
["number", "Number"],
|
||||
["choice", "Choice"],
|
||||
["text", "Text"],
|
||||
] as const) {
|
||||
type.append(el("option", { attrs: { value: v }, text: tf(k) }));
|
||||
}
|
||||
type.value = form.type;
|
||||
type.addEventListener("change", () => {
|
||||
form.type = type.value as CfVariable["type"];
|
||||
drawVars(st);
|
||||
});
|
||||
parts.push(
|
||||
el("div", {
|
||||
className: "ui-card m02-dfpanel-row m02-dfpanel-form",
|
||||
children: [
|
||||
formField(st, "VarName", "name"),
|
||||
el("label", {
|
||||
className: "m02-dfpanel-fieldbox",
|
||||
children: [
|
||||
el("span", { className: "ui-text ui-text--muted", text: tf("VarType") }),
|
||||
type,
|
||||
],
|
||||
}),
|
||||
formField(st, "VarUnit", "unit"),
|
||||
...(form.type === "number"
|
||||
? [formField(st, "VarMin", "min", "is-num"), formField(st, "VarMax", "max", "is-num")]
|
||||
: []),
|
||||
...(form.type === "choice" ? [formField(st, "VarChoices", "choices")] : []),
|
||||
formField(st, "VarExample", "example"),
|
||||
el("span", {
|
||||
className: "ui-badge ui-badge--pill ui-badge--neutral",
|
||||
attrs: { "data-size": "24" },
|
||||
text: `${tf("VarCell")} ${st.cell}`,
|
||||
}),
|
||||
createButton({
|
||||
label: tf("Make"),
|
||||
variant: "filled",
|
||||
size: "sm",
|
||||
onClick: () => makeVariable(st),
|
||||
}),
|
||||
createButton({
|
||||
label: tf("Cancel"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => ((st.form = null), drawVars(st)),
|
||||
}),
|
||||
...(form.why
|
||||
? [el("span", { className: "ui-text ui-text--warning", text: `⚠ ${form.why}` })]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
st.parts.vars.replaceChildren(...parts);
|
||||
}
|
||||
|
||||
function drawBar(st: State): void {
|
||||
const f = funcOf(st);
|
||||
if (st.readOnly) {
|
||||
st.parts.bar.replaceChildren(
|
||||
el("span", { className: "ui-text ui-text--muted", text: tf("ReadOnly") }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const outNow = f?.output.cell ? `${tf("OutNow")} ${f.output.cell}` : tf("OutNone");
|
||||
st.parts.bar.replaceChildren(
|
||||
el("div", {
|
||||
className: "m02-dfpanel-row",
|
||||
children: [
|
||||
createButton({
|
||||
label: tf("OutBtn"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => setOutput(st, st.cell),
|
||||
}),
|
||||
el("span", {
|
||||
className: "ui-badge ui-badge--pill ui-badge--success m02-dfpanel-chip is-out",
|
||||
attrs: { "data-size": "24" },
|
||||
text: outNow,
|
||||
}),
|
||||
el("span", { className: "ui-text ui-text--muted", text: tf("OutOnly") }),
|
||||
],
|
||||
}),
|
||||
...(f?.output.cell
|
||||
? [
|
||||
createRoundRow({
|
||||
round: f.output.round,
|
||||
suggest: st.suggest,
|
||||
readOnly: st.readOnly,
|
||||
onApply: (round) => writeRound(st, round),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
el("div", {
|
||||
className: "m02-dfpanel-row",
|
||||
children: [
|
||||
el("span", { className: "m02-dfpanel-label", text: tf("Frames") }),
|
||||
...FRAMES.map(([key, formula]) =>
|
||||
createButton({
|
||||
label: tf(key),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => putFormula(st, formula),
|
||||
}),
|
||||
),
|
||||
el("span", { className: "ui-text ui-text--muted", text: tf("FrameHint") }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** 출력 끝수 정하기 — 모델의 output.round (null = 벗김) */
|
||||
function writeRound(st: State, round: CfRound | null): void {
|
||||
patchFunc(st, (x) => {
|
||||
const { round: _old, ...rest } = x.output;
|
||||
return { ...x, output: round ? { ...rest, round } : rest };
|
||||
});
|
||||
}
|
||||
|
||||
/** 출력 칸 하나 정하기 — 다시 고르면 바뀜 */
|
||||
function setOutput(st: State, cell: string): void {
|
||||
const f = funcOf(st);
|
||||
if (!f) return;
|
||||
if (f.inputs.some((i) => i.cell === cell) || f.variables.some((v) => v.cell === cell)) {
|
||||
return void showToast(tf("Busy"), "error");
|
||||
}
|
||||
const row = cell.replace(/^[A-Z]+/, "");
|
||||
const label = f.sheet.칸[`A${row}`];
|
||||
const name = String(label?.값 ?? f.output.name ?? "").trim() || cell;
|
||||
patchFunc(st, (x) => ({ ...x, output: { ...x.output, cell, name } }));
|
||||
}
|
||||
|
||||
/* ------------------------------ 편집기 ------------------------------ */
|
||||
|
||||
/** 틀 수식을 고른 칸에 넣음 — 수식 입력줄에 쳐서 확정하는 길(되돌리기 · 검사 그대로) */
|
||||
function putFormula(st: State, formula: string): void {
|
||||
const root = st.parts.sheet;
|
||||
const bar = root.querySelector<HTMLTextAreaElement>(".ss-formula");
|
||||
const addr = root.querySelector<HTMLInputElement>(".ss-addr");
|
||||
if (!bar || !addr || st.readOnly) return;
|
||||
const at = st.cell;
|
||||
bar.focus();
|
||||
bar.value = formula;
|
||||
bar.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
bar.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "Enter", ctrlKey: true, bubbles: true, cancelable: true }),
|
||||
);
|
||||
addr.value = at;
|
||||
addr.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }),
|
||||
);
|
||||
}
|
||||
|
||||
/** 서식 번호는 이 판이 안 들고 있음 — 지워 둠 */
|
||||
function plain(sheet: CfFunc["sheet"]): CfFunc["sheet"] {
|
||||
for (const cell of Object.values(sheet.칸)) delete cell.서식;
|
||||
return sheet;
|
||||
}
|
||||
|
||||
/** 편집기가 문서를 냄 — 받은 알약이 놓인 칸을 직접 고쳤으면 되돌리고 한 줄 */
|
||||
function onBook(st: State, book: Workbook): void {
|
||||
const f = funcOf(st);
|
||||
if (!f) return;
|
||||
const sheet = plain(structuredClone(book.시트[0]));
|
||||
const shown = shownOf(st);
|
||||
const hit = f.inputs.find(
|
||||
(i) =>
|
||||
i.cell &&
|
||||
JSON.stringify(shown.칸[i.cell.toUpperCase()]) !==
|
||||
JSON.stringify(sheet.칸[i.cell.toUpperCase()]),
|
||||
);
|
||||
if (hit) {
|
||||
showToast(tf("Locked"), "error");
|
||||
st.json = JSON.stringify(shown);
|
||||
return st.handle?.setDoc(bookOf(shown));
|
||||
}
|
||||
// 보이려고 넣은 들어온 값은 저장하는 시트에 남기지 않음
|
||||
for (const i of f.inputs) {
|
||||
const key = i.cell.toUpperCase();
|
||||
if (!i.cell) continue;
|
||||
if (f.sheet.칸[key]) sheet.칸[key] = f.sheet.칸[key];
|
||||
else delete sheet.칸[key];
|
||||
}
|
||||
st.json = JSON.stringify(withArrived(f, sheet, st.arrived));
|
||||
patchFunc(st, (x) => ({ ...x, sheet }));
|
||||
setTimeout(() => refreshSnap(st), 0);
|
||||
}
|
||||
|
||||
/** 계산값 새로 받고 표시 다시 — 편집기가 바뀐 뒤 · 처음 뜬 뒤에만 */
|
||||
function refreshSnap(st: State): void {
|
||||
if (!st.handle) return;
|
||||
st.snap = st.handle.recalc();
|
||||
st.redraw();
|
||||
}
|
||||
|
||||
async function mountSheet(st: State): Promise<void> {
|
||||
const { createSpreadsheet } = await loadSheet();
|
||||
const f = funcOf(st);
|
||||
if (!st.parts.sheet.isConnected || st.handle || !f) return;
|
||||
st.parts.sheet.replaceChildren();
|
||||
const shown = shownOf(st);
|
||||
st.json = JSON.stringify(shown);
|
||||
st.handle = createSpreadsheet(st.parts.sheet, bookOf(shown), {
|
||||
preview: true,
|
||||
tabs: false,
|
||||
readOnly: st.readOnly,
|
||||
conditions: conditionsOf(() => extra(st)),
|
||||
onChange: (b) => onBook(st, b),
|
||||
onSelect: (s) => {
|
||||
st.cell = s.칸;
|
||||
drawHead(st);
|
||||
if (st.form) drawVars(st);
|
||||
},
|
||||
});
|
||||
st.observer = new MutationObserver(() => requestAnimationFrame(() => applyMarks(st)));
|
||||
st.observer.observe(st.parts.sheet, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["data-addr"],
|
||||
});
|
||||
st.parts.sheet.addEventListener("dragover", (e) => !st.readOnly && e.preventDefault());
|
||||
st.parts.sheet.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
const port = e.dataTransfer?.getData("text/plain");
|
||||
const cell = (e.target as Element | null)?.closest<HTMLElement>(".aislo-cell")?.dataset.addr;
|
||||
if (port && cell) placePort(st, port, cell);
|
||||
});
|
||||
refreshSnap(st);
|
||||
}
|
||||
|
||||
/** 이 상자 받는 알약에 선 규칙을 지나 들어온 값 — 셈이 막히면 빈 값 */
|
||||
const arrivedOf = (lg: CfLogic, boxId: string, o: DetailOptions): Record<string, unknown> =>
|
||||
evalSafe(lg, o)?.arrived?.[boxId] ?? {};
|
||||
|
||||
/** 함수 상자 판 그리기 — 같은 상자면 편집기를 그대로 두고 알약 · 표시만 다시 */
|
||||
export function renderFuncPanel(
|
||||
host: HTMLElement,
|
||||
lg: CfLogic,
|
||||
boxId: string,
|
||||
on: Handlers,
|
||||
opts: DetailOptions & { readOnly?: boolean } = {},
|
||||
): void {
|
||||
const old = states.get(host);
|
||||
const readOnly = !!opts.readOnly;
|
||||
if (old && old.boxId === boxId && old.readOnly === readOnly) {
|
||||
old.lg = lg;
|
||||
old.on = on;
|
||||
old.detail = opts;
|
||||
old.arrived = arrivedOf(lg, boxId, opts);
|
||||
const f = funcOf(old);
|
||||
if (old.handle && f && JSON.stringify(shownOf(old)) !== old.json) {
|
||||
old.json = JSON.stringify(shownOf(old));
|
||||
old.handle.setDoc(bookOf(shownOf(old)));
|
||||
setTimeout(() => refreshSnap(old), 0);
|
||||
return;
|
||||
}
|
||||
return old.redraw();
|
||||
}
|
||||
if (old) {
|
||||
old.observer?.disconnect();
|
||||
old.handle?.destroy();
|
||||
}
|
||||
const parts: State["parts"] = {
|
||||
head: el("div", { className: "m02-dfpanel-head" }),
|
||||
formula: el("div"),
|
||||
cells: el("div"),
|
||||
got: el("div", { className: "m02-dfpanel-row" }),
|
||||
vars: el("div", { className: "m02-dfpanel-vars" }),
|
||||
bar: el("div", { className: "m02-dfpanel-bar" }),
|
||||
msg: el("p", {
|
||||
className: "ui-banner ui-banner--warning m02-dfpanel-msg",
|
||||
attrs: { role: "status" },
|
||||
}),
|
||||
sheet: el("div", { className: "m02-dfpanel-sheet", text: tf("Loading") }),
|
||||
};
|
||||
const st: State = {
|
||||
boxId,
|
||||
lg,
|
||||
on,
|
||||
readOnly,
|
||||
detail: opts,
|
||||
arrived: arrivedOf(lg, boxId, opts),
|
||||
handle: null,
|
||||
snap: {},
|
||||
json: "",
|
||||
cell: "A1",
|
||||
form: null,
|
||||
suggest: null,
|
||||
parts,
|
||||
observer: null,
|
||||
redraw: () => {
|
||||
drawHead(st);
|
||||
drawGot(st);
|
||||
drawVars(st);
|
||||
drawBar(st);
|
||||
drawFormula(st.parts.formula, extra(st));
|
||||
drawCells(st.parts.cells, extra(st));
|
||||
applyMarks(st);
|
||||
},
|
||||
};
|
||||
states.set(host, st);
|
||||
host.classList.add("m02-dfpanel");
|
||||
host.replaceChildren(
|
||||
parts.head,
|
||||
parts.formula,
|
||||
parts.got,
|
||||
parts.vars,
|
||||
parts.bar,
|
||||
parts.msg,
|
||||
el("div", { className: "m02-dfpanel-body", children: [parts.sheet, parts.cells] }),
|
||||
);
|
||||
parts.msg.hidden = true;
|
||||
st.redraw();
|
||||
void mountSheet(st);
|
||||
}
|
||||
Reference in New Issue
Block a user