feat(est): reproduce native tables and calculations
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
const EstModel = (() => {
|
||||
const labels = {
|
||||
Index: "구분",
|
||||
Code: "코드",
|
||||
Name: "품명",
|
||||
Size: "규격",
|
||||
Unite: "단위",
|
||||
GN: "수량",
|
||||
TU: "단가",
|
||||
TC: "금액",
|
||||
NU: "노무단가",
|
||||
GU: "재료단가",
|
||||
JU: "경비단가",
|
||||
SU: "실적단가",
|
||||
NC: "노무비",
|
||||
GC: "재료비",
|
||||
JC: "경비",
|
||||
SC: "실적",
|
||||
Level: "레벨",
|
||||
LinkIndex: "연결종류",
|
||||
CalcIndex: "계산구분",
|
||||
SCheck: "결과비목",
|
||||
OCheck: "기준비목",
|
||||
SKind: "구간",
|
||||
Diget: "자릿수코드",
|
||||
DigetIndex: "처리방식",
|
||||
Gangub: "맞춤자리",
|
||||
Ratio: "조정률",
|
||||
NachalRatio: "낙찰률",
|
||||
ValueIndex: "단가선택",
|
||||
SelectField: "선택출처",
|
||||
NCGasu: "운전사계수",
|
||||
LineData: "산출근거",
|
||||
Gubun: "기호",
|
||||
Cost: "원가금액",
|
||||
Youl: "요율",
|
||||
VSusic: "수식",
|
||||
BaseField: "기초연결",
|
||||
InputTC: "입력금액",
|
||||
Precision: "줄 소수자리",
|
||||
RoundMode: "줄 처리방식",
|
||||
};
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
labels[`Cost${i}`] = `출처${i}`;
|
||||
labels[`Page${i}`] = `쪽수${i}`;
|
||||
}
|
||||
const numeric = new Set([
|
||||
"Index",
|
||||
"PkID",
|
||||
"GN",
|
||||
"TU",
|
||||
"TC",
|
||||
"NU",
|
||||
"GU",
|
||||
"JU",
|
||||
"SU",
|
||||
"NC",
|
||||
"GC",
|
||||
"JC",
|
||||
"SC",
|
||||
"Level",
|
||||
"LinkIndex",
|
||||
"CalcIndex",
|
||||
"SCheck",
|
||||
"OCheck",
|
||||
"SKind",
|
||||
"Diget",
|
||||
"DigetIndex",
|
||||
"Gangub",
|
||||
"Ratio",
|
||||
"NachalRatio",
|
||||
"ValueIndex",
|
||||
"SelectField",
|
||||
"NCGasu",
|
||||
"InputTC",
|
||||
"Precision",
|
||||
"RoundMode",
|
||||
"Cost",
|
||||
...Array.from({ length: 6 }, (_, i) => `Cost${i + 1}`),
|
||||
]);
|
||||
for (const key of ["ANU", "AGU", "AJU", "ASU", "EscPlanned", "EscActual", "EscProgress"])
|
||||
numeric.add(key);
|
||||
const definitions = {
|
||||
EstMainDat: [
|
||||
"Level",
|
||||
"Code",
|
||||
"Name",
|
||||
"Size",
|
||||
"Unite",
|
||||
"GN",
|
||||
"TU",
|
||||
"NU",
|
||||
"GU",
|
||||
"JU",
|
||||
"SU",
|
||||
"TC",
|
||||
"NC",
|
||||
"GC",
|
||||
"JC",
|
||||
"SC",
|
||||
"LinkIndex",
|
||||
"CalcIndex",
|
||||
"SCheck",
|
||||
"OCheck",
|
||||
"SKind",
|
||||
"Gangub",
|
||||
"Index",
|
||||
"Diget",
|
||||
"DigetIndex",
|
||||
],
|
||||
HeadMainDat: [
|
||||
"Index",
|
||||
"Code",
|
||||
"Name",
|
||||
"Size",
|
||||
"Unite",
|
||||
"GN",
|
||||
"NU",
|
||||
"GU",
|
||||
"JU",
|
||||
"SU",
|
||||
"TC",
|
||||
"NC",
|
||||
"GC",
|
||||
"JC",
|
||||
"SC",
|
||||
"LinkIndex",
|
||||
"CalcIndex",
|
||||
"SCheck",
|
||||
"OCheck",
|
||||
"SKind",
|
||||
"Diget",
|
||||
"DigetIndex",
|
||||
],
|
||||
CostMainDat: [
|
||||
"Index",
|
||||
"Code",
|
||||
"LineData",
|
||||
"Level",
|
||||
"LinkIndex",
|
||||
"SCheck",
|
||||
"Precision",
|
||||
"RoundMode",
|
||||
"TC",
|
||||
"NC",
|
||||
"GC",
|
||||
"JC",
|
||||
"SC",
|
||||
],
|
||||
MachMainDat: [
|
||||
"Index",
|
||||
"Code",
|
||||
"Name",
|
||||
"Size",
|
||||
"Unite",
|
||||
"GN",
|
||||
"TU",
|
||||
"Ratio",
|
||||
"TC",
|
||||
"NC",
|
||||
"GC",
|
||||
"JC",
|
||||
"NCGasu",
|
||||
"SCheck",
|
||||
"LinkIndex",
|
||||
"CalcIndex",
|
||||
],
|
||||
MachGijunDat: ["Index", "Name", "TC", "Bigo"],
|
||||
Wonga: [
|
||||
"Gubun",
|
||||
"Name",
|
||||
"Youl",
|
||||
"VSusic",
|
||||
"Cost",
|
||||
"BaseField",
|
||||
"LinkIndex",
|
||||
"CalcIndex",
|
||||
"Diget",
|
||||
"DigetIndex",
|
||||
],
|
||||
StuffcDat: [
|
||||
"Code",
|
||||
"Name",
|
||||
"Size",
|
||||
"Unite",
|
||||
"ValueIndex",
|
||||
"SelectField",
|
||||
"Cost1",
|
||||
"Cost2",
|
||||
"Cost3",
|
||||
"Cost4",
|
||||
"Cost5",
|
||||
"Cost6",
|
||||
"TC",
|
||||
"Ratio",
|
||||
"NachalRatio",
|
||||
"Diget",
|
||||
"DigetIndex",
|
||||
],
|
||||
LaborcDat: [
|
||||
"Code",
|
||||
"Name",
|
||||
"Unite",
|
||||
"ValueIndex",
|
||||
"SelectField",
|
||||
"Cost1",
|
||||
"Cost2",
|
||||
"Cost3",
|
||||
"Cost4",
|
||||
"Cost5",
|
||||
"Cost6",
|
||||
"TC",
|
||||
"Ratio",
|
||||
"NachalRatio",
|
||||
],
|
||||
EstiMateDat: [
|
||||
"Code",
|
||||
"Name",
|
||||
"Size",
|
||||
"Unite",
|
||||
"InputTC",
|
||||
"TC",
|
||||
"NC",
|
||||
"GC",
|
||||
"JC",
|
||||
"SC",
|
||||
"Ratio",
|
||||
"NachalRatio",
|
||||
],
|
||||
SilJukDat: [
|
||||
"Code",
|
||||
"Name",
|
||||
"Size",
|
||||
"Unite",
|
||||
"Cost1",
|
||||
"TC",
|
||||
"NC",
|
||||
"GC",
|
||||
"JC",
|
||||
"SC",
|
||||
"Ratio",
|
||||
"NachalRatio",
|
||||
],
|
||||
};
|
||||
function columns(table) {
|
||||
return (
|
||||
definitions[table] || ["Index", "Code", "Name", "Size", "Unite", "TC", "NC", "GC", "JC", "SC"]
|
||||
);
|
||||
}
|
||||
function validate(project) {
|
||||
if (
|
||||
!project ||
|
||||
project.schema !== "est-web-2" ||
|
||||
typeof project.name !== "string" ||
|
||||
!project.tables ||
|
||||
typeof project.tables !== "object" ||
|
||||
Array.isArray(project.tables)
|
||||
)
|
||||
throw Error("EST 웹앱 프로젝트 형식이 아닙니다.");
|
||||
let records = 0;
|
||||
for (const [table, rows] of Object.entries(project.tables)) {
|
||||
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(table) || !Array.isArray(rows))
|
||||
throw Error("테이블 형식 오류");
|
||||
records += rows.length;
|
||||
if (records > 500000) throw Error("레코드 수 한도 초과");
|
||||
for (const row of rows) {
|
||||
if (!row || typeof row !== "object" || Array.isArray(row)) throw Error("행 형식 오류");
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (["__proto__", "constructor", "prototype"].includes(key))
|
||||
throw Error("허용하지 않는 필드 이름");
|
||||
if (
|
||||
numeric.has(key) &&
|
||||
value != null &&
|
||||
(typeof value !== "number" ||
|
||||
!Number.isFinite(value) ||
|
||||
Math.abs(value) > Number.MAX_SAFE_INTEGER)
|
||||
)
|
||||
throw Error(`수치 필드 오류: ${table}.${key}`);
|
||||
if (typeof value === "string" && value.length > 200000)
|
||||
throw Error("문자열 길이 한도 초과");
|
||||
}
|
||||
}
|
||||
}
|
||||
project.options ||= {};
|
||||
for (const key of ["quantities", "progress", "annual", "drawings"]) {
|
||||
if (project[key] === undefined) project[key] = [];
|
||||
if (!Array.isArray(project[key])) throw Error(`${key} 형식 오류`);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
function legacy(data) {
|
||||
if (data.schema === "est-web-2") return validate(data);
|
||||
if (!["sources", "units", "estimate", "costs"].every((key) => Array.isArray(data[key])))
|
||||
throw Error("지원하지 않는 프로젝트 파일");
|
||||
const project = EstNative.empty(),
|
||||
mapping = {
|
||||
재료비: ["StuffcDat", 5, "GC"],
|
||||
노무비: ["LaborcDat", 6, "NC"],
|
||||
경비: ["EstiMateDat", 101, "JC"],
|
||||
실적: ["SilJukDat", 21, "SC"],
|
||||
};
|
||||
project.name = data.projectName || "이전 작업";
|
||||
project.legacyOriginal = structuredClone(data);
|
||||
for (const source of data.sources) {
|
||||
const [table, kind, component] = mapping[source.kind] || mapping.재료비;
|
||||
project.tables[table].push({
|
||||
Code: source.id,
|
||||
Name: source.name,
|
||||
Size: source.spec,
|
||||
Unite: source.unit,
|
||||
TC: source.price,
|
||||
InputTC: source.price,
|
||||
[component]: source.price,
|
||||
ValueIndex: 4,
|
||||
});
|
||||
source._nativeKind = kind;
|
||||
}
|
||||
data.units.forEach((unit, index) => {
|
||||
project.tables.HeadMainDat.push({
|
||||
Index: index + 1,
|
||||
Code: unit.id,
|
||||
Name: unit.name,
|
||||
Unite: unit.unit,
|
||||
});
|
||||
unit.lines.forEach((line) => {
|
||||
const source = data.sources.find((s) => s.id === line.sourceId);
|
||||
project.tables.HeadMainDat.push({
|
||||
Index: 0,
|
||||
Code: line.type === "일위대가" ? line.unitId : line.sourceId,
|
||||
Name: source?.name,
|
||||
Size: line.base === "합계" ? "합계" : line.base,
|
||||
Unite: line.type === "비율" ? "%" : source?.unit,
|
||||
GN: line.quantity,
|
||||
LinkIndex:
|
||||
line.type === "일위대가" ? 103 : line.type === "비율" ? 58 : source?._nativeKind || 0,
|
||||
SCheck: { 재료비: 1, 노무비: 2, 경비: 3 }[line.kind] || 0,
|
||||
Diget: line.digits ? line.digits + 1 : 0,
|
||||
DigetIndex: line.digits ? 2 : 0,
|
||||
});
|
||||
});
|
||||
project.tables.HeadTitleDat.push({ Code: unit.id, Name: unit.name, Unite: unit.unit });
|
||||
});
|
||||
project.tables.EstMainDat = data.estimate.map((row, index) => ({
|
||||
PkID: index + 1,
|
||||
Index: 0,
|
||||
Code: row.unitId,
|
||||
Name: row.name,
|
||||
GN: row.quantity,
|
||||
LinkIndex: 103,
|
||||
Level: 0,
|
||||
NewItem: true,
|
||||
}));
|
||||
project.tables.EstMainDat.push({ Name: "순공사비", LinkIndex: 54 });
|
||||
const bases = { 재료비: "GC", 노무비: "NC", 경비: "JC", 실적: "SC", 직접공사비: "TC" };
|
||||
project.tables.Wonga = [{ Gubun: "기초", Name: "직접공사비", BaseField: "TC", Cost: 0 }];
|
||||
for (const row of data.costs)
|
||||
project.tables.Wonga.push({
|
||||
Gubun: row.id,
|
||||
Name: row.name,
|
||||
Youl: `${row.rate}%`,
|
||||
VSusic:
|
||||
row.formula ||
|
||||
`[${row.base === "누계" ? "기초" : bases[row.base] || "TC"}]*${row.rate}/100`,
|
||||
});
|
||||
return validate(project);
|
||||
}
|
||||
function newRow(table, rows) {
|
||||
const max = Math.max(0, ...rows.map((r) => Number(r.PkID) || 0));
|
||||
const row = {
|
||||
PkID: max + 1,
|
||||
Index: 0,
|
||||
Name: "새 항목",
|
||||
GN: 1,
|
||||
CalcIndex: 0,
|
||||
LinkIndex: 0,
|
||||
Level: 0,
|
||||
};
|
||||
if (["StuffcDat", "LaborcDat", "SilJukDat", "EstiMateDat"].includes(table))
|
||||
Object.assign(row, { Code: `U${Date.now().toString(36)}`, ValueIndex: 4, InputTC: 0, TC: 0 });
|
||||
if (table === "Wonga") Object.assign(row, { Gubun: `R${max + 1}`, VSusic: "", Cost: 0 });
|
||||
if (table === "CostMainDat") Object.assign(row, { LineData: "Q = 1 + 1 = \x122\x12" });
|
||||
return row;
|
||||
}
|
||||
return { labels, numeric, columns, validate, legacy, newRow };
|
||||
})();
|
||||
Reference in New Issue
Block a user