/* ============================================================================= * M01_EasyCalc_Entry.ts * 쉬운 단가산출 시험 길 — 들어가는 단추 · 큰 창 틀 · 시작 화면 · 고치는 화면 틀(위 띠 · 자리들) * 모양은 `docs/raw/M01_쉬운단가/설계.md` · 이 폴더째 지워도 됨(폴더 밖 참조는 LogicLab.ts 두 줄). * 1걸음 = 틀만 — 카드 · 설계 때 정하는 값 · 실무 일위대가표 · 저장 · 지우기 · 되돌리기는 2걸음에 이음. * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, createInputField, createSelectField, createTag, el, showConfirmDialog, showToast, } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import { fetchLogic, fetchLogics, type LogicRow, type LogicSummary, } from "../M01_MasterData_UI_Logic_Api"; import "./M01_EasyCalc_Style.css"; const TEXT = { Open: ["쉬운 단가산출(시험)", "Easy unit-price (trial)"], Title: ["쉬운 단가산출", "Easy unit-price"], CloseWindow: ["창 닫기", "Close window"], Close: ["닫기", "Close"], Loading: ["불러오는 중", "Loading"], LoadFailed: ["목록을 못 불러옴 — {v}", "Could not load the list — {v}"], Start_Ask: ["무엇과 비슷한가요?", "What is it similar to?"], Start_Search: ["이름으로 찾기 — 예: 거푸집", "Search by name — e.g. formwork"], Start_Blank: ["빈 종이로 새로 만들기", "Start from a blank sheet"], Start_Mine: ["내가 만든 것", "Made by me"], Start_MineNone: ["아직 없음", "None yet"], Start_Found: ["찾은 것", "Found"], Start_Count: ["{n} 개 중 {m} 개 보임 — 더 좁혀 찾기", "Showing {m} of {n} — narrow the search"], Start_None: ["찾은 것 없음", "Nothing found"], Base: ["품셈 기본", "Standard"], Mine: ["내가 만든 것", "Made by me"], CannotOpen: ["쉬운 길로 못 엶", "Not for the easy view"], Per: ["{u} 당", "per {u}"], Name: ["이름", "Name"], NamePlaceholder: ["예: 거푸집 설치", "e.g. formwork setup"], Spec: ["크기", "Size"], Unit: ["단위", "Unit"], Total: ["{u} 만드는 데 {v} 원", "{v} won to make {u}"], Undo: ["되돌리기", "Undo"], Save: ["저장", "Save"], Delete: ["지우기", "Delete"], Unsaved: ["저장 안 함", "Not saved"], BaseNote: [ "품셈 기본은 읽기만 — 고치려면 아래 단추로 내 것을 만듦", "Standard items are read-only — use the button below to make your own copy", ], BaseCopy: ["이걸 바탕으로 고치기", "Edit a copy of this"], Trial: ["설계 때 정하는 값", "Values set at design time"], Cards: ["무엇이 들어가나", "What goes in"], Sheet: ["실무 일위대가표로 보기", "View as a unit-cost sheet"], NotYet: ["준비 중", "Coming soon"], Leave_Ask: [ "저장 안 한 것이 있음 — 저장하지 않고 닫을까요?", "There are unsaved changes — close without saving?", ], Leave_Ok: ["저장 안 하고 닫기", "Close without saving"], } as const; type TextKey = keyof typeof TEXT; const te = (key: TextKey, vars: Record = {}): string => Object.entries(vars).reduce( (text, [k, v]) => text.replace(`{${k}}`, String(v)), (TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]) as string, ); /** 목록에 한 번에 보이는 줄 수 */ const SHOWN = 50; /** 고치는 화면이 쥔 것 — 2걸음에 Model 의 EasyDoc 으로 바꿈 */ interface Paper { key: string | null; version: string | null; isBase: boolean; name: string; spec: string; resultUnit: string; row: LogicRow | null; } const isMine = (key: string): boolean => key.startsWith("GX"); /** 돈이 아닌 결과(작업량 등) — 쉬운 길은 「원/…」 만 엶 */ const canOpen = (item: LogicSummary): boolean => item.결과단위.startsWith("원/"); /** 「원/㎡」 → 「㎡」 */ const unitOf = (resultUnit: string): string => resultUnit.replace(/^원\//, ""); /** 「㎡」 → 「1㎡」 · 「100m」 은 그대로 */ const amountOf = (resultUnit: string): string => { const unit = unitOf(resultUnit); return /^\d/.test(unit) ? unit : `1${unit}`; }; /** 「추가」 옆에 놓는 단추 — 누르면 큰 창 */ export function createEasyCalcButton(): HTMLButtonElement { return createButton({ label: te("Open"), variant: "filled", onClick: openEasyCalc }); } function openEasyCalc(): void { let body: HTMLElement; let paper: Paper | null = null; let dirty = false; let logics: LogicSummary[] | null = null; let query = ""; /** 저장 안 한 것이 있으면 한 번 물음 — 닫아도 되면 true */ const mayLeave = async (): Promise => !paper || !dirty || showConfirmDialog(te("Leave_Ask"), te("Leave_Ok")); const notYet = (): void => showToast(te("NotYet"), "info"); /* ---------- 시작 화면 ---------- */ const listRow = (item: LogicSummary): HTMLElement => { const ok = canOpen(item); const row = el("button", { className: "m01-easycalc__item", attrs: { type: "button" }, children: [ el("span", { className: "m01-easycalc__item-name", text: item.이름 }), createTag( isMine(item.키) ? te("Mine") : te("Base"), isMine(item.키) ? "accent" : "neutral", ), el("span", { className: "m01-easycalc__item-unit", text: ok ? te("Per", { u: amountOf(item.결과단위) }) : item.결과단위, }), ...(ok ? [] : [createTag(te("CannotOpen"), "warning")]), ], }); row.disabled = !ok; row.addEventListener("click", () => void openPaper(item)); return row; }; const showStart = async (): Promise => { paper = null; dirty = false; const status = el("p", { className: "m01-easycalc__muted", text: te("Loading") }); const mineHost = el("div", { className: "m01-easycalc__list" }); const foundHost = el("div", { className: "m01-easycalc__list" }); const search = createInputField({ type: "search", placeholder: te("Start_Search"), value: query, onInput: (v) => { query = v; drawFound(); }, }); search.root.classList.add("m01-easycalc__search"); const drawFound = (): void => { if (!logics) return; const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean); const hits = words.length ? logics.filter((x) => words.every((w) => x.이름.toLowerCase().includes(w))) : logics; status.textContent = !hits.length ? te("Start_None") : hits.length > SHOWN ? te("Start_Count", { n: hits.length.toLocaleString(), m: SHOWN }) : ""; foundHost.replaceChildren(...hits.slice(0, SHOWN).map(listRow)); }; body.replaceChildren( el("div", { className: "m01-easycalc__start", children: [ el("div", { className: "m01-easycalc__start-col", children: [ el("h3", { className: "m01-easycalc__ask", text: te("Start_Ask") }), search.root, el("h4", { text: te("Start_Found") }), status, foundHost, ], }), el("div", { className: "m01-easycalc__start-col m01-easycalc__start-side", children: [ createBigButton(te("Start_Blank"), () => openBlank()), el("h4", { text: te("Start_Mine") }), mineHost, ], }), ], }), ); search.input.focus(); try { logics ??= await fetchLogics(); } catch (error) { status.textContent = te("LoadFailed", { v: error instanceof Error ? error.message : String(error), }); return; } const mine = logics.filter((x) => isMine(x.키)); mineHost.replaceChildren( ...(mine.length ? mine.map(listRow) : [el("p", { className: "m01-easycalc__muted", text: te("Start_MineNone") })]), ); drawFound(); }; /* ---------- 고치는 화면 ---------- */ const openBlank = (): void => { paper = { key: null, version: null, isBase: false, name: "", spec: "", resultUnit: unitChoices()[0] ?? "원/㎡", row: null, }; showPaper(); }; const openPaper = async (item: LogicSummary): Promise => { body.replaceChildren(el("p", { className: "m01-easycalc__muted", text: te("Loading") })); try { const one = await fetchLogic(item.키); paper = { key: one.logic.키, version: one.version, isBase: !isMine(one.logic.키), name: one.logic.이름, spec: "", resultUnit: one.logic.결과단위, row: one.logic, }; showPaper(); } catch (error) { showToast(error instanceof Error ? error.message : String(error), "error"); void showStart(); } }; /** 단위 고르기 후보 — 목록에 많이 쓰인 「원/…」 차례 */ const unitChoices = (): string[] => { const count = new Map(); for (const x of logics ?? []) { if (canOpen(x)) count.set(x.결과단위, (count.get(x.결과단위) ?? 0) + 1); } return [...count.entries()].sort((a, b) => b[1] - a[1]).map(([u]) => u); }; const showPaper = (): void => { const p = paper; if (!p) return; const readOnly = p.isBase; const unsaved = createTag(te("Unsaved"), "warning"); unsaved.hidden = true; const total = el("div", { className: "m01-easycalc__total" }); const drawTotal = (): void => { total.textContent = te("Total", { u: amountOf(p.resultUnit), v: "—" }); }; const touched = (): void => { dirty = true; unsaved.hidden = false; }; const name = createInputField({ label: te("Name"), placeholder: te("NamePlaceholder"), value: p.name, onInput: (v) => { p.name = v; touched(); }, }); const spec = createInputField({ label: te("Spec"), value: p.spec, onInput: (v) => { p.spec = v; touched(); }, }); const units = unitChoices(); if (!units.includes(p.resultUnit)) units.unshift(p.resultUnit); const unit = createSelectField({ label: te("Unit"), options: units.map((u) => ({ value: u, text: unitOf(u) })), value: p.resultUnit, disabled: readOnly, onChange: (v) => { p.resultUnit = v; drawTotal(); touched(); }, }); name.input.disabled = readOnly; spec.input.disabled = readOnly; name.root.classList.add("m01-easycalc__name"); const undo = createButton({ label: te("Undo"), variant: "ghost", onClick: notYet }); undo.disabled = true; const close = createButton({ label: te("Close"), variant: "ghost", onClick: () => void (async () => { if (await mayLeave()) void showStart(); })(), }); const actions = readOnly ? [close] : [ undo, createButton({ label: te("Save"), onClick: notYet }), ...(p.key && isMine(p.key) ? [createButton({ label: te("Delete"), variant: "danger", onClick: notYet })] : []), close, ]; const section = (title: string, extra = ""): HTMLElement => el("section", { className: `m01-easycalc__part ${extra}`.trim(), children: [ el("h4", { text: title }), el("p", { className: "m01-easycalc__muted", text: te("NotYet") }), ], }); const sheet = el("details", { className: "m01-easycalc__part m01-easycalc__sheet", children: [ el("summary", { text: te("Sheet") }), el("p", { className: "m01-easycalc__muted", text: te("NotYet") }), ], }); drawTotal(); body.replaceChildren( el("div", { className: "m01-easycalc__strip", children: [ name.root, spec.root, unit.root, total, unsaved, el("div", { className: "m01-easycalc__actions", children: actions }), ], }), ...(readOnly ? [ el("div", { className: "m01-easycalc__base", children: [ el("p", { text: te("BaseNote") }), createBigButton(te("BaseCopy"), notYet), ], }), ] : []), section(te("Trial"), "m01-easycalc__trial"), section(te("Cards"), "m01-easycalc__cards"), sheet, ); if (!readOnly) name.input.focus(); }; // 화면 함수들을 다 만든 뒤에 엶 — mount 가 바로 showStart 를 부름 openModal({ title: te("Title"), closeLabel: te("CloseWindow"), dialogClass: "m01-easycalc", beforeClose: mayLeave, mount: (host) => { body = host; void showStart(); }, }); } function createBigButton(label: string, onClick: () => void): HTMLButtonElement { const button = createButton({ label, variant: "filled", onClick }); button.classList.add("m01-easycalc__big"); return button; }