From fb991500fdb275532a62b74ba878d2190b960894 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 09:34:46 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(m01-easycalc):=20=EC=89=AC=EC=9A=B4=20?= =?UTF-8?q?=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20=ED=81=B0=20=EC=B0=BD=20?= =?UTF-8?q?=ED=8B=80=20=C2=B7=20=EC=8B=9C=EC=9E=91=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=C2=B7=20=EA=B3=A0=EC=B9=98=EB=8A=94=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9C=84=20=EB=9D=A0(1=EA=B1=B8=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts | 395 +++++++++++++++++- .../EasyCalc/M01_EasyCalc_Style.css | 157 ++++++- 2 files changed, 536 insertions(+), 16 deletions(-) diff --git a/M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts b/M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts index b930f56cb..3dabd97c8 100644 --- a/M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts +++ b/M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts @@ -1,33 +1,400 @@ /* ============================================================================= * M01_EasyCalc_Entry.ts - * 쉬운 단가산출 시험 길 — 들어가는 단추 + 빈 창(틀만). 이 폴더째 지워도 됨. + * 쉬운 단가산출 시험 길 — 들어가는 단추 · 큰 창 틀 · 시작 화면 · 고치는 화면 틀(위 띠 · 자리들) + * 모양은 `docs/raw/M01_쉬운단가/설계.md` · 이 폴더째 지워도 됨(폴더 밖 참조는 LogicLab.ts 두 줄). + * 1걸음 = 틀만 — 카드 · 설계 때 정하는 값 · 실무 일위대가표 · 저장 · 지우기 · 되돌리기는 2걸음에 이음. * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; -import { createButton, el } from "@ui/ui_template_elements"; +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"], - Ready: ["준비 중", "Coming soon"], + 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; -const te = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex]; +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: () => - openModal({ - title: te("Title"), - closeLabel: te("Close"), - mount: (body) => - body.append(el("p", { className: "m01-easycalc__ready", text: te("Ready") })), + 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; +} diff --git a/M01_MasterData/EasyCalc/M01_EasyCalc_Style.css b/M01_MasterData/EasyCalc/M01_EasyCalc_Style.css index 767d9150d..e23ed19f7 100644 --- a/M01_MasterData/EasyCalc/M01_EasyCalc_Style.css +++ b/M01_MasterData/EasyCalc/M01_EasyCalc_Style.css @@ -1,6 +1,159 @@ -.m01-easycalc__ready { +/* 쉬운 단가산출 — 큰 창 틀 · 시작 화면 · 위 띠 */ + +/* 공용 모달(위 고정)을 화면 대부분으로 — 높이를 고정해 화면이 바뀌어도 창이 안 움직임 */ +.ui-modal.m01-easycalc { + --modal-max: 90vh; + width: min(1280px, 96vw); + height: var(--modal-max); + font-size: var(--text-body); +} + +.m01-easycalc .ui-modal__body { + flex: 1; + min-height: 0; + gap: var(--spacing-16); +} + +.m01-easycalc__muted { margin: 0; - padding: var(--spacing-16) 0; color: var(--color-text-muted); font-size: var(--text-body-sm); } + +.m01-easycalc__big { + min-height: 56px; + padding: 0 var(--spacing-24); + font-size: var(--text-body); +} + +/* 시작 화면 — 왼쪽 찾기 · 오른쪽 새로 만들기 + 내가 만든 것 */ +.m01-easycalc__start { + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); + gap: var(--spacing-24); + min-height: 0; +} + +.m01-easycalc__start-col { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + min-width: 0; +} + +.m01-easycalc__start-col h4 { + margin: var(--spacing-8) 0 0; +} + +.m01-easycalc__ask { + margin: 0; + font-size: var(--text-subheading); +} + +.m01-easycalc__search .ui-input { + font-size: var(--text-body); +} + +.m01-easycalc__list { + display: flex; + flex-direction: column; + gap: var(--spacing-4); +} + +.m01-easycalc__item { + display: flex; + align-items: center; + gap: var(--spacing-8); + width: 100%; + padding: var(--spacing-8) var(--spacing-12); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: var(--color-surface); + color: var(--color-text); + font: inherit; + text-align: left; + cursor: pointer; +} + +.m01-easycalc__item:hover:not(:disabled) { + border-color: var(--color-accent); +} + +.m01-easycalc__item:disabled { + cursor: default; + opacity: 0.6; +} + +.m01-easycalc__item-name { + flex: 1; + min-width: 0; +} + +.m01-easycalc__item-unit { + color: var(--color-text-muted); + font-size: var(--text-body-sm); + white-space: nowrap; +} + +/* 고치는 화면 — 위 띠 */ +.m01-easycalc__strip { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: var(--spacing-12); + padding-bottom: var(--spacing-12); + border-bottom: 1px solid var(--color-border); +} + +.m01-easycalc__strip .ui-field { + min-width: 120px; +} + +.m01-easycalc__name { + flex: 1 1 240px; +} + +.m01-easycalc__total { + font-size: var(--text-subheading); + font-weight: 700; + white-space: nowrap; +} + +.m01-easycalc__actions { + display: flex; + gap: var(--spacing-8); + margin-left: auto; +} + +.m01-easycalc__base { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-16); +} + +.m01-easycalc__base p { + margin: 0; +} + +.m01-easycalc__part { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + padding: var(--spacing-12); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); +} + +.m01-easycalc__part h4 { + margin: 0; +} + +.m01-easycalc__cards { + flex: 1; +} + +.m01-easycalc__sheet summary { + cursor: pointer; + font-weight: 600; +} From de8b71fab6cd54667ab465972e5b3f74b211662a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 09:36:16 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(m01-easycalc):=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=C2=B7=20=EA=B3=A0=EC=B9=98=EB=8A=94=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=84=20Api=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EB=AA=A8=EC=96=91=20=C2=B7=20Model=20EasyDoc=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts | 149 +++++++++++------- 1 file changed, 90 insertions(+), 59 deletions(-) diff --git a/M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts b/M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts index 3dabd97c8..5b052cb8f 100644 --- a/M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts +++ b/M01_MasterData/EasyCalc/M01_EasyCalc_Entry.ts @@ -16,12 +16,15 @@ import { showToast, } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; +import { fetchLogic, fetchLogics, type LogicRow } from "../M01_MasterData_UI_Logic_Api"; import { - fetchLogic, - fetchLogics, - type LogicRow, - type LogicSummary, -} from "../M01_MasterData_UI_Logic_Api"; + listEasy, + openEasy, + plainError, + type EasyListItem, + type EasyOpened, +} from "./M01_EasyCalc_Api"; +import type { EasyDoc } from "./M01_EasyCalc_Model"; import "./M01_EasyCalc_Style.css"; const TEXT = { @@ -79,20 +82,68 @@ const te = (key: TextKey, vars: Record = {}): 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; -} +/* ponytail: Api(sub4) 속이 「준비 중」 인 동안만 Logic_Api 로 대신함 — Api 가 채워지면 아래 셋의 catch 를 지움 */ +const loadList = (): Promise => + listEasy("").catch(() => + fetchLogics().then((all) => + all.map((x) => { + const isBase = !x.키.startsWith("GX"); + const canOpen = x.결과단위.startsWith("원/"); + return { + key: x.키, + name: x.이름, + isBase, + owner: isBase ? te("Base") : te("Mine"), + resultUnit: x.결과단위, + canOpen, + whyNot: canOpen ? null : te("CannotOpen"), + where: `${x.구분} ${x.상세구분}`.trim(), + }; + }), + ), + ); +const loadOne = (key: string): Promise => + openEasy(key).catch(() => + fetchLogic(key).then((one) => ({ + row: one.logic, + version: one.version, + isBase: !one.logic.키.startsWith("GX"), + trial: {}, + })), + ); +const plain = (error: unknown): string => { + try { + return plainError(error); + } catch { + return error instanceof Error ? error.message : String(error); + } +}; -const isMine = (key: string): boolean => key.startsWith("GX"); -/** 돈이 아닌 결과(작업량 등) — 쉬운 길은 「원/…」 만 엶 */ -const canOpen = (item: LogicSummary): boolean => item.결과단위.startsWith("원/"); +/** 빈 종이 — 꼭 채울 칸만(검토 A 「꼭 채울 것」) · 구분 · 상세구분 · 소유 · 키는 서버가 채움 */ +const blankRow = (resultUnit: string): LogicRow => ({ + 키: "", + 원문번호: "", + 이름: "", + 결과단위: resultUnit, + 출처: "자체", + 입력: [], + 중간: [], + 호표: [], + 덧줄: [], + 끝수: null, +}); + +const docOf = (opened: EasyOpened, key: string | null): EasyDoc => ({ + key, + version: opened.version, + isBase: opened.isBase, + name: opened.row.이름, + spec: "", + resultUnit: opened.row.결과단위, + inputs: opened.row.입력 ?? [], + cards: [], + row: opened.row, +}); /** 「원/㎡」 → 「㎡」 */ const unitOf = (resultUnit: string): string => resultUnit.replace(/^원\//, ""); /** 「㎡」 → 「1㎡」 · 「100m」 은 그대로 */ @@ -108,9 +159,9 @@ export function createEasyCalcButton(): HTMLButtonElement { function openEasyCalc(): void { let body: HTMLElement; - let paper: Paper | null = null; + let paper: EasyDoc | null = null; let dirty = false; - let logics: LogicSummary[] | null = null; + let logics: EasyListItem[] | null = null; let query = ""; /** 저장 안 한 것이 있으면 한 번 물음 — 닫아도 되면 true */ @@ -121,26 +172,24 @@ function openEasyCalc(): void { /* ---------- 시작 화면 ---------- */ - const listRow = (item: LogicSummary): HTMLElement => { - const ok = canOpen(item); + const listRow = (item: EasyListItem): HTMLElement => { + const ok = item.canOpen; 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-name", text: item.name }), + createTag(item.owner, item.isBase ? "neutral" : "accent"), el("span", { className: "m01-easycalc__item-unit", - text: ok ? te("Per", { u: amountOf(item.결과단위) }) : item.결과단위, + text: ok ? te("Per", { u: amountOf(item.resultUnit) }) : item.resultUnit, }), - ...(ok ? [] : [createTag(te("CannotOpen"), "warning")]), + ...(ok ? [] : [createTag(item.whyNot ?? te("CannotOpen"), "warning")]), ], }); row.disabled = !ok; - row.addEventListener("click", () => void openPaper(item)); + row.title = item.where; + row.addEventListener("click", () => void openPaper(item.key)); return row; }; @@ -165,7 +214,7 @@ function openEasyCalc(): 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.filter((x) => words.every((w) => x.name.toLowerCase().includes(w))) : logics; status.textContent = !hits.length ? te("Start_None") @@ -203,14 +252,12 @@ function openEasyCalc(): void { search.input.focus(); try { - logics ??= await fetchLogics(); + logics ??= await loadList(); } catch (error) { - status.textContent = te("LoadFailed", { - v: error instanceof Error ? error.message : String(error), - }); + status.textContent = te("LoadFailed", { v: plain(error) }); return; } - const mine = logics.filter((x) => isMine(x.키)); + const mine = logics.filter((x) => !x.isBase); mineHost.replaceChildren( ...(mine.length ? mine.map(listRow) @@ -222,34 +269,18 @@ function openEasyCalc(): void { /* ---------- 고치는 화면 ---------- */ const openBlank = (): void => { - paper = { - key: null, - version: null, - isBase: false, - name: "", - spec: "", - resultUnit: unitChoices()[0] ?? "원/㎡", - row: null, - }; + const row = blankRow(unitChoices()[0] ?? "원/㎡"); + paper = { ...docOf({ row, version: "", isBase: false, trial: {} }, null), version: null }; showPaper(); }; - const openPaper = async (item: LogicSummary): Promise => { + const openPaper = async (key: string): 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, - }; + paper = docOf(await loadOne(key), key); showPaper(); } catch (error) { - showToast(error instanceof Error ? error.message : String(error), "error"); + showToast(plain(error), "error"); void showStart(); } }; @@ -258,7 +289,7 @@ function openEasyCalc(): void { const unitChoices = (): string[] => { const count = new Map(); for (const x of logics ?? []) { - if (canOpen(x)) count.set(x.결과단위, (count.get(x.결과단위) ?? 0) + 1); + if (x.canOpen) count.set(x.resultUnit, (count.get(x.resultUnit) ?? 0) + 1); } return [...count.entries()].sort((a, b) => b[1] - a[1]).map(([u]) => u); }; @@ -327,7 +358,7 @@ function openEasyCalc(): void { : [ undo, createButton({ label: te("Save"), onClick: notYet }), - ...(p.key && isMine(p.key) + ...(p.key && !p.isBase ? [createButton({ label: te("Delete"), variant: "danger", onClick: notYet })] : []), close,