diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Page.ts b/M01_MasterData/M01_MasterData_UI_Logic_Page.ts index a815bb40..f51f9ab6 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Page.ts @@ -61,14 +61,13 @@ interface Opened extends Draft { values: Record; } -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 = (v: T): T => JSON.parse(JSON.stringify(v)) as T; -function loadDrafts(): Record { +function loadDrafts(easy: boolean): Record { try { - return JSON.parse(sessionStorage.getItem(CACHE_KEY) ?? "{}") as Record; + return JSON.parse(sessionStorage.getItem(cacheKey(easy)) ?? "{}") as Record; } catch { return {}; } @@ -83,13 +82,14 @@ export async function mountM01Logic( host: HTMLElement, side: SideHandle, openKey?: string, + easy = false, ): Promise { - 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 => { 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 => { diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Text.ts b/M01_MasterData/M01_MasterData_UI_Logic_Text.ts index 49b4ec40..8f4f05f2 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Text.ts @@ -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}」 을 지울까요? [저장] 때 파일에서 빠짐", diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index 16091ea6..f47ca4ef 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -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], }), ], }); diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index 6c1a4311..8b7c0e95 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -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; 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(); const bodies = new Map(); const buttons = new Map(); @@ -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 }; }