Files
Aislo/tools/est-test/native-mach-formula.js
T
eomsangdonandClaude Opus 5.5 5e7613eef7 feat(est-test): 기계경비 공식 모음과 불러오기
- 원본처럼 공식 명칭·본문 목록 편집, 단가산출에 본문 줄 넣기
- 옛 EST Plus mach_Text.dat 구공식변환

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 21:15:00 +09:00

165 lines
6.9 KiB
JavaScript

// 원본 기계경비 공식(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(
"기계경비 공식",
`<div style="max-height:380px;overflow:auto">${list
.map(
(f, i) =>
`<div class="form-fields"><label>No ${i + 1} 명칭<input name="mf-name" data-i="${i}" value="${esc(f.name)}"></label><label>공식<textarea name="mf-body" data-i="${i}" rows="4" style="width:100%">${esc(f.body)}</textarea></label><button data-action="mach-formula-insert" data-i="${i}">이 위에 삽입</button> <button data-action="mach-formula-delete" data-i="${i}">삭제</button>${cost ? ` <button data-action="mach-formula-use" data-i="${i}">단가산출에 불러오기</button>` : ""}</div>`,
)
.join(
"<hr>",
)}</div><button data-action="mach-formula-append">추가</button> <button data-action="mach-formula-old">구공식변환(옛 mach_Text.dat)</button> <button data-action="mach-formula-save">저장</button>`,
);
}
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 };
})();