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:
2026-11-05 19:38:39 +09:00
co-authored by Claude Opus 5.5
parent 9456935d2d
commit 598b80eb26
91 changed files with 203 additions and 19307 deletions
-303
View File
@@ -1,303 +0,0 @@
/* =============================================================================
* M02_DF_Panel.ts
* 오른쪽 흐름도 패널(흐름도 적용 정함 5) — mountFlowPanel(host, opts) → { set, destroy, expand }
* 패널 안 세로: 머리(제목 · 「크게 보기」 · 접기) → 다이어그램(+ 입력 · 함수 · 출력 · 실행 취소 · 맞추기) →
* 상세 내용(상자 · 알약 · 선 판) → 원가 · 검증 칸(접기). 저장 단추 없음(페이지 [저장]) · 바뀔 때마다 onChange.
* 너비 = 부르는 쪽 칸 안 100% · 왼쪽 가장자리 끌기 → onResize(px) · 마지막 너비 localStorage ·
* 접기 = 얇은 띠(「흐름도 ▸」) · 크게 보기 = 화면 전체(같은 흐름 객체 · 닫으면 패널로 · 고친 것 그대로).
* 고치는 핵심은 `M02_DF_Paper.ts`(판 DOM 을 옮겨 끼움).
* ========================================================================== */
import { isCtrlKey } from "../../A00_Common/key_shortcut";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, el } from "@ui/ui_template_elements";
import { openModal } from "@ui/ui_template_modal";
import type { CfLogic } from "./M02_DF_Model";
import { createPaper } from "./M02_DF_Paper";
import { renderStandards } from "./M02_DF_Standards";
import "./M02_DF_Panel.css";
import "@ui/ui_template_toolbar.css";
const TEXT = {
Big: ["크게 보기", "Full screen"],
Standards: ["표준 표", "Standards"],
Toggle: ["흐름도", "Flow"],
ToggleOpen: ["흐름도 펴기", "Open the flow panel"],
ToggleClose: ["흐름도 접기", "Collapse the flow panel"],
Close: ["닫기", "Close"],
Detail: ["상세 내용", "Details"],
Cost: ["원가 · 검증", "Cost · check"],
Grip: ["패널 너비 — 끌거나 화살표 키", "Panel width — drag or arrow keys"],
} as const;
const tf = (key: keyof typeof TEXT): string =>
TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0];
export interface FlowPanelOpts {
logic: CfLogic;
readOnly?: boolean;
title: string;
/** 고칠 때마다 — 객체 그대로(페이지 캐시에 쌓음 · [저장] 은 페이지) */
onChange(lg: CfLogic): void;
/** 다른 로직 입력이 부르는 로직 */
logics?(key: string): Promise<CfLogic | null>;
/** 패널 너비가 바뀜(끌기 · 접기 · 펴기) — 부르는 쪽이 칸 너비를 맞춤 */
onResize?(px: number): void;
/** 접힘을 부르는 쪽이 정함(처음 값) — 주면 접기 단추는 onFold 만 부르고 기억도 부르는 쪽 몫 */
folded?: boolean;
onFold?(on: boolean): void;
}
export interface FlowPanel {
/** 밖에서 바꿈(고친 것 버리기 · 다른 로직 열기) */
set(lg: CfLogic): void;
destroy(): void;
/** 크게 보기 열기 */
expand(): void;
/** 밖에서 접기 · 펴기(onFold 를 부르지 않음) */
fold(on: boolean): void;
}
/* ── 너비(DOM 없음) ─────────────────────────────────────────────────── */
export const PANEL_MIN = 320;
export const STRIP_WIDTH = 40;
/** 패널에서 저절로 맞출 때 가장 작은 확대 — 글이 읽히는 최소 */
export const PANEL_FIT_MIN = 0.6;
/** 패널 「한눈에」 단추 — 전체가 보이게(가장 작은 확대) */
export const PANEL_FIT_ALL_MIN = 0.25;
const WIDTH_KEY = "m02.dfpanel.width";
const FOLD_KEY = "m02.dfpanel.folded";
/** 끌어서 정한 너비 — 왼쪽 가장자리를 왼쪽으로 끌면 넓어짐 · 가장 작게 PANEL_MIN · 가장 크게 화면의 85% */
export function dragWidth(start: number, startX: number, x: number, viewport: number): number {
const max = Math.max(PANEL_MIN, Math.round(viewport * 0.85));
return Math.min(max, Math.max(PANEL_MIN, Math.round(start + (startX - x))));
}
/** 저장해 둔 너비 — 숫자가 아니거나 너무 작으면 null */
export function storedWidth(raw: string | null): number | null {
const n = Number(raw);
return raw && Number.isFinite(n) && n >= PANEL_MIN ? Math.round(n) : null;
}
const readStore = (key: string): string | null => {
try {
return localStorage.getItem(key);
} catch {
return null;
}
};
const writeStore = (key: string, value: string): void => {
try {
localStorage.setItem(key, value);
} catch {
/* 저장 칸이 막힌 브라우저 — 이번 화면에서만 */
}
};
/* ── 패널 ───────────────────────────────────────────────────────────── */
export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPanel {
/** 크게 보기 화면 — 열려 있을 때만 */
let full: HTMLElement | null = null;
const paper = createPaper({
logic: opts.logic,
readOnly: opts.readOnly,
onChange: opts.onChange,
logics: opts.logics,
// 패널 처음 배율 = 글이 읽히는 최소(넘치면 왼쪽 위 입력 상자부터) · 「한눈에」 단추는 전체 · 크게 보기는 90%
fitMin: () => (full ? 0.9 : PANEL_FIT_MIN),
fitAllMin: () => (full ? 0.9 : PANEL_FIT_ALL_MIN),
});
const { diagram, detail, summary, note, undo } = paper.parts;
let width = storedWidth(readStore(WIDTH_KEY));
let folded = opts.folded ?? readStore(FOLD_KEY) === "1";
const tell = (): void =>
opts.onResize?.(folded ? STRIP_WIDTH : (width ?? Math.max(PANEL_MIN, host.clientWidth)));
const sm = (label: string, onClick: () => void, variant: "ghost" | "filled" = "ghost") =>
createButton({ label, size: "sm", variant, onClick });
/* 「흐름도」 단추 하나 — 누르면 접고 · 다시 누르면 펼침(펼치면 머리 · 접으면 얇은 띠에 놓임) */
const toggle = createButton({
label: tf("Toggle"),
size: "sm",
variant: "ghost",
onClick: () => (opts.onFold ? opts.onFold(!folded) : setFolded(!folded)),
});
toggle.classList.add("m02-dfp__toggle");
const head = el("header", {
className: "m02-dfp__head",
children: [
toggle,
el("strong", { className: "m02-dfp__title", text: opts.title }),
sm(tf("Big"), () => expand()),
sm(tf("Standards"), () => openStandards()),
],
});
/** 「표준 표」(단위 · 금액 표준) — 고치기는 시스템 관리자만(보기만이면 칸 · 단추를 막음) */
const openStandards = (): void => {
openModal({
title: tf("Standards"),
closeLabel: tf("Close"),
mount: (box: HTMLElement) => {
const lock = el("fieldset", { className: "m02-dfp__lock" });
lock.disabled = !!opts.readOnly;
box.append(lock);
renderStandards(lock, { change: () => paper.redraw() }, { title: false });
},
});
};
/* 펼친 패널 */
const costBox = el("details", {
className: "m02-dfp__cost",
attrs: { open: "" },
children: [el("summary", { text: tf("Cost") })],
});
const tools = el("div", { className: "ui-toolbar m02-dfp__tools", children: [undo] });
const open = el("div", {
className: "m02-dfp__open",
children: [head, tools, note, diagram, detail, costBox],
});
costBox.append(summary);
/* 접힌 띠 — 같은 「흐름도」 단추만 */
const strip = el("div", { className: "m02-dfp__strip" });
/* 왼쪽 가장자리 — 끌기 · 화살표 키 */
const grip = el("div", {
className: "m02-dfp__grip",
attrs: {
role: "separator",
"aria-orientation": "vertical",
"aria-label": tf("Grip"),
tabindex: "0",
},
});
const setWidth = (px: number, save: boolean): void => {
width = px;
if (save) writeStore(WIDTH_KEY, String(px));
tell();
};
grip.addEventListener("pointerdown", (ev) => {
if (ev.button !== 0) return;
ev.preventDefault();
const start = root.getBoundingClientRect().width;
const startX = ev.clientX;
root.classList.add("is-resizing");
const move = (m: PointerEvent): void =>
setWidth(dragWidth(start, startX, m.clientX, window.innerWidth), false);
const up = (u: PointerEvent): void => {
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
root.classList.remove("is-resizing");
setWidth(dragWidth(start, startX, u.clientX, window.innerWidth), true);
paper.refit();
};
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up);
});
grip.addEventListener("keydown", (ev) => {
const step = ev.shiftKey ? 80 : 20;
const d = ev.key === "ArrowLeft" ? step : ev.key === "ArrowRight" ? -step : 0;
if (!d) return;
ev.preventDefault();
const now = root.getBoundingClientRect().width;
setWidth(dragWidth(now, 0, -d, window.innerWidth), true);
});
const root = el("aside", { className: "m02-dfp", children: [grip, open, strip] });
root.addEventListener("keydown", (ev) => {
if (isCtrlKey(ev, "z") && !ev.shiftKey) {
if ((ev.target as HTMLElement).closest("input, textarea, [contenteditable]")) return;
ev.preventDefault();
paper.undo();
}
});
const setFolded = (on: boolean): void => {
// 접기 전 너비를 기억 — 끈 적이 없어도 펴면 그 너비로
if (on && !folded && width === null && root.isConnected)
width = Math.round(root.getBoundingClientRect().width);
folded = on;
if (!opts.onFold) writeStore(FOLD_KEY, on ? "1" : "0");
root.classList.toggle("is-folded", on);
open.hidden = on;
strip.hidden = !on;
grip.hidden = on;
(on ? strip : head).prepend(toggle);
toggle.setAttribute("aria-expanded", String(!on));
toggle.title = tf(on ? "ToggleOpen" : "ToggleClose");
tell();
if (!on) paper.refit();
};
/* 크게 보기 — 같은 판을 화면 전체로 옮겨 끼우고 닫으면 되돌림 */
const expand = (): void => {
if (full) return;
const close = sm(tf("Close"), () => shrink());
full = el("div", {
className: "m02-dfp-full",
attrs: { role: "dialog", "aria-modal": "true", "aria-label": opts.title, tabindex: "-1" },
children: [
el("header", {
className: "m02-dfp-full__head",
children: [el("strong", { text: opts.title }), undo, close],
}),
note,
el("div", { className: "m02-dfp-full__diagram", children: [diagram] }),
el("div", {
className: "m02-dfp-full__lower",
children: [
el("section", { children: [el("h3", { text: tf("Detail") }), detail] }),
el("section", { children: [el("h3", { text: tf("Cost") }), summary] }),
],
}),
],
});
full.addEventListener("keydown", (ev) => {
if (ev.key === "Escape" && !ev.defaultPrevented) shrink();
if (isCtrlKey(ev, "z") && !ev.shiftKey) {
if ((ev.target as HTMLElement).closest("input, textarea, [contenteditable]")) return;
ev.preventDefault();
paper.undo();
}
});
document.body.append(full);
full.focus();
paper.refit();
};
const shrink = (): void => {
if (!full) return;
full.remove();
full = null;
tools.append(undo);
costBox.append(summary);
// 머리 · 실행 취소 줄 뒤에 판을 처음 차례대로 되돌려 끼움
open.replaceChildren(
open.firstElementChild as HTMLElement,
tools,
note,
diagram,
detail,
costBox,
);
paper.refit();
};
host.replaceChildren(root);
setFolded(folded);
return {
set: (lg) => paper.set(lg),
destroy: () => {
shrink();
root.remove();
},
expand,
fold: (on) => on !== folded && setFolded(on),
};
}