- cp949 에 없는 글자는 「?」 로 바꿔 씀(22MB 공사 저장 중 멈춤) - 기계경비 본문 Index(−3·−2·−1) 유지 - % 단위 수량은 소수로 써서 음수·큰 값도 되돌아옴 - 경쟁사 예제 estx 2건 저장값 대조·구버전 왕복 시험 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
602 lines
19 KiB
JavaScript
602 lines
19 KiB
JavaScript
// 구버전 EST Plus 자료(.est/.ebk = ZIP, 표마다 Delphi 고정 레코드 파일 — tmp 역분석 spec_estplus.md) 읽기·쓰기.
|
||
// 레코드 394 바이트(기본) · 565 바이트(.ini [공통] data_version = 3). 문자 = ShortString(cp949), 금액 = Extended(10바이트).
|
||
// 읽기(mnuFileEstPlusLoad → 0x112ee48 · 표별 변환 0xd91504): .dtm 내역 · .hdt 일위 · .cct 단산 · .mdt 기계 · .ldt 노임 · .sdt 자재 ·
|
||
// .sil 표준 · .sti 견적(565 만) · .wdt/.swdt 원가 · c* = 설계변경(「_」 칸). 원본처럼 대부분 행의 코드를 비우므로, 웹은 호표 머리에
|
||
// 코드를 매기고 본문 연결 행은 같은 품명·규격의 원천 코드를 찾아 붙인다(웹 보충). 메모(.MEM)·.mot(기계 기준)는 읽지 않는다.
|
||
// 쓰기(ActionConvertToEstPlus → 0xd96354/0xd960b8): 394 판으로 위 표 파일 + .ini 를 ZIP 으로.
|
||
// 단가비교(TfrmCompare 0xf8998c): 고른 종류 파일의 Index > 0 레코드 노무·재료·경비 금액을 차례로.
|
||
const { pack, unpack } = require("./archive.cjs");
|
||
|
||
const num = (v) => Number(v) || 0;
|
||
const {
|
||
HEAD,
|
||
AMOUNTS,
|
||
BODY,
|
||
KIND,
|
||
LINK_IN,
|
||
LINK_OUT,
|
||
readX,
|
||
writeX,
|
||
decoder,
|
||
writeS,
|
||
records,
|
||
decode,
|
||
cp949,
|
||
} = require("./legacy-estplus-layout.cjs");
|
||
|
||
const link = (v) => LINK_IN[v] ?? v;
|
||
const percent = (r, li, skip) =>
|
||
["%", "%"].includes(r.Unite) && !skip.includes(li) && r.GN < 1 ? r.GN * 100 : r.GN;
|
||
const clean = (t) =>
|
||
String(t ?? "")
|
||
.replace(/\n/g, "")
|
||
.trim();
|
||
const pad = (p, n) => p + String(n).padStart(10, "0");
|
||
// 연결 코드 줄이기(0xd86924): A 로 시작하면 끝의 0 을 1자 남을 때까지
|
||
const trimA = (code) => (/^A/.test(code) ? code.replace(/0+$/, "") || "A" : code);
|
||
|
||
function convert(files, name) {
|
||
const warnings = new Set();
|
||
const ini = [...files].find(([k]) => k.endsWith(".ini"))?.[1];
|
||
const iniText = ini ? decoder.decode(ini) : "";
|
||
const iniValue = (key) =>
|
||
new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`, "mi").exec(iniText)?.[1]?.trim();
|
||
const size = Number(iniValue("data_version")) === 3 ? 565 : 394;
|
||
const file = (ext) => [...files].find(([k]) => k.endsWith("." + ext))?.[1];
|
||
const parse = (ext, type = ext) => {
|
||
const buf = file(ext);
|
||
if (!buf) return [];
|
||
if (buf.length % size) warnings.add(`${ext}: 레코드 크기(${size})로 나누어지지 않습니다.`);
|
||
return records(buf, size).map((r) => decode(r, size, type));
|
||
};
|
||
const t = {};
|
||
const projectName = iniValue("project_title_name") || name;
|
||
// 내역서
|
||
const est = (rows) =>
|
||
rows.map((r) => {
|
||
let li = link(r.link),
|
||
calc = 0;
|
||
if (r.link === 64) ((li = 0), (calc = 1));
|
||
const row = {
|
||
Index: li === 57 && r.check ? r.check : r.Index,
|
||
LinkIndex: li,
|
||
Code: r.link === 21 ? r.code : "",
|
||
Name: r.link === 56 ? projectName : clean(r.Name),
|
||
Size: clean(r.Size),
|
||
Unite: clean(r.Unite),
|
||
Bigo: r.Bigo,
|
||
Level: r.Level,
|
||
Pr: r.Pr,
|
||
...Object.fromEntries(AMOUNTS.map((f) => [f, r[f]])),
|
||
SKind: r.SKind,
|
||
OCheck: r.OCheck,
|
||
SCheck: r.check > 0 ? 3 : r.SCheck,
|
||
Gangub: r.Gangub,
|
||
Biul: r.Biul,
|
||
CalcIndex: calc,
|
||
NewItem: r.NewItem,
|
||
PageSkip: r.PageSkip,
|
||
MullangIndex: r.MullangIndex,
|
||
ExcelSheetIndex: r.ExcelSheetIndex,
|
||
};
|
||
row.GN = percent(row, li, [51, 52, 57, 58, 62, 64]);
|
||
if ([51, 62].includes(li) && row.Code) row.Code = trimA(row.Code);
|
||
return row;
|
||
});
|
||
t.EstMainDat = est(parse("dtm"));
|
||
// 일위대가
|
||
const heads = parse("hdt");
|
||
t.HeadMainDat = heads.map((r) => {
|
||
const li = r.link === 64 ? 0 : link(r.link);
|
||
const row = {
|
||
Index: r.Index,
|
||
LinkIndex: li,
|
||
Code: r.Index > 0 ? pad("U", r.Index) : [21, 62].includes(r.link) ? r.code : "",
|
||
Name: clean(r.Name),
|
||
Size: clean(r.Size),
|
||
Unite: clean(r.Unite),
|
||
Bigo: r.Bigo,
|
||
Level: r.Level,
|
||
...Object.fromEntries(AMOUNTS.map((f) => [f, r[f]])),
|
||
SKind: r.SKind,
|
||
OCheck: r.OCheck,
|
||
SCheck: r.check > 0 ? 3 : r.SCheck,
|
||
Gangub: r.Gangub,
|
||
Biul: r.Biul,
|
||
CalcIndex: r.link === 64 ? 1 : 0,
|
||
MullangIndex: r.MullangIndex,
|
||
ExcelSheetIndex: r.ExcelSheetIndex,
|
||
};
|
||
row.GN = percent(row, li, [51, 52, 57, 58, 62, 64]);
|
||
if ([51, 52, 58, 62].includes(li) && row.Code && r.Index <= 0) row.Code = trimA(row.Code);
|
||
return row;
|
||
});
|
||
t.HeadTitleDat = heads
|
||
.filter((r) => r.Index > 0)
|
||
.map((r) => ({
|
||
Index: r.Index,
|
||
Code: pad("U", r.Index),
|
||
Name: clean(r.Name),
|
||
Size: clean(r.Size),
|
||
Unite: clean(r.Unite),
|
||
TC: r.TC,
|
||
NC: r.NC,
|
||
GC: r.GC,
|
||
JC: r.JC,
|
||
OTC: r.TC,
|
||
ONC: r.NC,
|
||
OGC: r.GC,
|
||
OJC: r.JC,
|
||
Bigo: r.Bigo,
|
||
NewItem: r.NewItem,
|
||
UserInput: r.link === 64,
|
||
}));
|
||
// 단가산출: 한 줄 = Name+Size+Bigo+Unite 이어 붙임 · 머리는 꼬리표(#14 품명 · #19 규격 · #21 단위)로 나눔
|
||
const costs = parse("cct");
|
||
const tag = (line, c) => {
|
||
const at = line.indexOf(c);
|
||
return at < 0
|
||
? ""
|
||
: line
|
||
.slice(at + 1)
|
||
.split(/[\x00-\x1f]/)[0]
|
||
.trim();
|
||
};
|
||
t.CostMainDat = costs.map((r) => {
|
||
const line = r.Name + r.Size + r.Bigo + r.Unite;
|
||
let index = r.Index === -4 ? -5 : r.Index,
|
||
ocheck = r.OCheck,
|
||
scheck = r.SCheck;
|
||
if (index < 0 && index >= -5) {
|
||
scheck = index;
|
||
ocheck = index;
|
||
index = 0;
|
||
}
|
||
return {
|
||
Index: index,
|
||
LinkIndex: r.link === 64 ? 0 : link(r.link),
|
||
Code: r.Index > 0 ? pad("D", r.Index) : r.link === 21 ? r.code : "",
|
||
...(r.Index > 0
|
||
? {
|
||
Name: tag(line, "\x0e") || clean(r.Name),
|
||
Size: tag(line, "\x13"),
|
||
Unite: tag(line, "\x15"),
|
||
}
|
||
: { LineData: line }),
|
||
Level: r.Level,
|
||
Diget: r.Diget,
|
||
DigetIndex: r.DigetIndex,
|
||
TC: r.TC,
|
||
NC: r.NC,
|
||
GC: r.GC,
|
||
JC: r.JC,
|
||
SKind: r.SKind,
|
||
OCheck: ocheck,
|
||
SCheck: scheck,
|
||
Biul: r.Biul,
|
||
CalcIndex: r.link === 64 ? 1 : 0,
|
||
};
|
||
});
|
||
t.CostTitleDat = t.CostMainDat.filter((r) => r.Index > 0).map((r) => ({
|
||
Index: r.Index,
|
||
Code: r.Code,
|
||
Name: r.Name,
|
||
Size: r.Size,
|
||
Unite: r.Unite,
|
||
TC: r.TC,
|
||
NC: r.NC,
|
||
GC: r.GC,
|
||
JC: r.JC,
|
||
OTC: r.TC,
|
||
ONC: r.NC,
|
||
OGC: r.GC,
|
||
OJC: r.JC,
|
||
}));
|
||
// 기계경비: 머리 X220 TC · 210 NC · 200 GC · 190 JC, 본문 −3 손료 · −2 연료 · −1 노무
|
||
t.MachMainDat = [];
|
||
t.MachTitleDat = [];
|
||
for (const r of parse("mdt")) {
|
||
if (!r.code && !clean(r.Name) && !r.X220) continue;
|
||
const base = {
|
||
LinkIndex: link(r.link),
|
||
Code: "",
|
||
Name: clean(r.Name),
|
||
CalcIndex: r.link === 64 ? 1 : 0,
|
||
};
|
||
if (r.Index > 0) {
|
||
if (t.MachTitleDat.length) t.MachMainDat.push({ Name: " " });
|
||
const head = {
|
||
...base,
|
||
Index: r.Index,
|
||
Code: pad("E", r.Index),
|
||
Size: clean(r.Size),
|
||
Unite: "hr",
|
||
TC: r.X220,
|
||
NC: r.X210,
|
||
GC: r.X200,
|
||
JC: r.X190,
|
||
};
|
||
t.MachMainDat.push({ ...head, Selected: 0 });
|
||
t.MachTitleDat.push({ ...head, UserInput: r.link === 64 });
|
||
} else if (r.Index === -3)
|
||
t.MachMainDat.push({
|
||
...base,
|
||
Index: r.Index,
|
||
Selected: 1,
|
||
Size: "",
|
||
GN: r.X200,
|
||
Ratio: r.X210,
|
||
TU: r.X190,
|
||
JC: r.X220,
|
||
Unite: clean(r.Size).replace(/\\/g, "₩"),
|
||
});
|
||
else if (r.Index === -2)
|
||
t.MachMainDat.push({
|
||
...base,
|
||
Index: r.Index,
|
||
Selected: 6,
|
||
Size: clean(r.Size),
|
||
GN: r.X190,
|
||
TU: r.X200,
|
||
GC: r.X220,
|
||
Unite: /^(경|휘|중)/.test(clean(r.Unite))
|
||
? "ℓ"
|
||
: /잡/.test(base.Name)
|
||
? "%"
|
||
: clean(r.Unite),
|
||
});
|
||
else if (r.Index === -1)
|
||
t.MachMainDat.push({
|
||
...base,
|
||
Index: r.Index,
|
||
Selected: 11,
|
||
Size: clean(r.Size),
|
||
GN: r.X190,
|
||
TU: r.X200,
|
||
NC: r.X220,
|
||
NCGasu: Number(clean(r.Size).replace(/[^\d.]/g, "")) || 1,
|
||
Unite: clean(r.Unite) || "인",
|
||
});
|
||
}
|
||
// 본문 행 TC := NC + GC + JC
|
||
for (const r of t.MachMainDat)
|
||
if ([1, 6, 11].includes(r.Selected)) {
|
||
for (const f of ["NC", "GC", "JC"]) r[f] = num(r[f]);
|
||
r.TC = r.NC + r.GC + r.JC;
|
||
}
|
||
// 기초단가
|
||
const skip = (r) => !r.code && !clean(r.Name);
|
||
t.LaborcDat = parse("ldt")
|
||
.filter((r) => !skip(r))
|
||
.map((r, i) => ({
|
||
Index: i + 1,
|
||
Code: pad("L", i + 1),
|
||
Name: clean(r.Name),
|
||
Size: clean(r.Size),
|
||
Unite: "인",
|
||
Cost1: r.TC,
|
||
TC: r.TC,
|
||
OTC: r.TC,
|
||
ValueIndex: 0,
|
||
Gangub: 0,
|
||
Bigo: r.Bigo,
|
||
}));
|
||
t.StuffcDat = parse("sdt")
|
||
.filter((r) => !skip(r))
|
||
.map((r, i) => ({
|
||
Index: i + 1,
|
||
Code: pad("M", i + 1),
|
||
Name: clean(r.Name),
|
||
Size: clean(r.Size),
|
||
Unite: clean(r.Unite),
|
||
...Object.fromEntries(
|
||
[1, 2, 3, 4, 5, 6].flatMap((n) => [
|
||
[`Cost${n}`, r[`Cost${n}`]],
|
||
[`Page${n}`, r[`Page${n}`]],
|
||
]),
|
||
),
|
||
TC: r.TC,
|
||
OTC: r.TC,
|
||
Bigo: r.Bigo,
|
||
}));
|
||
t.SilJukDat = parse("sil")
|
||
.filter((r) => !skip(r))
|
||
.map((r, i) => {
|
||
const only = !r.TC;
|
||
const v = only
|
||
? { TC: r.JC, NC: 0, GC: 0, JC: r.JC }
|
||
: { TC: r.TC, NC: r.NC, GC: r.GC, JC: r.JC };
|
||
return {
|
||
Index: i + 1,
|
||
Code: r.code,
|
||
Name: clean(r.Name),
|
||
Size: clean(r.Size),
|
||
Unite: clean(r.Unite),
|
||
Cost1: v.TC,
|
||
...v,
|
||
OTC: v.TC,
|
||
ONC: v.NC,
|
||
OGC: v.GC,
|
||
OJC: v.JC,
|
||
NCRatio: r.NCRatio,
|
||
};
|
||
});
|
||
t.EstiMateDat =
|
||
size === 565
|
||
? parse("sti")
|
||
.filter((r) => !skip(r))
|
||
.map((r, i) => ({
|
||
Index: i + 1,
|
||
Code: r.code,
|
||
Name: clean(r.Name),
|
||
Size: clean(r.Size),
|
||
Unite: clean(r.Unite),
|
||
GN: r.GN,
|
||
TC: r.TC,
|
||
NC: r.NC,
|
||
GC: r.GC,
|
||
JC: r.JC,
|
||
OTC: r.TC,
|
||
ONC: r.NC,
|
||
OGC: r.GC,
|
||
OJC: r.JC,
|
||
Ratio: r.Ratio,
|
||
NachalRatio: r.NachalRatio,
|
||
Diget: r.Diget,
|
||
DigetIndex: r.DigetIndex,
|
||
}))
|
||
: [];
|
||
const wonga = file("wdt") ? parse("wdt") : parse("swdt", "wdt");
|
||
t.Wonga = wonga.map((r) => ({
|
||
Index: r.Index,
|
||
LinkIndex: r.link,
|
||
Name: clean(r.Name),
|
||
VSusic: clean(r.Size),
|
||
VGungu: clean(r.Size),
|
||
Biul: clean(r.Unite),
|
||
Youl: clean(r.Unite),
|
||
Checked: r.Checked,
|
||
Cost: r.Cost,
|
||
Diget: Math.round(r.Diget),
|
||
EndLineIndex: r.EndLineIndex,
|
||
Bimog1: r.Bimog1,
|
||
Bimog2: r.Bimog2,
|
||
Gubun: r.Gubun,
|
||
Pr: "",
|
||
GabgiInput: false,
|
||
CalcIndex: r.link === 64 ? 1 : 0,
|
||
}));
|
||
// 설계변경(c*) → 같은 표의 「_」 칸, 차례대로
|
||
const change = {
|
||
cdtm: ["EstMainDat", "dtm"],
|
||
chdt: ["HeadMainDat", "hdt"],
|
||
ccct: ["CostMainDat", "cct"],
|
||
};
|
||
let changed = false;
|
||
for (const [ext, [table, type]] of Object.entries(change)) {
|
||
const rows = parse(ext, type);
|
||
if (!rows.length) continue;
|
||
changed = true;
|
||
rows.forEach((r, i) => {
|
||
const target = (t[table][i] ||= {});
|
||
target.Index_ = r.Index;
|
||
for (const f of ["Name", "Size", "Unite"]) target[f + "_"] = clean(r[f]);
|
||
for (const f of AMOUNTS) if (r[f] != null) target[f + "_"] = r[f];
|
||
});
|
||
}
|
||
// 본문 연결 행: 같은 품명·규격의 원천 코드(웹 보충)
|
||
const source = {
|
||
5: t.StuffcDat,
|
||
6: t.LaborcDat,
|
||
21: t.SilJukDat,
|
||
101: t.EstiMateDat,
|
||
103: t.HeadTitleDat,
|
||
104: t.CostTitleDat,
|
||
105: t.MachTitleDat,
|
||
};
|
||
const key = (r) => `${clean(r.Name).replace(/\s/g, "")}|${clean(r.Size).replace(/\s/g, "")}`;
|
||
const maps = Object.fromEntries(
|
||
Object.entries(source).map(([k, rows]) => [k, new Map(rows.map((r) => [key(r), r.Code]))]),
|
||
);
|
||
let linked = 0,
|
||
missing = 0;
|
||
for (const rows of [t.EstMainDat, t.HeadMainDat])
|
||
for (const r of rows)
|
||
if (!(num(r.Index) > 0) && maps[r.LinkIndex] && !r.Code) {
|
||
const code = maps[r.LinkIndex].get(key(r));
|
||
if (code) ((r.Code = code), linked++);
|
||
else missing++;
|
||
}
|
||
if (missing)
|
||
warnings.add(
|
||
`연결 원천을 품명·규격으로 찾지 못한 행 ${missing}개(코드 없음 — 저장 금액 유지).`,
|
||
);
|
||
if (files.has(".mem") || [...files.keys()].some((k) => k.endsWith(".mem")))
|
||
warnings.add("근거 메모(.MEM)는 읽지 않았습니다.");
|
||
if (file("mot")) warnings.add("기계경비 기준표(.mot)는 읽지 않았습니다.");
|
||
const mode = Number(iniValue("last_work_mode")) || 1;
|
||
const standard = /^(1|true)$/i.test(iniValue("siljuk_est_mode") || "");
|
||
for (const k of ["MachGijunDat", "CadMainDat", "CadTitleDat"]) t[k] ||= [];
|
||
return {
|
||
schema: "est-web-2",
|
||
name: projectName,
|
||
tables: t,
|
||
options: {
|
||
SourceRefresh: false,
|
||
AutoCalc: false,
|
||
TotalFix: false,
|
||
VerFormat: true,
|
||
WorkMode: standard ? (mode === 2 ? 4 : 3) : mode,
|
||
...(changed && { Mode: "설계변경" }),
|
||
},
|
||
quantities: [],
|
||
quantitySheets: [],
|
||
progress: [],
|
||
annual: [],
|
||
schedule: [],
|
||
drawings: [],
|
||
importWarnings: [
|
||
`구버전 EST Plus 자료(레코드 ${size}바이트)를 변환했습니다. 저장된 금액을 그대로 두려고 자동계산을 껐습니다. 품명·규격으로 연결 ${linked}개.`,
|
||
...warnings,
|
||
],
|
||
};
|
||
}
|
||
const lower = (files) => new Map([...files].map(([k, v]) => [k.toLowerCase(), v]));
|
||
function read(buffer, name = "EST Plus 자료") {
|
||
return convert(lower(unpack(buffer)), name);
|
||
}
|
||
|
||
// 쓰기(394 판)
|
||
function record(type, row) {
|
||
const rec = Buffer.alloc(394),
|
||
h = HEAD[394];
|
||
rec.writeInt32LE(-1, 0);
|
||
rec.writeInt32LE(Math.trunc(num(row.Index)), 5);
|
||
rec[9] = row.UserInput ? 64 : (LINK_OUT[num(row.LinkIndex)] ?? num(row.LinkIndex)) & 0xff;
|
||
for (const [k, f] of [
|
||
["code", "Code"],
|
||
["name", "Name"],
|
||
["size", "Size"],
|
||
["unite", "Unite"],
|
||
["bigo", "Bigo"],
|
||
])
|
||
writeS(rec, h[k][0], h[k][1], row[f]);
|
||
rec.writeInt32LE(-1, h.memo1);
|
||
rec.writeInt32LE(Math.trunc(num(row.MullangIndex)), h.mul);
|
||
rec.writeInt32LE(-1, h.memo2);
|
||
rec[h.page] = num(row.PageSkip) & 0xff;
|
||
rec[171] = 1;
|
||
rec[h.checked] = row.Checked ? 1 : 0;
|
||
rec[h.newItem] = row.NewItem ? 1 : 0;
|
||
rec[h.sheet] = num(row.ExcelSheetIndex) & 0xff;
|
||
for (const at of [177, 181, 185]) rec.writeInt32LE(-1, at);
|
||
rec[h.kind] = KIND[type];
|
||
for (const [f, t, at, , n] of BODY[type === "swdt" ? "wdt" : type] || []) {
|
||
if (!at) continue;
|
||
const v = row[f];
|
||
if (t === "X") writeX(rec, at, v);
|
||
else if (t === "L") rec.writeInt32LE(Math.trunc(num(v)), at);
|
||
else if (t === "b") rec.writeInt8(Math.max(-128, Math.min(127, Math.trunc(num(v)))), at);
|
||
else if (t === "B") rec[at] = num(v) & 0xff;
|
||
else writeS(rec, at, n, v);
|
||
}
|
||
return rec;
|
||
}
|
||
// 단가산출 글줄을 Name(50)·Size(50)·Bigo(20)·Unite(8) 칸에 차례로 나눠 담음(읽을 때 이어 붙이면 같은 줄)
|
||
function splitLine(text) {
|
||
const parts = ["", "", "", ""],
|
||
caps = [50, 50, 20, 8];
|
||
let k = 0,
|
||
used = 0;
|
||
for (const ch of String(text ?? "")) {
|
||
const n = cp949(ch).length;
|
||
while (k < 4 && used + n > caps[k]) ((k += 1), (used = 0));
|
||
if (k >= 4) break;
|
||
parts[k] += ch;
|
||
used += n;
|
||
}
|
||
return { Name: parts[0], Size: parts[1], Bigo: parts[2], Unite: parts[3] };
|
||
}
|
||
function write(project) {
|
||
const t = project.tables,
|
||
out = [];
|
||
const add = (ext, type, rows) =>
|
||
rows.length && out.push([`data.${ext}`, Buffer.concat(rows.map((r) => record(type, r)))]);
|
||
// % 단위 수량은 소수로 씀(읽을 때 1 미만이면 ×100 — 음수·100 넘는 값도 되돌아오게)
|
||
const pct = (rows) =>
|
||
rows.map((r) =>
|
||
["%", "%"].includes(String(r.Unite ?? "").trim()) &&
|
||
![51, 52, 57, 58, 62, 64].includes(num(r.LinkIndex)) &&
|
||
!(num(r.Index) > 0)
|
||
? { ...r, GN: num(r.GN) / 100 }
|
||
: r,
|
||
);
|
||
add("dtm", "dtm", pct(t.EstMainDat || []));
|
||
add("hdt", "hdt", pct(t.HeadMainDat || []));
|
||
add(
|
||
"cct",
|
||
"cct",
|
||
(t.CostMainDat || []).map((r) => ({
|
||
...r,
|
||
...splitLine(
|
||
num(r.Index) > 0 && !r.LineData
|
||
? `\x0e${r.Name ?? ""}\x0e\x13${r.Size ?? ""}\x13\x15${r.Unite ?? ""}\x15`
|
||
: r.LineData,
|
||
),
|
||
Index:
|
||
num(r.Index) > 0
|
||
? r.Index
|
||
: num(r.SCheck) < 0
|
||
? num(r.SCheck) === -5
|
||
? -4
|
||
: r.SCheck
|
||
: r.Index,
|
||
})),
|
||
);
|
||
add(
|
||
"mdt",
|
||
"mdt",
|
||
(t.MachMainDat || [])
|
||
.filter((r) => String(r.Name ?? "").trim() || num(r.Index) > 0)
|
||
.map((r) => {
|
||
const s = num(r.Selected);
|
||
if (num(r.Index) > 0) return { ...r, X220: r.TC, X210: r.NC, X200: r.GC, X190: r.JC };
|
||
if (s === 1)
|
||
return {
|
||
...r,
|
||
Index: -3,
|
||
Size: r.Unite,
|
||
X200: r.GN,
|
||
X210: r.Ratio,
|
||
X190: r.TU,
|
||
X220: r.JC,
|
||
};
|
||
if (s === 6) return { ...r, Index: -2, X190: r.GN, X200: r.TU, X220: r.GC };
|
||
return { ...r, Index: -1, X190: r.GN, X200: r.TU, X220: r.NC };
|
||
}),
|
||
);
|
||
add("ldt", "ldt", t.LaborcDat || []);
|
||
add("sdt", "sdt", t.StuffcDat || []);
|
||
add("sil", "sil", t.SilJukDat || []);
|
||
add(
|
||
"wdt",
|
||
"wdt",
|
||
(t.Wonga || []).map((r) => ({
|
||
...r,
|
||
Size: r.VSusic,
|
||
Unite: r.Youl ?? r.Biul,
|
||
Bigo: String(Math.trunc(num(r.Cost))),
|
||
})),
|
||
);
|
||
const mode = num(project.options?.WorkMode) || 1;
|
||
out.push([
|
||
"data.ini",
|
||
cp949(
|
||
`[공통]\r\nlast_work_mode=${mode > 2 ? mode - 2 : mode}\r\nsiljuk_est_mode=${mode > 2 ? 1 : 0}\r\ndata_version=0\r\nproject_title_name=${project.name ?? ""}\r\n`,
|
||
),
|
||
]);
|
||
return pack(new Map(out));
|
||
}
|
||
|
||
// 단가비교: 종류(0 기계 · 1 단산 · 2 일위 · 3 내역) 파일의 Index > 0 레코드 노무·재료·경비
|
||
const COMPARE = [
|
||
["mdt", 210, 200, 190],
|
||
["cct", 203, 213, 223],
|
||
["hdt", 231, 251, 271],
|
||
["dtm", 258, 278, 298],
|
||
];
|
||
function compare(buffer, kind) {
|
||
const files = lower(unpack(buffer)),
|
||
[ext, n, g, j] = COMPARE[kind] || [];
|
||
if (!ext) throw Error("비교할 항목을 선택하세요.");
|
||
const ini = [...files].find(([k]) => k.endsWith(".ini"))?.[1];
|
||
if (ini && /data_version\s*=\s*3/i.test(decoder.decode(ini)))
|
||
throw Error("565 바이트 판 자료의 단가비교 자리는 확인되지 않았습니다.");
|
||
const buf = [...files].find(([k]) => k.endsWith("." + ext))?.[1];
|
||
if (!buf) throw Error(`비교 파일(.${ext})이 없습니다.`);
|
||
return records(buf, 394)
|
||
.filter((r) => r.readInt32LE(5) > 0)
|
||
.map((r) => ({ ONU: readX(r, n), OGU: readX(r, g), OJU: readX(r, j) }));
|
||
}
|
||
module.exports = { read, write, compare, readX, writeX, splitLine };
|