- 원본 문서 레코드(MIDAS) 읽기, 공사명 동기화, 공사금액 자동입력·한글금액 - 설명서·시방서 갑지 붙여넣기, 인쇄 미리보기 반영, 편집 시 ESTX 저장 거부 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
4.5 KiB
JavaScript
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(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, '"')
|
|
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)))
|
|
.replace(/&/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 };
|