feat(M01): 옛 일위대가 로직 컨테이너는 호표 표 그대로 · 아래에 「로직 쉽게 보기·만들기」 컨테이너를 더해 방식 C + 새로 만들기 모달을 담음

- 일위대가 로직 컨테이너 = 로직을 누르면 옛 호표 표 + 시험 계산 칸이 바로 열림 · 보기 전환 단추와 보기 캐시 값 없앰 · 「새로 만들기」 는 옛 초안 방식
- 새 컨테이너 = 같은 목록·API 를 쓰되 거름 기억과 저장 안 한 것 캐시를 따로 둠 · 로직을 고르면 흐름 그림 · 새로 만들기 모달 · 본떠 만들기 · 상자 고치기

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 12:12:16 +09:00
co-authored by Claude Sonnet 5
parent 5bef49df88
commit 83752f8698
4 changed files with 91 additions and 38 deletions
+52 -32
View File
@@ -61,14 +61,13 @@ interface Opened extends Draft {
values: Record<string, string>;
}
const CACHE_KEY = "m01_logic_drafts";
/** 기본 보기 = 흐름 그림 · 「고급」 = 옛 호표 표 + 시험 계산 칸 */
const VIEW_KEY = "m01.logic.view";
/** 두 컨테이너(옛 호표 화면 · 쉽게 보기·만들기)가 서로의 저장 안 한 것을 덮지 않게 캐시 칸을 가름 */
const cacheKey = (easy: boolean): string => (easy ? "m01_logic_drafts_easy" : "m01_logic_drafts");
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
function loadDrafts(): Record<string, Draft> {
function loadDrafts(easy: boolean): Record<string, Draft> {
try {
return JSON.parse(sessionStorage.getItem(CACHE_KEY) ?? "{}") as Record<string, Draft>;
return JSON.parse(sessionStorage.getItem(cacheKey(easy)) ?? "{}") as Record<string, Draft>;
} catch {
return {};
}
@@ -83,13 +82,14 @@ export async function mountM01Logic(
host: HTMLElement,
side: SideHandle,
openKey?: string,
easy = false,
): Promise<LogicHandle> {
let drafts = loadDrafts();
let drafts = loadDrafts(easy);
let files: LogicFile[] = [];
let opened: Opened | null = null;
let calcInputs = "";
let view: "flow" | "advanced" =
sessionStorage.getItem(VIEW_KEY) === "advanced" ? "advanced" : "flow";
/** 옛 컨테이너 = 호표 표 + 시험 계산 칸 · 쉽게 보기·만들기 컨테이너 = 흐름 그림 */
const view: "flow" | "advanced" = easy ? "flow" : "advanced";
const editor = el("div", { className: "m01-logic__editor" });
const errors = el("div", { className: "m01-logic__reasons", attrs: { hidden: "" } });
@@ -103,7 +103,7 @@ export async function mountM01Logic(
const persist = (): void => {
try {
sessionStorage.setItem(CACHE_KEY, JSON.stringify(drafts));
sessionStorage.setItem(cacheKey(easy), JSON.stringify(drafts));
} catch {
/* 캐시가 막혀도 화면 안의 고친 것은 남음 */
}
@@ -176,7 +176,7 @@ export async function mountM01Logic(
const listing = !opened;
list.root.hidden = !listing || !loaded;
waiting.hidden = loaded;
editor.hidden = back.hidden = viewButton.hidden = listing;
editor.hidden = back.hidden = listing;
calc.hidden = listing || view === "flow";
if (!opened) return;
if (!opened.row) {
@@ -274,8 +274,8 @@ export async function mountM01Logic(
// 왼쪽 「전체」 + 구분 → 상세구분 거름 — 고르면 목록으로 돌아와 그 범위만
filterHost.replaceChildren(
side.filter({
id: "로직|",
store: "m01.filter.로직",
id: easy ? "쉬움|" : "로직|",
store: easy ? "m01.filter.로직쉬움" : "m01.filter.로직",
subs,
total: list.total(),
subLabel: L("M01_LaborSub"),
@@ -291,7 +291,7 @@ export async function mountM01Logic(
};
/** 「새로 만들기」 모달 — 저장·본떠 저장이 끝나면 그 새 자체 로직을 흐름 그림으로 바로 엶 */
const onNew = (): void =>
const openWizard = (): void =>
openLogicWizard({
onSaved: (key) =>
void reload()
@@ -299,6 +299,44 @@ export async function mountM01Logic(
.catch(failed),
});
/** 옛 컨테이너의 「새로 만들기」 — 빈 줄 하나를 초안으로 세움(저장 때 서버가 키를 줌) */
const onDraftNew = (): void => {
const f = files.find((x) => x.file === opened?.file) ?? files[0];
if (!f) return;
const id = `new:${Date.now()}`;
// 원문번호는 절 번호만(부문은 구분 칸) · 키는 저장 때 서버가 줌
const row: LogicRow = {
: "",
: `${tx("New_Key")} ${Object.keys(drafts).length + 1}`,
: "",
: "원/",
: "",
: "공용",
: [],
: [],
: [],
: [],
끝수: null,
};
const next: Opened = {
id,
sub: subOf(f),
file: f.file,
version: f.version,
origKey: null,
row,
original: null,
prices: {},
reasons: [],
lines: null,
values: {},
};
drafts[id] = pick(next, clone(row));
persist();
show(next);
};
const onNew = easy ? openWizard : onDraftNew;
/** 「본떠 만들기」 — 정본을 자체 파일로 복제하고(계약 5장) 그 새 로직을 바로 엶 */
const onCopy = async (from: Opened): Promise<void> => {
if (!from.origKey) return;
@@ -392,23 +430,6 @@ export async function mountM01Logic(
}
};
const viewButton = createButton({
label: tx("View_Advanced"),
variant: "ghost",
onClick: () => {
view = view === "flow" ? "advanced" : "flow";
try {
sessionStorage.setItem(VIEW_KEY, view);
} catch {
/* 캐시가 막혀도 이 판 안에서는 바뀐 보기가 남음 */
}
viewButton.textContent = view === "flow" ? tx("View_Advanced") : tx("View_Flow");
editor.replaceChildren();
drawEditor();
drawCalc();
},
});
viewButton.textContent = view === "flow" ? tx("View_Advanced") : tx("View_Flow");
const saveButton = createButton({ label: tx("Bar_Save"), onClick: () => void onSave() });
const discardButton = createButton({
label: tx("Bar_Discard"),
@@ -420,7 +441,6 @@ export async function mountM01Logic(
children: [
back,
el("h2", { text: tx("Title") }),
viewButton,
pending,
createButton({ label: tx("Bar_New"), variant: "ghost", onClick: onNew }),
createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }),
@@ -440,7 +460,7 @@ export async function mountM01Logic(
],
}),
);
side.logicHost.replaceChildren(filterHost);
(easy ? side.easyHost : side.logicHost).replaceChildren(filterHost);
show(null);
showLoadingOverlay();
const openKeyOn = async (key: string): Promise<void> => {
@@ -85,6 +85,7 @@ const TEXT = {
Save_Failed: ["저장 못 함", "Save failed"],
Save_Nothing: ["고친 것 없음", "Nothing to save"],
Load_Failed: ["불러오지 못함", "Load failed"],
Easy_Title: ["로직 쉽게 보기·만들기", "Logic — easy view & create"],
Loading: ["불러오는 중", "Loading"],
Confirm_Delete: [
"로직 「{v}」 을 지울까요? [저장] 때 파일에서 빠짐",
+19 -3
View File
@@ -54,6 +54,7 @@ function buildPage(): HTMLElement {
let query = "";
let dispose = (): void => {};
let logicMounted = false;
let easyMounted = false;
/* --- 우측: 머리(제목·찾기·저장) + 알림 + 본문 --- */
const title = el("h2", { className: "m01-master__title", text: L("M01_PickFile") });
@@ -69,6 +70,7 @@ function buildPage(): HTMLElement {
children: [title, searchField.root, summary, drop, save],
});
const logicHost = el("div", { className: "m01-master__logic", attrs: { hidden: "" } });
const easyHost = el("div", { className: "m01-master__logic", attrs: { hidden: "" } });
const elementView = el("div", {
className: "m01-master__elements",
children: [head, notice, body],
@@ -86,8 +88,9 @@ function buildPage(): HTMLElement {
onDraftChange(refreshBar);
refreshBar();
const showMode = (mode: "elements" | "logic"): void => {
const showMode = (mode: "elements" | "logic" | "easy"): void => {
logicHost.hidden = mode !== "logic";
easyHost.hidden = mode !== "easy";
elementView.hidden = mode !== "elements";
};
@@ -193,8 +196,21 @@ function buildPage(): HTMLElement {
});
};
/** 「로직 쉽게 보기·만들기」 — 같은 로직 목록에 방식 C 흐름 그림 + 새로 만들기 모달 */
const openEasyTab = (): void => {
dispose();
dispose = (): void => {};
current = null;
showMode("easy");
if (easyMounted) return;
easyMounted = true;
void import("./M01_MasterData_UI_Logic_Page").then((m) =>
m.mountM01Logic(easyHost, side, undefined, true),
);
};
/* --- 좌측: 컨테이너 --- */
const side = buildSide(openFile, () => openLogicTab());
const side = buildSide(openFile, () => openLogicTab(), openEasyTab);
const layout = el("div", { className: "ui-workflow-layout m01-master" });
const main = el("main", {
@@ -202,7 +218,7 @@ function buildPage(): HTMLElement {
children: [
el("div", {
className: "m01-master__panel",
children: [elementView, logicHost],
children: [elementView, logicHost, easyHost],
}),
],
});
+19 -3
View File
@@ -9,6 +9,7 @@ import { createSelectField, el, showToast } from "@ui/ui_template_elements";
import { attachCollapsible } from "@ui/ui_template_collapsible";
import { t as L } from "@ui/ui_template_locale";
import { fetchFiles, fetchSubsAll, type FileInfo, type SubInfo } from "./M01_MasterData_Api_Fetch";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { renderTree, type MakeRow } from "./M01_MasterData_UI_Tree";
/** 컨테이너마다 기억하는 화면 상태 — 찾기 글 · 쪽 · 소요량·계수 표의 용도 대상 거름 (거름은 `m01.filter.*`) */
@@ -74,6 +75,8 @@ export interface SideHandle {
root: HTMLElement;
/** 일위대가 로직 컨테이너 안(로직 목록이 들어갈 자리) */
logicHost: HTMLElement;
/** 「로직 쉽게 보기·만들기」 컨테이너 안(같은 로직 목록이 들어갈 자리) */
easyHost: HTMLElement;
/** 그룹의 파일 판본을 다시 받음(저장 뒤) — 돌려받는 것 = 새 목록 */
refresh: (group: string) => Promise<FileInfo[]>;
setActive: (id: string | null) => void;
@@ -127,8 +130,13 @@ const LEAF: Group[] = ["환율", "요율"];
/** 거름이 있는 컨테이너 — 펼치면 「전체」(또는 닫기 전 거름) 이 바로 열림 */
const FILTERED: Group[] = ["인력", "기계", "소요량", "계수"];
const LOGIC_ID = "로직|";
const EASY_ID = "쉬움|";
export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): SideHandle {
export function buildSide(
onOpen: (pick: Pick) => void,
onLogic: () => void,
onEasy: () => void,
): SideHandle {
const lists = new Map<Group, FileInfo[]>();
const bodies = new Map<Group, HTMLElement>();
const buttons = new Map<string, HTMLElement>();
@@ -403,10 +411,18 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
onLogic();
});
const root = el("div", { className: "m01-side", children: [...groups, logic] });
const easyHost = el("div", { className: "m01-side__items" });
const easy = section(tx("Easy_Title"), easyHost);
easy.querySelector(".ui-collapsible__title")?.addEventListener("click", () => {
if (!easy.classList.contains("is-collapsed")) return;
setActive(buttons.has(EASY_ID) ? EASY_ID : null);
onEasy();
});
const root = el("div", { className: "m01-side", children: [...groups, logic, easy] });
attachCollapsible(root);
void Promise.all(GROUPS.map(refresh)).catch((error) =>
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"),
);
return { root, logicHost, refresh, setActive, filter };
return { root, logicHost, easyHost, refresh, setActive, filter };
}