feat(M01): 「로직 쉽게 보기·만들기」·「로직 흐름 보기」 컨테이너와 계산 코드 걷음 · 정본 로직 화면 복사본 「로직 개선 시험」 컨테이너 더함

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-21 19:26:58 +09:00
co-authored by Claude Sonnet 5
parent 472a63cfde
commit 5b69324207
24 changed files with 471 additions and 4242 deletions
+16 -96
View File
@@ -18,7 +18,6 @@ import {
} from "@ui/ui_template_elements";
import {
ApiError,
copyLogic,
fetchLogic,
fetchLogicFiles,
fetchLogics,
@@ -33,10 +32,7 @@ import {
} from "./M01_MasterData_UI_Logic_Api";
import { buildCalc } from "./M01_MasterData_UI_Logic_Calc";
import { buildEditor } from "./M01_MasterData_UI_Logic_Edit";
import { render as renderCView } from "./M01_MasterData_UI_Logic_CView";
import { isOwnLogic, mountFlow } from "./M01_MasterData_UI_Logic_Flow";
import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterData_UI_Logic_List";
import { openLogicWizard } from "./M01_MasterData_UI_Logic_Wizard";
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
import type { SideHandle } from "./M01_MasterData_UI_Side";
import { tx } from "./M01_MasterData_UI_Logic_Text";
@@ -62,15 +58,12 @@ interface Opened extends Draft {
values: Record<string, string>;
}
/** 세 컨테이너(옛 호표 화면 · 쉽게 보기·만들기 · 흐름 보기)가 서로의 저장 안 한 것을 덮지 않게 캐시 칸을 가름 */
export type LogicMode = "classic" | "easy" | "cview";
const SUFFIX: Record<LogicMode, string> = { classic: "", easy: "_easy", cview: "_cview" };
const cacheKey = (mode: LogicMode): string => `m01_logic_drafts${SUFFIX[mode]}`;
const cacheKey = "m01_logic_drafts";
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
function loadDrafts(mode: LogicMode): Record<string, Draft> {
function loadDrafts(): Record<string, Draft> {
try {
return JSON.parse(sessionStorage.getItem(cacheKey(mode)) ?? "{}") as Record<string, Draft>;
return JSON.parse(sessionStorage.getItem(cacheKey) ?? "{}") as Record<string, Draft>;
} catch {
return {};
}
@@ -85,20 +78,11 @@ export async function mountM01Logic(
host: HTMLElement,
side: SideHandle,
openKey?: string,
mode: LogicMode = "classic",
): Promise<LogicHandle> {
let drafts = loadDrafts(mode);
let drafts = loadDrafts();
let files: LogicFile[] = [];
let opened: Opened | null = null;
let calcInputs = "";
/** 옛 컨테이너 = 호표 표 + 시험 계산 칸 · 쉽게 보기·만들기 컨테이너 = 흐름 그림 */
const easy = mode === "easy";
const view: "flow" | "advanced" | "cview" = easy
? "flow"
: mode === "cview"
? "cview"
: "advanced";
const editor = el("div", { className: "m01-logic__editor" });
const errors = el("div", { className: "m01-logic__reasons", attrs: { hidden: "" } });
const calc = el("aside", { className: "m01-logic__calc" });
@@ -111,7 +95,7 @@ export async function mountM01Logic(
const persist = (): void => {
try {
sessionStorage.setItem(cacheKey(mode), JSON.stringify(drafts));
sessionStorage.setItem(cacheKey, JSON.stringify(drafts));
} catch {
/* 캐시가 막혀도 화면 안의 고친 것은 남음 */
}
@@ -146,7 +130,6 @@ export async function mountM01Logic(
if (opened.origKey !== null && now === opened.original) delete drafts[opened.id];
else drafts[opened.id] = pick(opened, clone(opened.row));
persist();
if (view !== "advanced") return; // 흐름 그림은 제 안에서 다시 셈 — 다시 그리면 펼친 상자가 접힘
const inputs = JSON.stringify(opened.row.입력 ?? []);
if (inputs !== calcInputs) drawCalc();
};
@@ -160,7 +143,7 @@ export async function mountM01Logic(
});
const drawCalc = (): void => {
if (!opened?.row || view !== "advanced") {
if (!opened?.row) {
calc.replaceChildren();
return;
}
@@ -180,12 +163,12 @@ export async function mountM01Logic(
};
const drawEditor = (): void => {
// 고른 로직이 없으면 목록 · 있으면 흐름 그림(기본) 또는 고급 = 호표 표 + 시험 계산
// 고른 로직이 없으면 목록 · 있으면 호표 표 + 시험 계산
const listing = !opened;
list.root.hidden = !listing || !loaded;
waiting.hidden = loaded;
editor.hidden = back.hidden = listing;
calc.hidden = listing || view !== "advanced";
calc.hidden = listing;
if (!opened) return;
if (!opened.row) {
editor.replaceChildren(
@@ -194,28 +177,6 @@ export async function mountM01Logic(
return;
}
const current = opened;
const shown = current.row as LogicRow;
if (view === "cview") {
renderCView(editor, current.origKey ?? current.id);
return;
}
if (view === "flow") {
mountFlow(editor, {
one: {
file: current.file,
version: current.version,
logic: shown,
blocked: false,
reasons: current.reasons,
prices: current.prices,
},
row: shown,
editable: isOwnLogic(current.file, shown),
onChange: touch,
onCopy: () => void onCopy(current),
});
return;
}
buildEditor(editor, {
row: current.row as LogicRow,
file: current.file,
@@ -286,12 +247,8 @@ export async function mountM01Logic(
// 왼쪽 「전체」 + 구분 → 상세구분 거름 — 고르면 목록으로 돌아와 그 범위만
filterHost.replaceChildren(
side.filter({
id: easy ? "쉬움|" : mode === "cview" ? "흐름|" : "로직|",
store: easy
? "m01.filter.로직쉬움"
: mode === "cview"
? "m01.filter.로직흐름"
: "m01.filter.로직",
id: "로직|",
store: "m01.filter.로직",
subs,
total: list.total(),
subLabel: L("M01_LaborSub"),
@@ -306,15 +263,6 @@ export async function mountM01Logic(
persist();
};
/** 「새로 만들기」 모달 — 저장·본떠 저장이 끝나면 그 새 자체 로직을 흐름 그림으로 바로 엶 */
const openWizard = (): void =>
openLogicWizard({
onSaved: (key) =>
void reload()
.then(() => open(logicId("자체", key), "자체", key))
.catch(failed),
});
/** 옛 컨테이너의 「새로 만들기」 — 빈 줄 하나를 초안으로 세움(저장 때 서버가 키를 줌) */
const onDraftNew = (): void => {
const f = files.find((x) => x.file === opened?.file) ?? files[0];
@@ -351,23 +299,6 @@ export async function mountM01Logic(
persist();
show(next);
};
const onNew = easy ? openWizard : onDraftNew;
/** 「본떠 만들기」 — 정본을 자체 파일로 복제하고(계약 5장) 그 새 로직을 바로 엶 */
const onCopy = async (from: Opened): Promise<void> => {
if (!from.origKey) return;
showLoadingOverlay();
try {
const made = await copyLogic(from.origKey);
await reload();
showToast(tx("Edit_Copied", { v: made.key }), "success");
await open(logicId("자체", made.key), "자체", made.key);
} catch (error) {
failed(error);
} finally {
hideLoadingOverlay();
}
};
const onDelete = async (): Promise<void> => {
if (!opened?.row) return;
@@ -457,20 +388,11 @@ export async function mountM01Logic(
children: [
back,
el("h2", { text: tx("Title") }),
// 흐름 보기 = 읽기 + 시험 계산만 — 만들기·지우기·저장 단추 없음
...(mode === "cview"
? []
: [
pending,
createButton({ label: tx("Bar_New"), variant: "ghost", onClick: onNew }),
createButton({
label: tx("Bar_Delete"),
variant: "danger",
onClick: () => void onDelete(),
}),
discardButton,
saveButton,
]),
pending,
createButton({ label: tx("Bar_New"), variant: "ghost", onClick: onDraftNew }),
createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }),
discardButton,
saveButton,
],
});
host.replaceChildren(
@@ -485,9 +407,7 @@ export async function mountM01Logic(
],
}),
);
({ classic: side.logicHost, easy: side.easyHost, cview: side.cviewHost })[mode].replaceChildren(
filterHost,
);
side.logicHost.replaceChildren(filterHost);
show(null);
showLoadingOverlay();
const openKeyOn = async (key: string): Promise<void> => {