// 원본 원가계산서 양식(TFRMWONGABASE popupSaveAs·popupFormats·popupLoadFrom)과 양식목록(TFRMFORMATFILELIST) // — 현재 원가 표를 이름 붙여 서버 보관함(data/forms.json)에 등록하고, 목록에서 열기·삭제, 파일로 내려받기·불러오기. const EstFormsLibrary = (() => { // 원가 표 양식으로 남길 필드 — 금액(Cost)은 계산 결과라 남기지 않는다. function strip(rows) { return rows.map((row) => { const { Cost, ...rest } = row; return rest; }); } function validRows(rows) { if (!Array.isArray(rows) || !rows.length) throw Error("원가계산서 양식에 행이 없습니다."); if (rows.some((row) => !row || typeof row !== "object" || Array.isArray(row))) throw Error("원가계산서 양식 행이 올바르지 않습니다."); return rows; } async function list() { return (await EstApp.api("/api/forms")).filter((f) => f.kind === "wonga"); } async function show(state) { const { esc } = EstApp, forms = await list(); EstApp.showDialog( "원가계산서 양식 목록", `

현재 원가계산서의 항목·수식·요율을 양식으로 등록하고, 다른 공사에서 열어 씁니다. 금액은 열 때 다시 계산합니다.

${forms.map((f) => ``).join("") || ''}
양식 이름행등록작업
${esc(f.name)}${f.data.length}${esc(f.saved.slice(0, 10))}
등록된 양식이 없습니다.
`, ); state.formsLibrary = forms; } function apply(state, rows, name) { validRows(rows); if (!confirm(`현재 원가계산서를 「${name}」 양식으로 바꾸시겠습니까?`)) return; EstApp.byId("dialog").close(); // 도급액 고정은 저장된 금액을 그대로 쓰므로 금액 없는 양식에서는 풀어야 다시 계산된다(기초 뼈대와 같음). const fixed = state.project.options.TotalFix; EstApp.change((project) => { project.options.TotalFix = false; project.tables.Wonga = structuredClone(rows); }); state.notice = `원가계산서를 「${name}」 양식으로 바꿨습니다.${fixed ? " 도급액 고정은 풀었으니 필요하면 다시 맞추세요." : ""}`; } async function run(name, state, node) { if (name === "wonga-forms") await show(state); else if (name === "wonga-form-register") { const title = prompt("양식 이름", state.project.name || "원가계산서"); if (!title) return; const data = strip(validRows(state.project.tables.Wonga)); await EstApp.api("/api/forms", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: "wonga", name: title, data }), }); await show(state); } else if (name === "wonga-form-open") { const form = state.formsLibrary?.find((f) => f.name === node.dataset.name); if (!form) throw Error("양식을 찾을 수 없습니다. 목록을 다시 여세요."); apply(state, form.data, form.name); } else if (name === "wonga-form-delete") { if (!confirm(`「${node.dataset.name}」 양식을 지우시겠습니까?`)) return; await EstApp.api("/api/forms/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: "wonga", name: node.dataset.name }), }); await show(state); } else if (name === "wonga-form-export") { const data = strip(validRows(state.project.tables.Wonga)); EstApp.download( `${state.project.name || "원가계산서"}.wonga.json`, JSON.stringify({ kind: "est-web-wonga-1", rows: data }, null, 2), ); } else if (name === "wonga-form-import") { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.addEventListener("change", async () => { try { const file = input.files[0]; if (!file) return; const parsed = JSON.parse(await file.text()); if (parsed?.kind !== "est-web-wonga-1") throw Error("원가계산서 양식 파일이 아닙니다."); apply(state, parsed.rows, file.name); } catch (error) { state.notice = error.message; } EstApp.render(); }); input.click(); } } const names = new Set([ "wonga-forms", "wonga-form-register", "wonga-form-open", "wonga-form-delete", "wonga-form-export", "wonga-form-import", ]); // 동작 사슬은 동기 — 이름만 먼저 가리고 서버 요청은 뒤에서 돌린다. function handle(name, state, node) { if (!names.has(name)) return false; run(name, state, node).catch((error) => { state.notice = error.message; EstApp.render(); }); return true; } return { strip, validRows, handle }; })();