Files

701 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// JDLCHK.dll field declarations: 4e926c..4fb010; EST writer e44f28/e4739c/e541ec.
const { pack, unpack } = require("./archive.cjs");
const { createHash } = require("node:crypto");
const { cp949 } = require("./export-estx.cjs");
const schema = require("./jdl-schema.json");
const { exportMachines } = require("./jdl-machine-export.cjs");
const { orderUnits } = require("./jdl-unit-order.cjs");
const { isFixedPart, exportUnitPart } = require("./jdl-unit-part.cjs");
const { canPreserveSpecial, fixedSpecialPart } = require("./jdl-special-unit.cjs");
const { ordinaryMachinePart, machineQuantity } = require("./jdl-machine-parts.cjs");
const { matchesAmounts } = require("./jdl-mainpart.cjs");
const decoder = new TextDecoder("euc-kr", { fatal: true });
function fingerprint(project) {
const { originalJdlState, id, ...content } = project;
return createHash("sha256").update(JSON.stringify(content)).digest("hex");
}
function markOriginal(project) {
project.originalJdlState = fingerprint(project);
return project;
}
const num = (v) => {
const n = Number(v || 0);
if (!Number.isFinite(n)) throw Error("JDL 수치 오류");
return n;
};
function text(v, max = 40) {
const s = String(v ?? "");
if (/[\x00-\x1f]/.test(s)) throw Error("JDL 문자열에 제어문자가 있습니다.");
if (cp949(s).length > max) throw Error(`JDL 문자열 길이 ${max}바이트 초과: ${s}`);
return s;
}
function encode(name, rows) {
const fields = schema[name];
return cp949(
rows
.map(
(row) =>
fields
.map((f) => {
const v = row[f.name];
return f.kind === 6 ? String(num(v)) : text(v, f.kind === 16 ? 100000 : f.size || 1);
})
.join("\t") + "\x05\r\n",
)
.join(""),
);
}
function parse(name, bytes) {
const fields = schema[name];
if (!fields || !bytes) return [];
return decoder
.decode(bytes)
.split(/\r?\n/)
.filter((v) => v.trim())
.map((line, i) => {
const values = line
.replace(/\x05\s*$/, "")
.split("\t")
.map((v) => v.trim());
if (values.length !== fields.length)
throw Error(
`${name} ${i + 1}행: ${fields.length}개 필드가 필요합니다 (${values.length}개).`,
);
return Object.fromEntries(
fields.map((f, j) => [
f.name,
f.kind === 6 ? num(values[j]) : text(values[j], f.kind === 16 ? 100000 : f.size || 1),
]),
);
});
}
function output(project) {
if (project.originalJdl && project.originalJdlState === fingerprint(project))
return {
bytes: Buffer.from(project.originalJdl, "base64"),
warnings: [
"변경되지 않은 원본 JDL을 그대로 내보냅니다. 원본 검사 DLL 인증은 확인하지 않았습니다.",
],
};
const tables = Object.fromEntries(Object.keys(schema).map((k) => [k, []])),
warnings = [],
map = new Map(),
ambiguous = new Set();
let serial = 0;
function source(kind, row) {
const key = kind + ":" + row.Code;
if (row.Code && map.has(key)) {
map.delete(key);
ambiguous.add(key);
warnings.push(`${row.Code}: 중복 자원 코드는 독립 금액으로 전개합니다.`);
}
const prefix = kind === 5 ? "M" : kind === 6 ? "L" : "W",
code = prefix + String(++serial).padStart(10, "0");
const price =
kind === 5
? { F_MPRICE: num(row.TC) }
: kind === 6
? { F_LPRICE: num(row.TC) }
: { F_MPRICE: num(row.GC), F_LPRICE: num(row.NC), F_EPRICE: num(row.JC) + num(row.SC) };
const name = prefix === "M" ? "m.txt" : prefix === "L" ? "l.txt" : "w.txt";
tables[name].push({
F_CODE: code,
F_DES: row.Name,
F_SIZE: row.Size,
F_UNIT: row.Unite,
...price,
F_ORICODE: "00000000000",
});
if (row.Code && !ambiguous.has(key)) map.set(key, code);
return code;
}
for (const [kind, table] of [
[5, "StuffcDat"],
[6, "LaborcDat"],
[21, "SilJukDat"],
[101, "EstiMateDat"],
])
for (const row of project.tables[table] || []) source(kind, row);
for (const table of ["CostTitleDat", "MachTitleDat", "HeadTitleDat"])
for (const row of project.tables[table] || [])
source({ CostTitleDat: 104, MachTitleDat: 105, HeadTitleDat: 103 }[table], row);
for (const title of project.tables.CostTitleDat || []) {
if (!title.Code || ambiguous.has("104:" + title.Code)) continue;
const code = "D" + String(++serial).padStart(10, "0");
map.set("104:" + title.Code, code);
tables["danga.txt"].push({
F_CODE: code,
F_DES: title.Name,
F_SIZE: title.Size,
F_UNIT: title.Unite,
F_MPRICE: num(title.GC),
F_LPRICE: num(title.NC),
F_EPRICE: num(title.JC) + num(title.SC),
F_ORICODE: "00000000000",
});
}
exportMachines(project, tables, map, ambiguous, num, () => ++serial, source, warnings);
const unitGroups = new Map();
let unitGroup;
for (const row of project.tables.HeadMainDat || []) {
if (num(row.Index) > 0) {
unitGroup = [];
if (unitGroups.has(row.Code)) {
if (unitGroups.get(row.Code))
warnings.push(`${row.Code}: 중복 일위 상세는 고정 단가로 전개합니다.`);
unitGroups.set(row.Code, null);
} else unitGroups.set(row.Code, unitGroup);
} else if (unitGroup) unitGroup.push(row);
}
const exportedUnits = new Set();
for (const title of orderUnits(project.tables.HeadTitleDat || [], unitGroups)) {
const parts = unitGroups.get(title.Code);
if (
!title.Code ||
ambiguous.has("103:" + title.Code) ||
num(title.SC) ||
!parts?.length ||
parts.some(
(row) =>
(!isFixedPart(row, num) &&
(![5, 6, 21, 101, 103, 104, 105].includes(num(row.LinkIndex)) ||
num(row.CalcIndex) ||
(num(row.LinkIndex) === 103 && row.Code === title.Code) ||
!map.has(num(row.LinkIndex) + ":" + row.Code))) ||
["SKind", "Gangub", "OCheck", "Biul", "SC", "SU"].some((key) => num(row[key])),
)
)
continue;
const code = "U" + String(++serial).padStart(10, "0");
map.set("103:" + title.Code, code);
for (const row of parts)
if (!isFixedPart(row, num) && num(row.LinkIndex) === 103 && !exportedUnits.has(row.Code))
warnings.push(`${title.Code}/${row.Code}: 하위 일위대가를 고정 단가로 전개합니다.`);
if (parts.some((row) => isFixedPart(row, num)))
warnings.push(`${title.Code}: 일위 상세의 고정 금액을 별도 단가로 내보냅니다.`);
exportedUnits.add(title.Code);
tables["unit.txt"].push({
F_CODE: code,
F_DES: title.Name,
F_SIZE: title.Size,
F_UNIT: title.Unite,
F_MPRICE: num(title.GC),
F_LPRICE: num(title.NC),
F_EPRICE: num(title.JC) + num(title.SC),
F_ORICODE: "00000000000",
});
for (const [index, row] of parts.entries())
tables["unitpart.txt"].push(
exportUnitPart(row, code, index, map, source, num, () => ++serial),
);
}
let group = null,
groupNumber = 0,
sequence = 0;
function addGroup(name) {
if (++groupNumber > 99) throw Error("JDL 단일 레벨 변환은 공종 99개까지 가능합니다.");
group = {
F_CODE: String(groupNumber).padStart(2, "0"),
F_DES: text(name),
F_QQTY: 1,
F_MPRICE: 0,
F_LPRICE: 0,
F_EPRICE: 0,
F_ORICODE: "00000000000",
};
tables["gongjong.txt"].push(group);
sequence = 0;
}
for (const row of project.tables.EstMainDat) {
if (num(row.LinkIndex) === 54) break;
if (num(row.Level) > 0) {
addGroup(row.Name);
continue;
}
if (!group) addGroup(project.name);
if (num(row.CalcIndex) >= 2) {
warnings.push(`${row.Code || row.Name}: 합산 제외 행 생략`);
continue;
}
let key = map.get(num(row.LinkIndex) + ":" + row.Code),
gn = num(row.GN),
m = num(row.GU),
l = num(row.NU),
e = num(row.JU) + num(row.SU);
if (
!key ||
num(row.CalcIndex) === 1 ||
num(row.CalcIndex) === 3 ||
!matchesAmounts(row, gn, [m, l, e], num)
) {
key = source(0, {
Code: "LITERAL" + ++serial,
Name: row.Name,
Size: row.Size,
Unite: row.Unite,
GC: row.GC,
NC: row.NC,
JC: row.JC,
SC: row.SC,
});
gn = ["%", "%"].includes(row.Unite) ? 100 : 1;
m = num(row.GC);
l = num(row.NC);
e = num(row.JC) + num(row.SC);
warnings.push(`${row.Code || row.Name}: 저장 금액을 독립 항목으로 전개`);
}
tables["mainpart.txt"].push({
F_CODE: group.F_CODE,
F_SEQ: String(++sequence),
F_CODE1: key,
F_DES: row.Name,
F_SIZE: row.Size,
F_UNIT: row.Unite,
F_QQTY: gn,
F_MPRICE: m,
F_LPRICE: l,
F_EPRICE: e,
F_SPROPER: "0",
F_LOADBM: "T",
F_SAVEBM: "T",
F_SRATE: 0,
F_KWAN: "0",
F_CLASS: "0",
F_ORICODE: "00000000000",
});
group.F_MPRICE += num(row.GC);
group.F_LPRICE += num(row.NC);
group.F_EPRICE += num(row.JC) + num(row.SC);
}
if (!tables["gongjong.txt"].length) addGroup(project.name);
const direct = tables["gongjong.txt"].reduce(
(a, r) => ({ m: a.m + r.F_MPRICE, l: a.l + r.F_LPRICE, e: a.e + r.F_EPRICE }),
{ m: 0, l: 0, e: 0 },
);
const global = `[BASIC]\r\nMAKER=ESTWEB\r\nVERSION=2.00\r\nCDATE=${new Date().toISOString().slice(0, 10).replaceAll("-", "")}\r\n[CPART]\r\nMQBIT=7C\r\nMABIT=0C\r\nMTBIT=0C\r\nMCMETHOD=0LME\r\nUABIT=0C\r\nUTBIT=0C\r\nUCMETHOD=0LME\r\nDCMETHOD=0LME\r\nJABIT=0C\r\nJTBIT=0C\r\nJCMETHOD=0LME\r\nDQBIT=2R\r\nDABIT=0C\r\nDTBIT=0C\r\n[JPART]\r\n[VPART]\r\n[COST]\r\nMCOST=${direct.m}\r\nLCOST=${direct.l}\r\nECOST=${direct.e}\r\nTCOST=${direct.m + direct.l + direct.e}\r\n`;
const files = new Map([
["Global.ini", cp949(global)],
["Main.txt", cp949(["0", text(project.name, 80), "02"].join("\t") + "\x05\r\n")],
]);
for (const [name, rows] of Object.entries(tables)) files.set(name, encode(name, rows));
const fees = (project.tables.Wonga || [])
.map(
(r) =>
[
text(r.Gubun, 12),
text(r.Name),
text(r.Youl, 25),
text(r.VSusic, 200),
String(num(r.Cost)),
].join("\t") + "\x05\r\n",
)
.join("");
files.set("CostAccount.txt", cp949(fees));
warnings.unshift(
"JDL 기본 표를 출력합니다. 산근·기계와 특수 일위의 편집 산식, 관급/특수 업무 구조는 JSON/ESTX에 보관하세요. 원본 검사 DLL의 인증은 미확인입니다.",
);
return { bytes: pack(files), warnings };
}
function read(bytes, project) {
const files = new Map();
for (const [name, data] of unpack(bytes)) {
const key = name.toLowerCase();
if (files.has(key)) throw Error(`JDL 파일명 중복: ${name}`);
files.set(key, data);
}
if (!files.has("global.ini") || !files.has("main.txt"))
throw Error("Global.ini/Main.txt 없는 JDL입니다.");
const main = decoder
.decode(files.get("main.txt"))
.replace(/\x05\s*$/, "")
.split("\t");
if (main.length !== 3) throw Error("JDL Main.txt 필드 오류");
project.name = main[1].trim();
project.options.AutoCalc = false;
project.options.SourceRefresh = false;
const rows = Object.fromEntries(
Object.keys(schema).map((name) => [name, parse(name, files.get(name))]),
);
const byCode = new Map();
for (const [file, table, kind] of [
["m.txt", "StuffcDat", 5],
["l.txt", "LaborcDat", 6],
["e.txt", "EstiMateDat", 101],
["w.txt", "EstiMateDat", 101],
["j.txt", "EstiMateDat", 101],
["unit.txt", "HeadTitleDat", 103],
["junit.txt", "MachTitleDat", 105],
["danga.txt", "CostTitleDat", 104],
])
for (const r of rows[file]) {
if (byCode.has(r.F_CODE)) throw Error("JDL 자원 코드 중복: " + r.F_CODE);
const row = {
PkID: project.tables[table].length + 1,
Code: r.F_CODE,
Name: r.F_DES,
Size: r.F_SIZE,
Unite: r.F_UNIT,
GC: num(r.F_MPRICE),
NC: num(r.F_LPRICE),
JC: num(r.F_EPRICE),
SC: 0,
};
row.TC = row.InputTC = row.GC + row.NC + row.JC;
row.ValueIndex = 4;
project.tables[table].push(row);
byCode.set(row.Code, { row, kind });
}
let unitPk = 0;
const unitWarnings = [];
const machineParts = new Map();
for (const part of rows["jpart.txt"]) {
if (!machineParts.has(part.F_CODE)) machineParts.set(part.F_CODE, []);
machineParts.get(part.F_CODE).push(part);
}
let machineDetailed = false,
machineWeighted = false,
machineSpecial = false,
machineFallback = false;
for (const title of rows["junit.txt"]) {
const gc = num(title.F_MPRICE),
nc = num(title.F_LPRICE),
jc = num(title.F_EPRICE);
const amounts = { GC: gc, NC: nc, JC: jc, SC: 0, TC: gc + nc + jc };
const parts = machineParts.get(title.F_CODE) || [];
const uniqueSequences = new Set(parts.map((part) => part.F_SEQ));
const identifiable =
parts.length > 0 &&
uniqueSequences.size === parts.length &&
parts.every((part) => Boolean(part.F_SEQ) && num(part.F_QQTY) > 0);
const ordinary = identifiable && parts.every((part) => ordinaryMachinePart(part, num));
const matches = (weighted) =>
[
["F_MPRICE", gc],
["F_LPRICE", nc],
["F_EPRICE", jc],
].every(
([field, value]) =>
Math.abs(
parts.reduce(
(sum, part) => sum + num(part[field]) * (weighted ? machineQuantity(part, num) : 1),
0,
) - value,
) < 1e-6,
);
const direct = identifiable && matches(false);
const weighted = identifiable && !direct && matches(true);
const basic = (direct && parts.every((part) => machineQuantity(part, num) === 1)) || weighted;
project.tables.MachMainDat.push({
PkID: project.tables.MachMainDat.length + 1,
Index: 1,
Code: title.F_CODE,
JdlSummary: true,
Name: title.F_DES,
Size: title.F_SIZE,
Unite: title.F_UNIT,
...amounts,
});
if (basic) {
machineDetailed = true;
machineWeighted ||= weighted;
machineSpecial ||= !ordinary;
for (const part of parts) {
const factor = weighted ? machineQuantity(part, num) : 1,
GC = num(part.F_MPRICE) * factor,
NC = num(part.F_LPRICE) * factor,
JC = num(part.F_EPRICE) * factor;
project.tables.MachMainDat.push({
PkID: project.tables.MachMainDat.length + 1,
Index: 0,
CalcIndex: 1,
JdlSummary: true,
JdlSourceCode: part.F_CODE1,
JdlSourceQuantity: num(part.F_QQTY),
Name: part.F_DES,
Size: part.F_SIZE,
Unite: part.F_UNIT,
GN: num(part.F_QQTY),
GC,
NC,
JC,
SC: 0,
TC: GC + NC + JC,
});
}
} else {
machineFallback = true;
project.tables.MachMainDat.push({
PkID: project.tables.MachMainDat.length + 1,
Index: 0,
CalcIndex: 1,
JdlSummary: true,
Name: "JDL 저장 금액 (상세 산식 미변환)",
...amounts,
});
}
}
if (machineDetailed)
unitWarnings.push(
"기계 상세는 저장 금액별로 표시하며, 산식과 원천 단가 연결은 원본 JDL에 보존됩니다.",
);
if (machineWeighted)
unitWarnings.push(
"일부 기계 상세는 단위에 따른 수량×단가가 저장 합계와 일치해 계산 금액으로 표시합니다.",
);
if (machineSpecial)
unitWarnings.push("특수 기계 상세는 저장 금액으로 고정하며 특수 산식은 원문에 보존합니다.");
if (machineFallback)
unitWarnings.push(
"일부 기계경비는 저장 금액으로 가져왔습니다. 상세 산식은 원본 JDL에 보존됩니다.",
);
if (rows["jpart.txt"].some((r) => !byCode.has(r.F_CODE) || byCode.get(r.F_CODE).kind !== 105))
unitWarnings.push("부모 기계 단가가 없는 상세 행은 원본 JDL에만 보존됩니다.");
for (const title of rows["danga.txt"]) {
const gc = num(title.F_MPRICE),
nc = num(title.F_LPRICE),
jc = num(title.F_EPRICE);
project.tables.CostMainDat.push({
PkID: project.tables.CostMainDat.length + 1,
Index: 1,
Code: title.F_CODE,
JdlSummary: true,
Name: title.F_DES,
Size: title.F_SIZE,
Unite: title.F_UNIT,
GC: gc,
NC: nc,
JC: jc,
SC: 0,
TC: gc + nc + jc,
});
// shortcut: JDL 단가 산출 상세는 원문에만 보존; 산식 형식 확인 후 상세 변환.
for (const [key, value, check] of [
["GC", gc, -2],
["NC", nc, -1],
["JC", jc, -3],
]) {
const literal = value.toLocaleString("en-US", {
useGrouping: false,
maximumFractionDigits: 20,
});
project.tables.CostMainDat.push({
PkID: project.tables.CostMainDat.length + 1,
Index: 0,
SCheck: check,
JdlSummary: true,
Name: "JDL 저장 금액",
LineData: `${literal} = \x12${literal}\x12`,
[key]: value,
TC: value,
});
}
}
if (rows["danga.txt"].length)
unitWarnings.push("단가산출은 저장 금액으로 가져왔습니다. 상세 산식은 원본 JDL에 보존됩니다.");
const unitCodes = new Set(rows["unit.txt"].map((r) => r.F_CODE));
const partsByUnit = new Map();
for (const part of rows["unitpart.txt"]) {
if (!partsByUnit.has(part.F_CODE)) partsByUnit.set(part.F_CODE, []);
partsByUnit.get(part.F_CODE).push(part);
}
for (const title of rows["unit.txt"]) {
const gc = num(title.F_MPRICE),
nc = num(title.F_LPRICE),
jc = num(title.F_EPRICE);
const parts = partsByUnit.get(title.F_CODE) || [];
const preserveSpecial = canPreserveSpecial(parts, title, num);
let incomplete = !parts.length;
const included = { GC: 0, NC: 0, JC: 0 };
project.tables.HeadMainDat.push({
PkID: ++unitPk,
Index: 1,
Code: title.F_CODE,
TitleCode: title.F_CODE,
JdlSummary: true,
Name: title.F_DES,
Size: title.F_SIZE,
Unite: title.F_UNIT,
GC: gc,
NC: nc,
JC: jc,
SC: 0,
TC: gc + nc + jc,
});
for (const part of parts) {
const source = byCode.get(part.F_CODE1);
const basic =
source &&
["", "0"].includes(part.F_SPROPER) &&
["", "T"].includes(part.F_LOADBM) &&
["", "T"].includes(part.F_SAVEBM) &&
!num(part.F_SRATE) &&
["", "0"].includes(part.F_KWAN) &&
["", "0"].includes(part.F_CLASS);
if (!basic) {
if (preserveSpecial) {
const row = fixedSpecialPart(part, source, ++unitPk, num);
project.tables.HeadMainDat.push(row);
for (const key of ["GC", "NC", "JC"]) included[key] += row[key];
unitWarnings.push(
`일위 ${title.F_CODE}/${part.F_SEQ}: 특수 구성은 저장 금액으로 고정하며 산식은 원문에 보존합니다.`,
);
continue;
}
incomplete = true;
unitWarnings.push(`일위 ${title.F_CODE}/${part.F_SEQ}: 특수 구성은 원문에만 보존`);
continue;
}
const gc = num(part.F_MCOST),
nc = num(part.F_LCOST),
jc = num(part.F_ECOST);
const quantity = num(part.F_QQTY) / (["%", "%"].includes(part.F_UNIT) ? 100 : 1);
const fixedAmount = [
[gc, num(part.F_MPRICE)],
[nc, num(part.F_LPRICE)],
[jc, num(part.F_EPRICE)],
].some(([amount, price]) => Math.abs(amount - Math.trunc(quantity * price)) > 0.000001);
if (fixedAmount)
unitWarnings.push(`일위 ${title.F_CODE}/${part.F_SEQ}: 상세의 저장 금액을 고정합니다.`);
included.GC += gc;
included.NC += nc;
included.JC += jc;
project.tables.HeadMainDat.push({
PkID: ++unitPk,
Index: 0,
Code: part.F_CODE1,
Name: part.F_DES,
Size: part.F_SIZE,
Unite: part.F_UNIT,
LinkIndex: source.kind,
CalcIndex: fixedAmount ? 1 : 0,
...(fixedAmount ? { JdlSummary: true } : {}),
GN: num(part.F_QQTY),
GU: num(part.F_MPRICE),
NU: num(part.F_LPRICE),
JU: num(part.F_EPRICE),
SU: 0,
GC: gc,
NC: nc,
JC: jc,
SC: 0,
TC: gc + nc + jc,
TU: num(part.F_MPRICE) + num(part.F_LPRICE) + num(part.F_EPRICE),
});
}
if (
incomplete ||
["GC", "NC", "JC"].some((key, i) => Math.abs([gc, nc, jc][i] - included[key]) > 0.000001)
) {
const gc = num(title.F_MPRICE) - included.GC,
nc = num(title.F_LPRICE) - included.NC,
jc = num(title.F_EPRICE) - included.JC;
project.tables.HeadMainDat.push({
PkID: ++unitPk,
Index: 0,
CalcIndex: 1,
JdlSummary: true,
Name: "JDL 저장 금액 차액",
GC: gc,
NC: nc,
JC: jc,
SC: 0,
TC: gc + nc + jc,
});
unitWarnings.push(`일위 ${title.F_CODE}: 상세와 저장 합계의 차액을 보존합니다.`);
}
}
if (rows["unitpart.txt"].some((r) => !unitCodes.has(r.F_CODE)))
unitWarnings.push("부모 일위대가가 없는 상세 행은 원문에만 보존");
const headings = new Set();
const partsByGroup = new Map();
for (const part of rows["mainpart.txt"]) {
if (!partsByGroup.has(part.F_CODE)) partsByGroup.set(part.F_CODE, []);
partsByGroup.get(part.F_CODE).push(part);
}
let pk = 0;
for (const group of rows["gongjong.txt"]) {
if (headings.has(group.F_CODE)) throw Error("JDL 공종 코드 중복");
headings.add(group.F_CODE);
project.tables.EstMainDat.push({
PkID: ++pk,
Level: 1,
Code: group.F_CODE,
Name: group.F_DES,
GC: group.F_MPRICE,
NC: group.F_LPRICE,
JC: group.F_EPRICE,
SC: 0,
TC: group.F_MPRICE + group.F_LPRICE + group.F_EPRICE,
});
for (const r of partsByGroup.get(group.F_CODE) || []) {
const source = byCode.get(r.F_CODE1);
if (!source) throw Error("JDL 연결 자원 없음: " + r.F_CODE1);
const row = {
PkID: ++pk,
Code: r.F_CODE1,
Name: r.F_DES,
Size: r.F_SIZE,
Unite: r.F_UNIT,
GN: r.F_QQTY,
GU: r.F_MPRICE,
NU: r.F_LPRICE,
JU: r.F_EPRICE,
SU: 0,
LinkIndex: source.kind,
CalcIndex: 0,
};
const quantity = row.GN / (["%", "%"].includes(row.Unite) ? 100 : 1);
row.GC = Math.trunc(quantity * row.GU);
row.NC = Math.trunc(quantity * row.NU);
row.JC = Math.trunc(quantity * row.JU);
row.SC = 0;
row.TC = row.GC + row.NC + row.JC;
row.TU = row.GU + row.NU + row.JU;
project.tables.EstMainDat.push(row);
}
}
if (rows["mainpart.txt"].some((r) => !headings.has(r.F_CODE))) throw Error("JDL 부모 공종 없음");
const direct = project.tables.EstMainDat.filter((r) => !r.Level),
sum = (k) => direct.reduce((a, r) => a + num(r[k]), 0);
project.tables.EstMainDat.push({
PkID: ++pk,
Name: "순공사비",
LinkIndex: 54,
GC: sum("GC"),
NC: sum("NC"),
JC: sum("JC"),
SC: 0,
TC: sum("TC"),
});
if (files.has("costaccount.txt")) {
const lines = decoder
.decode(files.get("costaccount.txt"))
.split(/\r?\n/)
.filter((s) => s.trim());
for (const line of lines) {
const f = line.replace(/\x05\s*$/, "").split("\t");
if (f.length < 5) throw Error("JDL 원가 필드 오류");
project.tables.Wonga.push({
Gubun: f[0].trim(),
Name: f[1].trim(),
Youl: f[2].trim(),
VSusic: f[3].trim(),
Cost: num(f[4]),
});
}
}
project.originalJdl = bytes.toString("base64");
project.importWarnings = [
"JDL 저장 금액을 보존하기 위해 자동계산을 껐습니다. 복잡한 단가산출식·손료/기성 확장 파일은 원문에 보존되며 자동 변환하지 않습니다.",
...unitWarnings,
];
project.calculation = {
errors: [],
warnings: project.importWarnings,
direct: project.tables.EstMainDat.at(-1),
total: project.tables.Wonga.at(-1)?.Cost ?? sum("TC"),
};
return project;
}
module.exports = { output, read, parse, markOriginal };