/* ============================================================================= * M02_MyMachine_Form.ts * M02 내 기계 양식 한 장(PLAN 821-3 · 요구서 796 1 · 3 장) — 기계 양식 부품(`ui_template_machine_form` · 821-2) + * [저장] · [지우기] · 「개인 값」 표지 · 같은 이름 · 규격 알림 · 서버 정본 시간당 · 쓰는 곳. * 기본 = 손료계수 「바로 넣기」 · 「시간당 값 직접」 꼴 · 직종 · 연료 = M01 목록 고르기 · 옆 계산식 즉시(계수 짝 TS) * [저장] 때만 서버(자동저장 없음) — 서버가 정본으로 다시 셈한 시간당을 받아 보임 * 새 기계 = 「비슷한 기계에서 시작」(M01 · 내 기계 · 고른 키 = 서버 `본뜬`) 또는 빈 양식 · `seed` = 품셈 기계 복사 * 지우기 = 쓰는 단가산출 이름 보이고 확인 + 「이미 만든 단가산출은 마지막 값 유지」 * 부르는 곳 = 내 기계 화면(`M02_MyMachine_View`) · 기계 고르기 창 「새 기계 만들기」 · 줄 쌓기 「복사해 내 기계로」(창 `openMyMachineModal`) * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; import { createButton } from "@ui/ui_template_button"; import { el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { createMachineForm, HOURLY_KEY, type MachineSimilar, type MachineValues, } from "@ui/ui_template_machine_form"; import { openModal } from "@ui/ui_template_modal"; import { createText } from "@ui/ui_template_text"; import { createMyMachine, deleteMyMachine, fetchMyMachineLists, findSimilar, readMyMachine, updateMyMachine, type MyHourly, type MyMachine, type MyMachineBody, type MyMachineOne, type SimilarRow, } from "./M02_MyMachine_Api"; import { mt } from "./M02_MyMachine_Text"; import "./M02_MyMachine.css"; export interface MyMachineFormOptions { owner: string | null; /** 고칠 내 기계 키 — 없으면 새 기계 */ key?: string; /** 새 기계 바탕(복사해 내 기계로) — 값 · 이름 · 규격 · 본뜬 키 */ seed?: SimilarRow; /** 새 기계면 「비슷한 기계에서 시작」 을 펼침 */ openSimilar?: boolean; onSaved: (got: MyMachineOne) => void; onDeleted?: () => void; onBack?: () => void; onDirty?: (dirty: boolean) => void; } export interface MyMachineFormHandle { root: HTMLElement; dirty: () => boolean; } const won = (v: number | undefined): string => `${(v ?? 0).toLocaleString("ko-KR", { maximumFractionDigits: 2 })}${mt("Won")}`; /** 줄(저장 꼴) → 양식 값 묶음 — 직접 = 시간당 칸 하나 */ export function valuesOf(row: Pick): MachineValues { return row.방식 === "직접" ? { [HOURLY_KEY]: { ...(row.시간당 ?? {}) } } : { ...row.값 }; } /** 서버 시간당 한 줄 글 */ export function hourlyText(h: MyHourly | undefined): string { if (!h?.ok) return `${mt("Hourly_None")}${h?.reason ? ` — ${h.reason}` : ""}`; return `${won(h.계)} (${mt("Labor")} ${won(h.노무비)} · ${mt("Material")} ${won(h.재료비)} · ${mt("Expense")} ${won(h.경비)})`; } export async function mountMyMachineForm( host: HTMLElement, opt: MyMachineFormOptions, ): Promise { const [lists, one] = await Promise.all([ fetchMyMachineLists(), opt.key ? readMyMachine(opt.owner, opt.key) : Promise.resolve(null), ]); const row = one?.줄; const seed = opt.seed; let base = seed && seed.출처 === "M01" ? seed.키 : undefined; // 서버 `본뜬` const found = new Map(); const notice = el("div", { className: "m02-mymach__notice" }); const server = el("div", { className: "m02-mymach__server" }); const usesBox = el("div", { className: "m02-mymach__uses" }); const form = createMachineForm({ name: row?.이름 ?? seed?.이름 ?? "", spec: row?.규격 ?? seed?.규격 ?? "", values: row ? valuesOf(row) : seed ? valuesOf({ 방식: seed.방식 ?? "바로", 값: seed.값 ?? {}, 시간당: seed.시간당 }) : {}, coefMode: "direct", allowHourly: true, fuels: lists.fuels, jobs: lists.jobs, power: lists.power, hour: lists.hour, findSimilar: row ? undefined : async (q): Promise => (await findSimilar(opt.owner, q)).map((r) => { found.set(r.키, r); return { key: r.키, name: r.이름, spec: r.규격, tag: r.출처 === "개인" ? mt("Mine") : r.구분, values: valuesOf({ 방식: r.방식 ?? "바로", 값: r.값 ?? {}, 시간당: r.시간당 }), }; }), onSimilar: (item) => { base = item.key; checkSame(); }, onChange: () => { notice.replaceChildren(); // 지난 저장 막힘 글 지움(다시 [저장] 때 새로) opt.onDirty?.(dirty()); checkSame(); }, }); let saved = !opt.key && seed ? false : true; const dirty = (): boolean => form.state().changed || !saved; /* --- 같은 이름 · 규격 알림(M01 · 내 기계) --- */ let timer: number | undefined; let seq = 0; const sameText = (hits: { 키: string; 출처: string }[]): string => hits.length ? mt("Same").replace("{value}", hits.map((h) => `${h.출처} ${h.키}`).join(" · ")) : ""; const checkSame = (): void => { window.clearTimeout(timer); timer = window.setTimeout(async () => { const mine = ++seq; const s = form.state(); const rows = s.name ? await findSimilar(opt.owner, s.name).catch(() => []) : []; if (mine !== seq) return; const hits = rows.filter( (r) => r.키 !== opt.key && r.이름.trim() === s.name && (r.규격 ?? "").trim() === s.spec, ); form.setNotice(sameText(hits)); }, 300); }; /* --- 저장 --- */ const bodyOf = (): MyMachineBody | null => { const s = form.state(); const errors = [...s.errors, ...(s.name ? [] : [mt("Name_Needed")])]; notice.replaceChildren(...errors.map((e) => createText(e, "error"))); if (errors.length) return null; if (s.hourly) return { 이름: s.name, 규격: s.spec, 방식: "직접", 시간당: (s.values[HOURLY_KEY] ?? {}) as MyMachineBody["시간당"], }; const { [HOURLY_KEY]: _drop, ...vals } = s.values; return { 이름: s.name, 규격: s.spec, 방식: s.mode === "pumsem" ? "품셈" : "바로", 값: vals }; }; const saveBtn = createButton({ label: mt("Save"), variant: "filled", size: 28 }); saveBtn.dataset.mymachSave = ""; saveBtn.addEventListener("click", async () => { const body = bodyOf(); if (!body) return; saveBtn.disabled = true; try { const got = opt.key ? await updateMyMachine(opt.owner, opt.key, body) : await createMyMachine(opt.owner, body, base); saved = true; opt.onDirty?.(false); showToast(mt("Saved"), "success"); opt.onSaved(got); } catch (error) { notice.replaceChildren( createText(error instanceof Error ? error.message : mt("Failed"), "error"), ); } finally { saveBtn.disabled = false; } }); /* --- 지우기 --- */ const delBtn = createButton({ label: mt("Delete"), variant: "danger-ghost", size: 28 }); delBtn.hidden = !opt.key; delBtn.addEventListener("click", async () => { if (!opt.key) return; const uses = (await readMyMachine(opt.owner, opt.key).catch(() => one))?.쓰는곳 ?? []; const ask = [ mt("Delete_Ask").replace("{value}", String(uses.length)), ...uses.map((u) => `${u.키} ${u.이름}`), mt("Delete_Keep"), ].join("\n"); if (!(await showConfirmDialog(ask, mt("Delete")))) return; try { await deleteMyMachine(opt.owner, opt.key); saved = true; opt.onDirty?.(false); showToast(mt("Deleted"), "success"); opt.onDeleted?.(); } catch (error) { showToast(error instanceof Error ? error.message : mt("Failed"), "error"); } }); const back = opt.onBack ? [ createButton({ label: mt("Back"), variant: "ghost", size: 28, iconStart: "chevronLeft", onClick: opt.onBack, }), ] : []; const title = row ? `${row.이름} ${row.규격}`.trim() : mt("New"); server.replaceChildren( ...(one ? [createText(`${mt("Server_Hourly")} ${hourlyText(one.시간당)}`)] : []), ); if (one?.같은이름.length) form.setNotice(sameText(one.같은이름)); const uses = one?.쓰는곳 ?? []; usesBox.replaceChildren( el("strong", { text: `${mt("Uses")} ${uses.length}` }), ...(uses.length ? [el("ul", { children: uses.map((u) => el("li", { text: `${u.키} ${u.이름}` })) })] : [createText(mt("No_Uses"), "muted")]), ); const root = el("div", { className: "m02-mymach", attrs: { "data-mymach-form": opt.key ?? "new" }, children: [ el("div", { className: "ui-toolbar m02-mymach__bar", children: [ ...back, el("strong", { text: title }), el("span", { className: "ui-text ui-text--muted", text: opt.key ?? "" }), createBadge(mt("Own_Value"), "info"), saveBtn, delBtn, ], }), createText(mt("Own_Note"), "muted"), notice, server, form.root, ...(opt.key ? [usesBox] : []), ], }); host.replaceChildren(root); if (!row && (opt.openSimilar ?? !seed)) root.querySelector("details")?.setAttribute("open", ""); if (seed) { form.setNotice(mt("Copied").replace("{value}", `${seed.이름} ${seed.규격}`.trim())); opt.onDirty?.(true); } return { root, dirty }; } /** 창으로 띄움 — 저장하면 닫고 `done(답)` · 저장 안 하고 닫으면 물음 */ export function openMyMachineModal( owner: string | null, done: (got: MyMachineOne) => void, seed?: SimilarRow, ): void { let handle: MyMachineFormHandle | null = null; const { close } = openModal({ title: seed ? mt("Copy_Title") : mt("New"), closeLabel: mt("Close"), width: "1100px", wide: true, beforeClose: async () => !handle?.dirty() || showConfirmDialog(mt("Leave_Ask")), mount: (body) => { void mountMyMachineForm(body, { owner, seed, onSaved: (got) => { close(); done(got); }, }) .then((h) => (handle = h)) .catch((error) => { showToast(error instanceof Error ? error.message : mt("Failed"), "error"); close(); }); }, }); }