// 원본 기계경비 공식(TfrmMachGongSic 0xf146f0, 전역 파일 System\MainSys\MachText.Dat: Index·Name·BigoMemo)과 // 기계경비공식불러오기(TfrmMachText 0xd3d58c: 고른 공식 본문을 줄마다 단가산출 LineData 행으로 현재 행 뒤에 넣음, 계산하지 않음). // 웹은 공식 모음을 서버 양식 보관함(종류 machformula)에 둔다. 번호는 늘 1..N. // 구공식변환(숨김 단추 0xda5c18 → 0xd90c60): 옛 EST Plus mach_Text.dat 394바이트 레코드 — +5 정수 > 0 이면 새 공식 머리 // (이름 = +0x16 짧은 문자열 + +0x49 짧은 문자열), 아니면 본문 줄(앞글+뒤글+CR). // 원본이 기계경비 연결 때 덧붙이는 요약 4줄(노무비·재료비·경비·소계)은 형식 확인 전이라 넣지 않는다. const EstMachFormula = (() => { function parseOld(bytes) { const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); if (!view.length || view.length % 394) throw Error("구공식 파일 크기가 394바이트 단위가 아닙니다."); const decoder = new TextDecoder("euc-kr"), data = new DataView(view.buffer, view.byteOffset, view.byteLength); const short = (at, max) => decoder.decode(view.subarray(at + 1, at + 1 + Math.min(view[at], max))); const out = []; let current = null; for (let at = 0; at < view.length; at += 394) { const head = data.getInt32(at + 5, true), text = short(at + 0x16, 50) + short(at + 0x49, 255); if (head > 0) { current = { name: text.slice(0, 100), body: "" }; out.push(current); } else if (current) current.body += `${text}\r`; } return out.map((f, i) => ({ ...f, index: i + 1 })); } function lines(body) { return String(body ?? "") .split(/\r\n|\r|\n/) .filter((line, i, all) => line !== "" || i < all.length - 1); } function insertInto(project, at, body) { const rows = project.tables.CostMainDat; if (!rows?.[at]) throw Error("단가산출에서 넣을 위치(행)를 고르세요."); const add = lines(body); if (!add.length) throw Error("공식 본문이 비어 있습니다."); let pk = Math.max(0, ...rows.map((r) => Number(r.PkID) || 0)); rows.splice(at + 1, 0, ...add.map((LineData) => ({ PkID: ++pk, Index: 0, LineData }))); return add.length; } // 화면 async function load() { return (await EstApp.api("/api/forms")) .filter((f) => f.kind === "machformula") .sort((a, b) => (a.data.index ?? 0) - (b.data.index ?? 0)); } async function save(list) { const old = await load(); for (const f of old) if (!list.some((n) => n.name === f.name)) await EstApp.api("/api/forms/delete", post({ kind: "machformula", name: f.name })); for (const [i, f] of list.entries()) await EstApp.api( "/api/forms", post({ kind: "machformula", name: f.name, data: { index: i + 1, body: f.body } }), ); } const post = (body) => ({ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); async function editor(state) { const list = (await load()).map((f) => ({ name: f.name, body: f.data.body })); state.machFormulas = list; draw(state); } function draw(state) { const { esc } = EstApp, list = state.machFormulas, cost = state.view === "CostMainDat"; EstApp.showDialog( "기계경비 공식", `
${list .map( (f, i) => `
${cost ? ` ` : ""}
`, ) .join( "
", )}
`, ); } function read(state) { for (const node of document.querySelectorAll('#dialog-content [name="mf-name"]')) state.machFormulas[Number(node.dataset.i)].name = node.value.trim(); for (const node of document.querySelectorAll('#dialog-content [name="mf-body"]')) state.machFormulas[Number(node.dataset.i)].body = node.value; } function unique(list) { const seen = new Set(); for (const f of list) { if (!f.name) throw Error("공식 명칭을 입력하세요."); if (seen.has(f.name)) throw Error(`같은 명칭이 있습니다: ${f.name}`); seen.add(f.name); } } async function run(name, state, node) { const i = Number(node?.dataset.i); if (name === "mach-formula") return editor(state); read(state); const list = state.machFormulas; if (name === "mach-formula-append") list.push({ name: `새 공식 ${list.length + 1}`, body: "" }); else if (name === "mach-formula-insert") list.splice(i, 0, { name: `새 공식 ${list.length + 1}`, body: "" }); else if (name === "mach-formula-delete") list.splice(i, 1); else if (name === "mach-formula-save") { unique(list); await save(list); state.notice = `기계경비 공식 ${list.length}개를 저장했습니다.`; } else if (name === "mach-formula-use") { const at = [...state.selection].sort((a, b) => a - b).at(-1); let count; EstApp.change((project) => (count = insertInto(project, at, list[i].body))); EstApp.byId("dialog").close(); state.notice = `「${list[i].name}」 공식 ${count}줄을 넣었습니다.`; return; } else if (name === "mach-formula-old") { const picker = document.createElement("input"); picker.type = "file"; picker.accept = ".dat"; picker.addEventListener("change", async () => { try { const file = picker.files[0]; if (!file) return; for (const f of parseOld(new Uint8Array(await file.arrayBuffer()))) list.push({ name: f.name, body: f.body }); draw(state); } catch (error) { state.notice = error.message; EstApp.render(); } }); picker.click(); return; } draw(state); } const names = new Set([ "mach-formula", "mach-formula-append", "mach-formula-insert", "mach-formula-delete", "mach-formula-save", "mach-formula-use", "mach-formula-old", ]); 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 { parseOld, lines, insertInto, handle }; })();