wip(est-test): 개발 중 저장 3 — 깃 싱크(11-08)

This commit is contained in:
2026-11-08 17:44:13 +09:00
parent d39eece45e
commit db71cdbf7c
28 changed files with 1027 additions and 202 deletions
+163 -9
View File
@@ -135,12 +135,25 @@ const EstMachine = (() => {
function usage(project) {
const titles = project.tables?.MachTitleDat || [];
const grps = EstNative.groups(project.tables?.MachMainDat || []);
const original = EstNative.groups(project.baseline?.tables?.MachMainDat || []);
const oldTitles = project.baseline?.tables?.MachTitleDat || [];
const oldByCode = new Map();
for (const row of oldTitles) {
const rows = oldByCode.get(row.Code) || [];
rows.push(row);
oldByCode.set(row.Code, rows);
}
return titles.map((title, index) => {
const code = title.Code || "";
const group = grps.get(code);
const appliedPrice = EstMath.num(title.TC);
const hasOriginal = title.TC_ != null;
const originalPrice = hasOriginal ? EstMath.num(title.TC_) : null;
const saved = oldByCode.get(code)?.length === 1 ? oldByCode.get(code)[0] : null;
const hasOriginal = Boolean(saved) || title.TC_ != null;
const originalPrice = saved
? EstMath.num(saved.TC)
: hasOriginal
? EstMath.num(title.TC_)
: null;
const difference = hasOriginal ? appliedPrice - originalPrice : null;
return {
index,
@@ -157,6 +170,7 @@ const EstMachine = (() => {
detailCount: group ? group.rows.length : 0,
status: !hasOriginal ? "신규(당초없음)" : Math.abs(difference) > 1e-6 ? "당초차이" : "일치",
canApply: false, // 당초 계약값은 비교용이며 현재 단가 덮어쓰기 적용을 지원하지 않음
canRestore: Boolean(group && original.has(code)),
};
});
}
@@ -164,14 +178,28 @@ const EstMachine = (() => {
function changes(project) {
const labels = { new: "신규", deleted: "삭제", changed: "변경", unchanged: "동일" };
const titles = project.tables?.MachTitleDat || [];
return titles.map((title, index) => {
const old = project.baseline?.tables?.MachTitleDat || [];
const oldGroups = EstNative.groups(project.baseline?.tables?.MachMainDat || []);
const identities = EstRowMatch.identities(old, titles, (row) => String(row.Code || ""));
const previous = new Map(old.map((row, index) => [identities.original[index], row]));
const result = titles.map((title, index) => {
const appliedPrice = EstMath.num(title.TC);
const hasOriginal = title.TC_ != null;
const originalPrice = hasOriginal ? EstMath.num(title.TC_) : null;
const saved = previous.get(identities.current[index]);
previous.delete(identities.current[index]);
const hasOriginal = Boolean(saved) || title.TC_ != null;
const originalPrice = saved
? EstMath.num(saved.TC)
: hasOriginal
? EstMath.num(title.TC_)
: null;
const difference = hasOriginal ? appliedPrice - originalPrice : null;
const statusKey = !hasOriginal
? "new"
: Math.abs(difference) > 1e-6
: Math.abs(difference) > 1e-6 ||
(saved &&
["Name", "Size", "Unite"].some(
(field) => (saved[field] || "") !== (title[field] || ""),
))
? "changed"
: "unchanged";
return {
@@ -186,6 +214,20 @@ const EstMachine = (() => {
status: labels[statusKey] || statusKey,
};
});
for (const title of previous.values())
result.push({
index: null,
Code: title.Code || "",
Name: title.Name || "",
Size: title.Size || "",
Unite: title.Unite || "",
originalTC: EstMath.num(title.TC),
TC: 0,
difference: -EstMath.num(title.TC),
status: labels.deleted,
canRecover: oldGroups.has(title.Code),
});
return result;
}
function applyRowPrice() {
@@ -200,6 +242,77 @@ const EstMachine = (() => {
);
}
function ensureRestoredIds(saved, existing) {
const ids = saved.map((row) => row.PkID).filter((id) => id != null);
const unique = new Set(ids);
if (unique.size !== ids.length || existing.some((row) => unique.has(row.PkID)))
throw Error("기존 호표의 식별자와 충돌하여 복원할 수 없습니다.");
}
function restoreGroup(project, code) {
const current = project.tables?.MachMainDat || [],
baseline = project.baseline?.tables?.MachMainDat;
if (!Array.isArray(baseline))
throw Error("기계 호표의 당초 구성이 없습니다. 당초를 다시 확정하세요.");
const count = (rows) => rows.filter((row) => Number(row.Index) > 0 && row.Code === code).length;
if (!code || count(current) !== 1 || count(baseline) !== 1)
throw Error("복원할 기계 호표가 없거나 코드가 중복됩니다.");
const from = EstNative.groups(baseline).get(code),
to = EstNative.groups(current).get(code);
ensureRestoredIds(from.rows, [
...current.slice(0, to.start),
...current.slice(to.start + to.rows.length),
]);
return [
...current.slice(0, to.start),
...structuredClone(from.rows),
...current.slice(to.start + to.rows.length),
];
}
function recoverGroup(project, code) {
const baseline = project.baseline?.tables;
const original = baseline?.MachMainDat,
oldTitles = baseline?.MachTitleDat;
const current = project.tables.MachMainDat,
titles = project.tables.MachTitleDat;
if (!Array.isArray(original) || !Array.isArray(oldTitles))
throw Error("기계 호표의 당초 구성이 없습니다.");
const originals = original.filter((row) => Number(row.Index) > 0 && row.Code === code);
const savedTitles = oldTitles.filter((row) => row.Code === code);
if (
!code ||
originals.length !== 1 ||
savedTitles.length !== 1 ||
current.some((row) => Number(row.Index) > 0 && row.Code === code) ||
titles.some((row) => row.Code === code)
)
throw Error("복원할 호표가 없거나 코드가 중복됩니다.");
const group = EstNative.groups(original).get(code).rows;
ensureRestoredIds(group, current);
ensureRestoredIds(savedTitles, titles);
const position = (saved, existing, isHeader) => {
const at = saved.findIndex((row) => isHeader(row) && row.Code === code);
const positions = new Map();
existing.forEach((row, index) => {
if (isHeader(row) && !positions.has(row.Code)) positions.set(row.Code, index);
});
for (const row of saved.slice(at + 1))
if (isHeader(row) && positions.has(row.Code)) return positions.get(row.Code);
return existing.length;
};
const rowAt = position(original, current, (row) => Number(row.Index) > 0);
const titleAt = position(oldTitles, titles, () => true);
return {
rows: [...current.slice(0, rowAt), ...structuredClone(group), ...current.slice(rowAt)],
titles: [
...titles.slice(0, titleAt),
structuredClone(savedTitles[0]),
...titles.slice(titleAt),
],
};
}
function render() {
const { state, esc, number, byId } = EstApp,
isChange = state.view === "machine-change",
@@ -209,15 +322,46 @@ const EstMachine = (() => {
value = (v) => (v == null ? "" : number(v));
if (isChange) {
byId("work-content").innerHTML =
`<div class="business-tools"><b>기계경비 설계변경</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-machine-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-machine-page="1" ${(page + 1) * 100 >= rows.length ? "disabled" : ""}>다음</button></div><table class="sheet"><thead><tr><th>구분</th><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>당초 금액</th><th>변경 금액</th><th>증감</th><th>작업</th></tr></thead><tbody>${visible.map((row) => `<tr><td>${esc(row.status)}</td><td>${esc(row.Code)}</td><td>${esc(row.Name)}</td><td>${esc(row.Size)}</td><td>${esc(row.Unite)}</td><td>${value(row.originalTC)}</td><td>${value(row.TC)}</td><td>${value(row.difference)}</td><td><button data-machine-group="${esc(row.Code)}">호표 열기</button></td></tr>`).join("")}</tbody></table>`;
`<div class="business-tools"><b>기계경비 설계변경</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-machine-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-machine-page="1" ${(page + 1) * 100 >= rows.length ? "disabled" : ""}>다음</button></div><table class="sheet"><thead><tr><th>구분</th><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>당초 금액</th><th>변경 금액</th><th>증감</th><th>작업</th></tr></thead><tbody>${visible.map((row) => `<tr><td>${esc(row.status)}</td><td>${esc(row.Code)}</td><td>${esc(row.Name)}</td><td>${esc(row.Size)}</td><td>${esc(row.Unite)}</td><td>${value(row.originalTC)}</td><td>${value(row.TC)}</td><td>${value(row.difference)}</td><td>${row.index == null ? (row.canRecover ? `<button data-machine-recover="${esc(row.Code)}">당초 호표 복원</button>` : "") : `<button data-machine-group="${esc(row.Code)}">호표 열기</button>`}</td></tr>`).join("")}</tbody></table>`;
return;
}
byId("work-content").innerHTML =
`<div class="business-tools"><b>기계경비 사용 내역</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-machine-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-machine-page="1" ${(page + 1) * 100 >= rows.length ? "disabled" : ""}>다음</button></div><p>기계경비 호표의 산출 단가와 당초 계약 단가를 비교합니다. (기계 호표 단가는 MachMainDat 산출 공식에 의해 결정되며 당초값 덮어쓰기는 지원하지 않습니다.)</p><table class="sheet"><thead><tr><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>적용 단가</th><th>당초 단가</th><th>차이</th><th>상태</th><th>노무비</th><th>재료비</th><th>경비</th><th>작업</th></tr></thead><tbody>${visible.map((row) => `<tr><td>${esc(row.Code)}</td><td>${esc(row.Name)}</td><td>${esc(row.Size)}</td><td>${esc(row.Unite)}</td><td>${value(row.appliedPrice)}</td><td>${value(row.originalPrice)}</td><td>${value(row.difference)}</td><td>${esc(row.status)}</td><td>${value(row.NC)}</td><td>${value(row.GC)}</td><td>${value(row.JC)}</td><td><button data-machine-group="${esc(row.Code)}">호표 열기</button></td></tr>`).join("")}</tbody></table>`;
`<div class="business-tools"><b>기계경비 사용 내역</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-machine-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-machine-page="1" ${(page + 1) * 100 >= rows.length ? "disabled" : ""}>다음</button></div><p>자동계산이 켜져 있으면 당초 산식 복원 후 현재 기초단가로 다시 계산합니다.</p><table class="sheet"><thead><tr><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>적용 단가</th><th>당초 단가</th><th>차이</th><th>상태</th><th>노무비</th><th>재료비</th><th>경비</th><th>작업</th></tr></thead><tbody>${visible.map((row) => `<tr><td>${esc(row.Code)}</td><td>${esc(row.Name)}</td><td>${esc(row.Size)}</td><td>${esc(row.Unite)}</td><td>${value(row.appliedPrice)}</td><td>${value(row.originalPrice)}</td><td>${value(row.difference)}</td><td>${esc(row.status)}</td><td>${value(row.NC)}</td><td>${value(row.GC)}</td><td>${value(row.JC)}</td><td><button data-machine-group="${esc(row.Code)}">호표 열기</button>${row.canRestore ? ` <button data-machine-restore="${esc(row.Code)}">당초 산식 복원</button>` : ""}</td></tr>`).join("")}</tbody></table>`;
}
function install() {
document.addEventListener("click", (event) => {
const recover = event.target.closest("[data-machine-recover]");
if (recover) {
try {
const result = recoverGroup(EstApp.state.project, recover.dataset.machineRecover);
if (confirm("삭제한 기계 호표를 당초 구성으로 복원하시겠습니까?"))
EstApp.change(() => {
EstApp.state.project.tables.MachMainDat = result.rows;
EstApp.state.project.tables.MachTitleDat = result.titles;
});
} catch (error) {
EstApp.state.notice = error.message;
EstApp.render();
}
return;
}
const restore = event.target.closest("[data-machine-restore]");
if (restore) {
try {
const rows = restoreGroup(EstApp.state.project, restore.dataset.machineRestore);
if (
confirm(
"당초 기계 호표 구성을 복원하시겠습니까? 자동계산이 켜져 있으면 현재 기초단가로 다시 계산합니다.",
)
)
EstApp.change(() => (EstApp.state.project.tables.MachMainDat = rows));
} catch (error) {
EstApp.state.notice = error.message;
EstApp.render();
}
return;
}
const step = event.target.closest("[data-machine-page]");
if (step) {
page += Number(step.dataset.machinePage);
@@ -232,5 +376,15 @@ const EstMachine = (() => {
});
}
return { calculate, usage, changes, applyRowPrice, applyAllPrices, render, install };
return {
calculate,
usage,
changes,
applyRowPrice,
applyAllPrices,
restoreGroup,
recoverGroup,
render,
install,
};
})();