feat(m01-easycalc): Entry 잇기 — 열기 · 카드 · 표 · 설계 값 띠 · 계산 · 저장 · 본뜨기 · 지우기 · Ctrl+Z · Ctrl+S
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
/* =============================================================================
|
||||
* M01_EasyCalc_Entry.ts
|
||||
* 쉬운 단가산출 시험 길 — 들어가는 단추 · 큰 창 틀 · 시작 화면 · 고치는 화면 틀(위 띠 · 자리들)
|
||||
* 쉬운 단가산출 시험 길 — 들어가는 단추 · 큰 창 틀 · 시작 화면 · 고치는 화면(위 띠) · 모든 파일 잇기
|
||||
* 모양은 `docs/raw/M01_쉬운단가/설계.md` · 이 폴더째 지워도 됨(폴더 밖 참조는 LogicLab.ts 두 줄).
|
||||
* 1걸음 = 틀만 — 카드 · 설계 때 정하는 값 · 실무 일위대가표 · 저장 · 지우기 · 되돌리기는 2걸음에 이음.
|
||||
* 잇기: 열기(openEasy) → toDoc → renderCards · renderSheet · renderTrialStrip → 바뀔 때마다
|
||||
* EasyHistory.push · calcLater → 저장(새로 newEasy · 내 것 editEasy · 품셈 기본은 copyEasy 뒤 그 복사본)
|
||||
* · 지우기(usesOf 먼저 · 물음 한 번) · Ctrl+Z · Ctrl+S
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
@@ -16,15 +18,31 @@ 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 type { CalcAnswer } from "../M01_MasterData_UI_Logic_Api";
|
||||
import { formatMoney } from "../M01_MasterData_UI_Logic_Money";
|
||||
import {
|
||||
calcLater,
|
||||
copyEasy,
|
||||
deleteEasy,
|
||||
editEasy,
|
||||
listEasy,
|
||||
newEasy,
|
||||
openEasy,
|
||||
plainError,
|
||||
usesOf,
|
||||
type EasyListItem,
|
||||
type EasyOpened,
|
||||
} from "./M01_EasyCalc_Api";
|
||||
import type { EasyDoc } from "./M01_EasyCalc_Model";
|
||||
import { renderCards } from "./M01_EasyCalc_Cards";
|
||||
import {
|
||||
checkName,
|
||||
EasyHistory,
|
||||
newDoc,
|
||||
setEasyModelLanguage,
|
||||
toDoc,
|
||||
toRow,
|
||||
type EasyDoc,
|
||||
} from "./M01_EasyCalc_Model";
|
||||
import { renderSheet, renderTrialStrip } from "./M01_EasyCalc_Sheet";
|
||||
import "./M01_EasyCalc_Style.css";
|
||||
|
||||
const TEXT = {
|
||||
@@ -42,10 +60,8 @@ const TEXT = {
|
||||
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}"],
|
||||
CannotOpen: ["쉬운 길로 못 엶", "Not for the easy view"],
|
||||
Name: ["이름", "Name"],
|
||||
NamePlaceholder: ["예: 거푸집 설치", "e.g. formwork setup"],
|
||||
Spec: ["크기", "Size"],
|
||||
@@ -55,15 +71,23 @@ const TEXT = {
|
||||
Save: ["저장", "Save"],
|
||||
Delete: ["지우기", "Delete"],
|
||||
Unsaved: ["저장 안 함", "Not saved"],
|
||||
Saving: ["저장 중", "Saving"],
|
||||
Saved: ["저장했음", "Saved"],
|
||||
SaveFailed: ["저장 못 함", "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"],
|
||||
Copying: ["내 것으로 만드는 중", "Making your copy"],
|
||||
Copied: ["내 것으로 만들었음 — 이제 고칠 수 있음", "Your copy is ready — you can edit it now"],
|
||||
Cards: ["무엇이 들어가나", "What goes in"],
|
||||
Sheet: ["실무 일위대가표로 보기", "View as a unit-cost sheet"],
|
||||
NotYet: ["준비 중", "Coming soon"],
|
||||
Uses_Wait: ["쓰는 곳을 찾는 중 — 10초쯤 걸림", "Looking for places that use it — about 10 s"],
|
||||
Uses_Block: ["쓰는 곳이 있어 못 지움 — {v}", "Cannot delete — it is used by {v}"],
|
||||
Delete_Ask: ["「{v}」 을(를) 지울까요? 되살릴 수 없음", "Delete “{v}”? This cannot be undone"],
|
||||
Delete_Ok: ["지우기", "Delete"],
|
||||
Deleted: ["지웠음", "Deleted"],
|
||||
Leave_Ask: [
|
||||
"저장 안 한 것이 있음 — 저장하지 않고 닫을까요?",
|
||||
"There are unsaved changes — close without saving?",
|
||||
@@ -82,68 +106,6 @@ const te = (key: TextKey, vars: Record<string, string | number> = {}): string =>
|
||||
/** 목록에 한 번에 보이는 줄 수 */
|
||||
const SHOWN = 50;
|
||||
|
||||
/* ponytail: Api(sub4) 속이 「준비 중」 인 동안만 Logic_Api 로 대신함 — Api 가 채워지면 아래 셋의 catch 를 지움 */
|
||||
const loadList = (): Promise<EasyListItem[]> =>
|
||||
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<EasyOpened> =>
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
/** 빈 종이 — 꼭 채울 칸만(검토 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」 은 그대로 */
|
||||
@@ -158,44 +120,54 @@ export function createEasyCalcButton(): HTMLButtonElement {
|
||||
}
|
||||
|
||||
function openEasyCalc(): void {
|
||||
setEasyModelLanguage(currentLanguageIndex);
|
||||
let body: HTMLElement;
|
||||
let paper: EasyDoc | null = null;
|
||||
let dirty = false;
|
||||
/** 고치는 화면이 열려 있을 때만 */
|
||||
let paper: { history: EasyHistory; save: () => Promise<void>; undo: () => void } | null = null;
|
||||
let logics: EasyListItem[] | null = null;
|
||||
let query = "";
|
||||
|
||||
/** 저장 안 한 것이 있으면 한 번 물음 — 닫아도 되면 true */
|
||||
const mayLeave = async (): Promise<boolean> =>
|
||||
!paper || !dirty || showConfirmDialog(te("Leave_Ask"), te("Leave_Ok"));
|
||||
!paper?.history.dirty || showConfirmDialog(te("Leave_Ask"), te("Leave_Ok"));
|
||||
|
||||
const notYet = (): void => showToast(te("NotYet"), "info");
|
||||
/** Ctrl+Z · Ctrl+S — 이 창 안에서만(고르기 창 등 다른 창은 제 몫) */
|
||||
const onKey = (ev: KeyboardEvent): void => {
|
||||
if (!paper || !(ev.ctrlKey || ev.metaKey)) return;
|
||||
const key = ev.key.toLowerCase();
|
||||
if (key === "z" && !ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
paper.undo();
|
||||
} else if (key === "s") {
|
||||
ev.preventDefault();
|
||||
void paper.save();
|
||||
}
|
||||
};
|
||||
|
||||
/* ---------- 시작 화면 ---------- */
|
||||
|
||||
const listRow = (item: EasyListItem): HTMLElement => {
|
||||
const ok = item.canOpen;
|
||||
const row = el("button", {
|
||||
className: "m01-easycalc__item",
|
||||
attrs: { type: "button" },
|
||||
attrs: { type: "button", title: item.where },
|
||||
children: [
|
||||
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.resultUnit) }) : item.resultUnit,
|
||||
text: item.canOpen ? te("Per", { u: amountOf(item.resultUnit) }) : item.resultUnit,
|
||||
}),
|
||||
...(ok ? [] : [createTag(item.whyNot ?? te("CannotOpen"), "warning")]),
|
||||
...(item.canOpen ? [] : [createTag(te("CannotOpen"), "warning")]),
|
||||
],
|
||||
});
|
||||
row.disabled = !ok;
|
||||
row.title = item.where;
|
||||
if (item.whyNot) row.title = item.whyNot;
|
||||
row.disabled = !item.canOpen;
|
||||
row.addEventListener("click", () => void openPaper(item.key));
|
||||
return row;
|
||||
};
|
||||
|
||||
const showStart = async (): Promise<void> => {
|
||||
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" });
|
||||
@@ -252,9 +224,9 @@ function openEasyCalc(): void {
|
||||
search.input.focus();
|
||||
|
||||
try {
|
||||
logics ??= await loadList();
|
||||
logics ??= await listEasy("");
|
||||
} catch (error) {
|
||||
status.textContent = te("LoadFailed", { v: plain(error) });
|
||||
status.textContent = te("LoadFailed", { v: plainError(error) });
|
||||
return;
|
||||
}
|
||||
const mine = logics.filter((x) => !x.isBase);
|
||||
@@ -266,25 +238,6 @@ function openEasyCalc(): void {
|
||||
drawFound();
|
||||
};
|
||||
|
||||
/* ---------- 고치는 화면 ---------- */
|
||||
|
||||
const openBlank = (): void => {
|
||||
const row = blankRow(unitChoices()[0] ?? "원/㎡");
|
||||
paper = { ...docOf({ row, version: "", isBase: false, trial: {} }, null), version: null };
|
||||
showPaper();
|
||||
};
|
||||
|
||||
const openPaper = async (key: string): Promise<void> => {
|
||||
body.replaceChildren(el("p", { className: "m01-easycalc__muted", text: te("Loading") }));
|
||||
try {
|
||||
paper = docOf(await loadOne(key), key);
|
||||
showPaper();
|
||||
} catch (error) {
|
||||
showToast(plain(error), "error");
|
||||
void showStart();
|
||||
}
|
||||
};
|
||||
|
||||
/** 단위 고르기 후보 — 목록에 많이 쓰인 「원/…」 차례 */
|
||||
const unitChoices = (): string[] => {
|
||||
const count = new Map<string, number>();
|
||||
@@ -294,57 +247,59 @@ function openEasyCalc(): void {
|
||||
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 openBlank = (): void => showPaper(newDoc("", unitOf(unitChoices()[0] ?? "원/㎡")), {});
|
||||
|
||||
const openPaper = async (key: string): Promise<void> => {
|
||||
body.replaceChildren(el("p", { className: "m01-easycalc__muted", text: te("Loading") }));
|
||||
try {
|
||||
const opened = await openEasy(key);
|
||||
showPaper(
|
||||
toDoc(opened.row, { key, version: opened.version, isBase: opened.isBase }),
|
||||
opened.trial,
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(plainError(error), "error");
|
||||
void showStart();
|
||||
}
|
||||
};
|
||||
|
||||
const showPaper = (first: EasyDoc, firstTrial: Record<string, unknown>): void => {
|
||||
let doc = first;
|
||||
let trial = firstTrial;
|
||||
let ans: CalcAnswer | null = null;
|
||||
let history = new EasyHistory();
|
||||
history.push(doc);
|
||||
let busy = false;
|
||||
|
||||
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 note = el("p", { className: "m01-easycalc__note", attrs: { role: "status" } });
|
||||
const trialHost = el("div", { className: "m01-easycalc__part m01-easycalc__trial" });
|
||||
const cardsHost = el("div", { className: "m01-easycalc__cards" });
|
||||
const sheetHost = el("div");
|
||||
let trialShape = "";
|
||||
|
||||
const name = createInputField({
|
||||
label: te("Name"),
|
||||
placeholder: te("NamePlaceholder"),
|
||||
value: p.name,
|
||||
onInput: (v) => {
|
||||
p.name = v;
|
||||
touched();
|
||||
},
|
||||
onInput: (v) => edit({ ...doc, name: v }),
|
||||
});
|
||||
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);
|
||||
name.root.classList.add("m01-easycalc__name");
|
||||
const spec = createInputField({ label: te("Spec"), onInput: (v) => edit({ ...doc, spec: v }) });
|
||||
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();
|
||||
},
|
||||
options: [],
|
||||
onChange: (v) => edit({ ...doc, resultUnit: v }),
|
||||
});
|
||||
const undo = createButton({ label: te("Undo"), variant: "ghost", onClick: () => doUndo() });
|
||||
const saveButton = createButton({ label: te("Save"), onClick: () => void doSave() });
|
||||
const deleteButton = createButton({
|
||||
label: te("Delete"),
|
||||
variant: "danger",
|
||||
onClick: () => void doDelete(),
|
||||
});
|
||||
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",
|
||||
@@ -353,62 +308,195 @@ function openEasyCalc(): void {
|
||||
if (await mayLeave()) void showStart();
|
||||
})(),
|
||||
});
|
||||
const actions = readOnly
|
||||
? [close]
|
||||
: [
|
||||
undo,
|
||||
createButton({ label: te("Save"), onClick: notYet }),
|
||||
...(p.key && !p.isBase
|
||||
? [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",
|
||||
const actions = el("div", { className: "m01-easycalc__actions" });
|
||||
const baseBar = el("div", {
|
||||
className: "m01-easycalc__base",
|
||||
children: [
|
||||
el("summary", { text: te("Sheet") }),
|
||||
el("p", { className: "m01-easycalc__muted", text: te("NotYet") }),
|
||||
el("p", { text: te("BaseNote") }),
|
||||
createBigButton(te("BaseCopy"), () => void doCopy()),
|
||||
],
|
||||
});
|
||||
|
||||
drawTotal();
|
||||
/** 위 띠 칸을 doc 로 — 열 때 · 되돌릴 때 · 저장 뒤 */
|
||||
const fillStrip = (): void => {
|
||||
const units = unitChoices();
|
||||
if (!units.includes(doc.resultUnit)) units.unshift(doc.resultUnit);
|
||||
unit.setOptions(
|
||||
units.map((u) => ({ value: u, text: unitOf(u) })),
|
||||
doc.resultUnit,
|
||||
);
|
||||
name.input.value = doc.name;
|
||||
spec.input.value = doc.spec;
|
||||
for (const box of [name.input, spec.input, unit.select]) box.disabled = doc.isBase;
|
||||
actions.replaceChildren(
|
||||
...(doc.isBase ? [] : [undo, saveButton]),
|
||||
...(doc.key && !doc.isBase ? [deleteButton] : []),
|
||||
close,
|
||||
);
|
||||
baseBar.hidden = !doc.isBase;
|
||||
};
|
||||
|
||||
/** 금액 · 카드 · 표 — 계산 답이 올 때마다 · 바뀔 때마다 */
|
||||
const draw = (pending: boolean): void => {
|
||||
const value = ans?.ok ? formatMoney(ans.result ?? ans.sums?.["계"] ?? 0) : "—";
|
||||
total.textContent = te("Total", { u: amountOf(doc.resultUnit), v: value });
|
||||
total.classList.toggle("is-pending", pending);
|
||||
sheetHost.classList.toggle("is-pending", pending);
|
||||
unsaved.hidden = !history.dirty;
|
||||
undo.disabled = !history.canUndo;
|
||||
const shape = JSON.stringify(doc.inputs);
|
||||
if (shape !== trialShape) {
|
||||
trialShape = shape;
|
||||
renderTrialStrip(trialHost, doc.inputs, trial, (next) => {
|
||||
trial = next;
|
||||
calc();
|
||||
});
|
||||
}
|
||||
drawCards();
|
||||
renderSheet(sheetHost, { name: doc.name, resultUnit: doc.resultUnit }, ans);
|
||||
};
|
||||
|
||||
/** 카드 칸에 글을 넣는 중이면 그 칸을 떠난 뒤 그림(넣던 글이 안 날아가게) */
|
||||
const drawCards = (): void => {
|
||||
const typing = cardsHost.contains(document.activeElement) && document.activeElement !== null;
|
||||
if (typing && document.activeElement instanceof HTMLInputElement) {
|
||||
cardsHost.addEventListener("focusout", () => setTimeout(drawCards), { once: true });
|
||||
return;
|
||||
}
|
||||
renderCards(cardsHost, doc, ans, { change: edit });
|
||||
};
|
||||
|
||||
const calc = (): void => {
|
||||
draw(true);
|
||||
calcLater(toRow(doc), trial, (next, plain) => {
|
||||
ans = next;
|
||||
note.textContent = plain ?? "";
|
||||
draw(false);
|
||||
});
|
||||
};
|
||||
|
||||
const edit = (next: EasyDoc): void => {
|
||||
doc = next;
|
||||
history.push(doc);
|
||||
calc();
|
||||
};
|
||||
|
||||
const doUndo = (): void => {
|
||||
const back = history.undo();
|
||||
if (!back) return;
|
||||
doc = back;
|
||||
fillStrip();
|
||||
calc();
|
||||
};
|
||||
|
||||
/** 저장 뒤 · 본뜬 뒤 — 서버가 준 줄로 다시 엶 · 되돌리기 기록은 여기서 새로 */
|
||||
const reopen = (next: EasyDoc): void => {
|
||||
doc = next;
|
||||
history = new EasyHistory();
|
||||
history.push(doc);
|
||||
logics = null;
|
||||
fillStrip();
|
||||
calc();
|
||||
};
|
||||
|
||||
const doSave = async (): Promise<void> => {
|
||||
if (doc.isBase || busy) return;
|
||||
const mine = (logics ?? []).filter((x) => !x.isBase && x.key !== doc.key).map((x) => x.name);
|
||||
const bad = checkName(doc.name, mine);
|
||||
name.setError(bad ?? undefined);
|
||||
if (bad) return void name.input.focus();
|
||||
busy = true;
|
||||
saveButton.disabled = true;
|
||||
note.textContent = te("Saving");
|
||||
try {
|
||||
const row = toRow(doc);
|
||||
const saved =
|
||||
doc.key && doc.version ? await editEasy(doc.key, doc.version, row) : await newEasy(row);
|
||||
reopen(toDoc(saved.row, { key: saved.key, version: saved.version, isBase: false }));
|
||||
note.textContent = "";
|
||||
showToast(te("Saved"), "success");
|
||||
} catch (error) {
|
||||
note.textContent = `${te("SaveFailed")} — ${plainError(error)}`;
|
||||
} finally {
|
||||
busy = false;
|
||||
saveButton.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const doCopy = async (): Promise<void> => {
|
||||
if (!doc.key || busy) return;
|
||||
busy = true;
|
||||
note.textContent = te("Copying");
|
||||
try {
|
||||
const saved = await copyEasy(doc.key);
|
||||
reopen(toDoc(saved.row, { key: saved.key, version: saved.version, isBase: false }));
|
||||
note.textContent = te("Copied");
|
||||
} catch (error) {
|
||||
note.textContent = plainError(error);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
};
|
||||
|
||||
const doDelete = async (): Promise<void> => {
|
||||
if (!doc.key || !doc.version || doc.isBase || busy) return;
|
||||
busy = true;
|
||||
deleteButton.disabled = true;
|
||||
note.textContent = te("Uses_Wait");
|
||||
try {
|
||||
const uses = await usesOf(doc.key);
|
||||
const blocking = uses.filter((u) => u.key);
|
||||
if (blocking.length) {
|
||||
note.textContent = te("Uses_Block", {
|
||||
v: blocking.map((u) => (u.where ? `${u.where} ${u.name}` : u.name)).join(", "),
|
||||
});
|
||||
return;
|
||||
}
|
||||
note.textContent = uses.map((u) => u.name).join(" · ");
|
||||
if (!(await showConfirmDialog(te("Delete_Ask", { v: doc.name }), te("Delete_Ok")))) {
|
||||
return;
|
||||
}
|
||||
await deleteEasy(doc.key, doc.version);
|
||||
logics = null;
|
||||
paper = null;
|
||||
showToast(te("Deleted"), "success");
|
||||
void showStart();
|
||||
} catch (error) {
|
||||
note.textContent = plainError(error);
|
||||
} finally {
|
||||
busy = false;
|
||||
deleteButton.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
paper = {
|
||||
get history() {
|
||||
return history;
|
||||
},
|
||||
save: doSave,
|
||||
undo: doUndo,
|
||||
};
|
||||
|
||||
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 }),
|
||||
],
|
||||
children: [name.root, spec.root, unit.root, total, unsaved, actions],
|
||||
}),
|
||||
note,
|
||||
baseBar,
|
||||
trialHost,
|
||||
el("section", {
|
||||
className: "m01-easycalc__part",
|
||||
children: [el("h4", { text: te("Cards") }), cardsHost],
|
||||
}),
|
||||
el("details", {
|
||||
className: "m01-easycalc__part m01-easycalc__sheet",
|
||||
children: [el("summary", { text: te("Sheet") }), sheetHost],
|
||||
}),
|
||||
...(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();
|
||||
fillStrip();
|
||||
calc();
|
||||
if (!doc.isBase) name.input.focus();
|
||||
};
|
||||
|
||||
// 화면 함수들을 다 만든 뒤에 엶 — mount 가 바로 showStart 를 부름
|
||||
@@ -419,6 +507,7 @@ function openEasyCalc(): void {
|
||||
beforeClose: mayLeave,
|
||||
mount: (host) => {
|
||||
body = host;
|
||||
(host.parentElement ?? host).addEventListener("keydown", onKey);
|
||||
void showStart();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -157,3 +157,19 @@
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 고친 뒤 금액을 다시 셈하는 동안 옛 금액을 흐리게 */
|
||||
.m01-easycalc .is-pending {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.m01-easycalc__note {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.m01-easycalc__note:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user