Files
Aislo/tools/est-test/midas-read.cjs
eomsangdonandClaude Opus 5.5 ea50105ce5 feat(est-test): 갑지·표지·공사설명서·시방서 편집과 인쇄
- 원본 문서 레코드(MIDAS) 읽기, 공사명 동기화, 공사금액 자동입력·한글금액
- 설명서·시방서 갑지 붙여넣기, 인쇄 미리보기 반영, 편집 시 ESTX 저장 거부

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

127 lines
4.5 KiB
JavaScript

// 원본 문서 레코드(.opt = MIDAS TClientDataSet 이진 묶음, 머리 96 19 E0 BD) 일반 읽기 — 갑지·표지·공사설명서·시방서.
// 필드형: 1/4 정수 · 3/2 짧은 정수 · 4/8 실수 · 73/1 문자열(cp949) · 75/4 BLOB(바이트 그대로).
// WPTools BLOB 은 wptText 로 글자만 꺼낸다(서식 버림).
const zlib = require("node:zlib");
const decoder = new TextDecoder("euc-kr");
function readRecords(buffer) {
if (!buffer || buffer.length < 24 || buffer.readUInt32LE(0) !== 0xbde01996)
throw Error("문서 레코드 머리 오류");
const count = buffer.readUInt16LE(12),
rows = buffer.readUInt16LE(14),
start = buffer.readUInt16LE(22);
if (count > 1024 || rows > 10000 || start > buffer.length) throw Error("문서 레코드 크기 오류");
let at = 24;
const take = (n) => {
if (n < 0 || at + n > buffer.length) throw Error("문서 레코드 범위 오류");
const out = buffer.subarray(at, at + n);
at += n;
return out;
};
const fields = [];
for (let i = 0; i < count; i++) {
const name = take(take(1)[0]).toString("ascii"),
width = take(2).readUInt16LE(),
kind = take(2).readUInt16LE();
take(2);
const attributes = take(2).readUInt16LE();
for (let j = 0; j < attributes; j++) {
take(take(1)[0]);
const size = take(2).readUInt16LE(),
type = take(2).readUInt16LE();
if (type === 73) take(take(2).readUInt16LE());
else take(size);
}
fields.push({ name, width, kind });
}
at = start;
const out = [];
for (let r = 0; r < rows; r++) {
take(1);
const status = take(Math.ceil(count / 4)),
row = {};
fields.forEach(({ name, width, kind }, i) => {
const state = (status[i >> 2] >> ((i % 4) * 2)) & 3;
if (state === 1 || state === 2) return (row[name] = null);
if (kind === 1 && width === 4) row[name] = take(4).readInt32LE();
else if (kind === 3 && width === 2) row[name] = take(2).readInt16LE();
else if (kind === 4 && width === 8) row[name] = take(8).readDoubleLE();
else if (kind === 73 && width === 1)
row[name] = decoder.decode(take(take(1)[0])).replace(/\0+$/, "");
else if (kind === 75 && width === 4) row[name] = Buffer.from(take(take(4).readUInt32LE()));
else throw Error(`문서 레코드 필드형 오류: ${name}/${kind}/${width}`);
});
out.push(row);
}
return out;
}
function wptText(blob) {
if (!blob?.length) return "";
let bytes = blob;
if (bytes[0] === 0x78) bytes = zlib.inflateSync(bytes);
const source = decoder.decode(bytes);
if (!source.startsWith("<!WPTools")) return source.trim();
return source
.replace(/<(numberstyles|stylesheet)[\s\S]*?<\/\1>/gi, "")
.replace(/<\/(div|p)>|<br\s*\/?>/gi, "\n")
.replace(/<[^>]*>/g, "")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)))
.replace(/&amp;/g, "&")
.replace(/[ \t]+\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
// 갑지·표지·공사설명서·시방서 → project.docs (글자 필드만; 서식·여백은 원본에 그대로 둔다)
function readDocuments(files) {
const docs = {};
const one = (name) => {
const key = [...files.keys()].find((k) => k.toLowerCase() === name.toLowerCase());
return key ? readRecords(files.get(key))[0] || null : null;
};
const gabgi = one("DatGabgi.opt");
if (gabgi)
docs.gabgi = {
Name: gabgi.Name ?? "",
Position: gabgi.Position ?? "",
CompanyName: gabgi.CompanyName ?? "",
Explan: wptText(gabgi.EXPLEAN),
Cost: wptText(gabgi.Cost),
Gulga: gabgi.Gulga
? decoder
.decode(gabgi.Gulga)
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
: [],
Date: gabgi.Date ?? "",
YearShow: Boolean(gabgi.YearShow),
DayShow: Boolean(gabgi.DayShow),
CompanyPos: gabgi.CompanyPos ?? 1,
};
const cover = one("DatCover.opt");
if (cover)
docs.cover = {
Name: cover.Name ?? "",
Name2: cover.Name2 ?? "",
CompanyName: cover.CompanyName ?? "",
Date: cover.Date ?? "",
YearShow: Boolean(cover.YearShow),
CompanyPos: cover.CompanyPos ?? 1,
};
for (const [key, file] of [
["explan", "DatExplean.opt"],
["sibang", "DatSiBang.opt"],
]) {
const row = one(file);
if (row) docs[key] = { text: wptText(row.Editor) };
}
return docs;
}
module.exports = { readRecords, wptText, readDocuments };