252 lines
10 KiB
JavaScript
252 lines
10 KiB
JavaScript
// 원본 「현재데이타 갱신」(TFRMDATAUPDATE btnApply 0x10bc3d4 → 엔진 0x10e12c8 — spec_update) — 원천 공사(Main Data = 원본 main.estx,
|
|
// 또는 고른 .estx)의 단가로 현재 공사를 갱신. 항목: 노임(LaborcDat) · 자재(StuffcDat, 유류 제외) · 유류(이름이 휘발유·경유·중유 등 8종) ·
|
|
// 표준(SilJukDat, 코드 일치) · 기계(MachGijunDat 통째 교체 + MachTitleDat 이름·규격 일치 + 그 산출근거 줄 교체) · 환율(MachGijunDat 36·89·69·77·70·76).
|
|
// 맞추기: 코드(표준만) 또는 품명 / 품명+규격 / 품명+규격+단위(빈칸·|·(·) 제거, 단위는 첫 「당」·「[」·「]」 제거, 노임·기계는 단위 무시).
|
|
// 찾으면 단가 칸을 원천 값으로(코드·품명·규격·단위·Index·낙찰률·「_」 당초 칸 제외), 단가미갱신(Checked) 끔. 못 찾으면 Checked 켬.
|
|
// 행을 더하거나 지우지 않음(기계 산출근거 줄만 교체). 최신(웹접속)은 원본 회사 서버·유지보수 인증이 필요해 두지 않음.
|
|
// 원본 결함(환율 끔 때 파운드(70) 환율을 76 값으로 덮음)은 재현하지 않음. 자재 단가열 출처 재배치(OtherTables)는 같은 열 순서로 가정.
|
|
const EstDataUpdate = (() => {
|
|
const num = (v) => Number(v) || 0;
|
|
const norm = (s) => String(s ?? "").replace(/[\s|()]/g, "");
|
|
const unit = (s) => norm(s).replace("당", "").replace("[", "").replace("]", "");
|
|
const FUEL = ["휘발유", "경유", "중유", "가솔린", "디젤", "diesel", "gasoline", "heavyoil"];
|
|
const isFuel = (row) =>
|
|
FUEL.includes(
|
|
String(row.Name ?? "")
|
|
.replace(/[\s|]/g, "")
|
|
.toLowerCase(),
|
|
);
|
|
const RATES = [36, 89, 69, 77, 70, 76];
|
|
const SKIP = new Set([
|
|
"Code",
|
|
"Name",
|
|
"Size",
|
|
"Unite",
|
|
"Index",
|
|
"NachalRatio",
|
|
"ErrorCheck",
|
|
"PkID",
|
|
"Checked",
|
|
]);
|
|
|
|
function finder(source, kind) {
|
|
return (row) => {
|
|
if (kind === 21) {
|
|
const code = norm(row.Code);
|
|
return code ? source.find((s) => norm(s.Code) === code) : null;
|
|
}
|
|
const size = norm(row.Size),
|
|
u = kind === 6 || kind === 105 ? "" : unit(row.Unite);
|
|
return source.find((s) => {
|
|
if (norm(s.Name) !== norm(row.Name)) return false;
|
|
const sSize = kind === 6 ? "" : norm(s.Size);
|
|
if (!size && !u) return true;
|
|
if (sSize !== size) return false;
|
|
if (!u) return true;
|
|
return (kind === 105 ? "" : unit(s.Unite)) === u;
|
|
});
|
|
};
|
|
}
|
|
function copy(target, src, skipRatio) {
|
|
for (const [key, value] of Object.entries(src)) {
|
|
if (SKIP.has(key) || key.endsWith("_") || typeof value === "boolean") continue;
|
|
if (skipRatio && key === "Ratio") continue;
|
|
if (typeof value === "string" && !/Page|^Bigo$|^EXPLEAN$|^Date$/.test(key)) continue;
|
|
target[key] = structuredClone(value);
|
|
}
|
|
}
|
|
// 표 하나 갱신 → 못 찾은 수
|
|
function table(rows, source, kind, filter = () => true, skipRatio = false) {
|
|
const find = finder(source, kind);
|
|
let missing = 0;
|
|
for (const row of rows || []) {
|
|
if (!norm(row.Name) && !norm(row.Size)) continue;
|
|
if (!filter(row)) continue;
|
|
const src = find(row);
|
|
if (!src) {
|
|
missing++;
|
|
row.Checked = true;
|
|
continue;
|
|
}
|
|
row.Checked = false;
|
|
const code = row.Code;
|
|
copy(row, src, skipRatio);
|
|
if (kind === 21)
|
|
Object.assign(row, { Code: code, Name: src.Name, Size: src.Size, Unite: src.Unite });
|
|
}
|
|
return missing;
|
|
}
|
|
|
|
function groups(rows) {
|
|
const out = new Map();
|
|
let current = null;
|
|
(rows || []).forEach((row, i) => {
|
|
if (num(row.Index) > 0) {
|
|
current = { head: i, body: [] };
|
|
out.set(row.Code, current);
|
|
} else if (current) current.body.push(i);
|
|
});
|
|
return out;
|
|
}
|
|
function machine(project, source, notices) {
|
|
const t = project.tables,
|
|
s = source.tables;
|
|
// 손료 계수·환율 표 통째 교체, 「_」 당초 칸은 같은 Index 행의 옛 값 유지
|
|
const old = new Map((t.MachGijunDat || []).map((r) => [num(r.Index), r]));
|
|
t.MachGijunDat = structuredClone(s.MachGijunDat || []).map((r) => {
|
|
const prev = old.get(num(r.Index));
|
|
if (prev) for (const k of Object.keys(prev)) if (k.endsWith("_")) r[k] = prev[k];
|
|
return r;
|
|
});
|
|
if (project.baseline) {
|
|
notices.push(
|
|
"설계변경이 이루어진 데이타의 기계경비는적용기준(환율,노임,유류대)만 갱신됩니다.",
|
|
);
|
|
return 0;
|
|
}
|
|
const find = finder(s.MachTitleDat || [], 105);
|
|
let missing = 0;
|
|
const tg = groups(t.MachMainDat),
|
|
sg = groups(s.MachMainDat);
|
|
const replaced = [];
|
|
for (const title of t.MachTitleDat || []) {
|
|
if (!norm(title.Name) && !norm(title.Size)) continue;
|
|
const src = find(title);
|
|
if (!src) {
|
|
missing++;
|
|
title.Checked = true;
|
|
continue;
|
|
}
|
|
title.Checked = false;
|
|
copy(title, src);
|
|
const mine = tg.get(title.Code),
|
|
theirs = sg.get(src.Code);
|
|
if (mine && theirs) {
|
|
copy(t.MachMainDat[mine.head], s.MachMainDat[theirs.head]);
|
|
replaced.push([mine, theirs.body.map((i) => structuredClone(s.MachMainDat[i]))]);
|
|
}
|
|
}
|
|
// 산출근거 줄 교체(뒤에서부터) + 자재·노임 연결은 현재 공사의 같은 이름 자료로 다시 잇기(못 찾으면 코드 비우고 211/212)
|
|
replaced.sort((a, b) => b[0].head - a[0].head);
|
|
for (const [mine, body] of replaced) {
|
|
for (const row of body) {
|
|
const li = num(row.LinkIndex);
|
|
if (li !== 5 && li !== 6) continue;
|
|
const list = li === 5 ? t.StuffcDat : t.LaborcDat,
|
|
hit = (list || []).find(
|
|
(r) => norm(r.Name) === norm(row.Name) && (li === 6 || norm(r.Size) === norm(row.Size)),
|
|
);
|
|
if (hit) row.Code = hit.Code;
|
|
else Object.assign(row, { Code: "", LinkIndex: li === 5 ? 212 : 211 });
|
|
}
|
|
const start = mine.head + 1,
|
|
count = mine.body.length;
|
|
t.MachMainDat.splice(start, count, ...body);
|
|
}
|
|
return missing;
|
|
}
|
|
|
|
// options = { labor, stuff, siljuk, mach, fuel, rate }
|
|
function apply(project, source, o) {
|
|
const t = project.tables,
|
|
s = source.tables,
|
|
notices = [];
|
|
const keep = new Map(
|
|
(t.MachGijunDat || [])
|
|
.filter((r) => RATES.includes(num(r.Index)))
|
|
.map((r) => [num(r.Index), { TC: r.TC, Bigo: r.Bigo }]),
|
|
);
|
|
const miss = { 노임단가: 0, 자재단가: 0, 표준시장단가: 0, 기계경비: 0 };
|
|
if (o.labor) miss.노임단가 = table(t.LaborcDat, s.LaborcDat || [], 6);
|
|
if (o.stuff && o.fuel) miss.자재단가 = table(t.StuffcDat, s.StuffcDat || [], 5);
|
|
else if (o.fuel) table(t.StuffcDat, s.StuffcDat || [], 5, isFuel, true);
|
|
else if (o.stuff) table(t.StuffcDat, s.StuffcDat || [], 5, (r) => !isFuel(r), true);
|
|
if (o.siljuk) miss.표준시장단가 = table(t.SilJukDat, s.SilJukDat || [], 21);
|
|
if (o.mach) miss.기계경비 = machine(project, source, notices);
|
|
const rows = t.MachGijunDat || [];
|
|
if (o.rate && !o.mach)
|
|
for (const i of RATES) {
|
|
const mine = rows.find((r) => num(r.Index) === i),
|
|
theirs = (s.MachGijunDat || []).find((r) => num(r.Index) === i);
|
|
if (mine && theirs) Object.assign(mine, { TC: theirs.TC, Bigo: theirs.Bigo });
|
|
}
|
|
if (!o.rate)
|
|
for (const [i, v] of keep) {
|
|
const mine = rows.find((r) => num(r.Index) === i);
|
|
if (mine) Object.assign(mine, v);
|
|
}
|
|
const parts = Object.entries(miss)
|
|
.filter(([, n]) => n > 0)
|
|
.map(([k, n]) => `${k}에서 ${n} 개`);
|
|
if (parts.length)
|
|
notices.push(
|
|
`${parts.join(", ")}의 찾지못한 데이타가 있습니다.\n\n단가미갱신란에 체크된 데이타를 검토하세요.`,
|
|
);
|
|
return { missing: miss, notices };
|
|
}
|
|
|
|
// 화면
|
|
function dialog(state) {
|
|
const opts = [
|
|
["labor", "노임단가"],
|
|
["stuff", "자재단가"],
|
|
["siljuk", "표준단가"],
|
|
["mach", "기계경비"],
|
|
["fuel", "유류대"],
|
|
["rate", "환율"],
|
|
];
|
|
EstApp.showDialog(
|
|
"현재데이타 갱신",
|
|
`<p><label><input type="radio" name="du-src" value="main" checked> Main Data로 갱신(원본 설치 main.estx)</label><br><label><input type="radio" name="du-src" value="file"> 선택파일로 갱신(.estx)</label><br><label><input type="radio" disabled> 최신기초 자료로 갱신(웹접속) — 원본 회사 서버·유지보수 인증이 필요해 쓰지 않음</label></p><p>갱신시 적용할 항목 : ${opts.map(([k, l]) => `<label><input type="checkbox" name="du-opt" value="${k}" checked> ${l}</label>`).join(" ")}</p><p class="business-note">※주의사항 — 현재데이타 갱신후 이전 상태로 복원할 수 없습니다(저장 전에는 ↶ 취소로 되돌림).</p><button data-action="data-update-run">갱 신</button>`,
|
|
);
|
|
}
|
|
async function source(kind) {
|
|
if (kind === "main") return EstApp.api("/api/main-data");
|
|
const file = await new Promise((resolve) => {
|
|
const picker = document.createElement("input");
|
|
picker.type = "file";
|
|
picker.accept = ".estx";
|
|
picker.addEventListener("change", () => resolve(picker.files[0]));
|
|
picker.click();
|
|
});
|
|
if (!file) throw Error("갱신할 파일을 선택하지 않았습니다.");
|
|
return EstApp.api(`/api/import?name=${encodeURIComponent(file.name)}`, {
|
|
method: "POST",
|
|
body: await file.arrayBuffer(),
|
|
});
|
|
}
|
|
async function run(state) {
|
|
const kind = document.querySelector('#dialog-content [name="du-src"]:checked')?.value,
|
|
o = Object.fromEntries(
|
|
[...document.querySelectorAll('#dialog-content [name="du-opt"]')].map((n) => [
|
|
n.value,
|
|
n.checked,
|
|
]),
|
|
);
|
|
if (!Object.values(o).some(Boolean)) throw Error("갱신할 항목을 고르세요.");
|
|
const src = await source(kind);
|
|
if (
|
|
!confirm(
|
|
`현재 작업데이타를 "${kind === "main" ? "Main" : src.name}"의 단가로 갱신하시겠습니까?`,
|
|
)
|
|
)
|
|
return;
|
|
let result;
|
|
EstApp.byId("dialog").close();
|
|
EstApp.change((project) => (result = apply(project, src, o)));
|
|
state.notice = result.notices.join(" / ") || "현재데이타를 갱신했습니다.";
|
|
EstApp.render();
|
|
}
|
|
function handle(name, state) {
|
|
if (name === "data-update") dialog(state);
|
|
else if (name === "data-update-run")
|
|
run(state).catch((error) => {
|
|
state.notice = error.message;
|
|
EstApp.render();
|
|
});
|
|
else return false;
|
|
return true;
|
|
}
|
|
return { apply, isFuel, table, handle };
|
|
})();
|