wip(est-test): 개발 중 저장 — 전역 깃 싱크(요구 686-2) · tools/est-test · resources/tester
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
const EstBasicPrices = (() => {
|
||||
let page = 0;
|
||||
const kinds = {
|
||||
material: {
|
||||
key: "material",
|
||||
table: "StuffcDat",
|
||||
linkIndex: 5,
|
||||
unitField: "GU",
|
||||
costField: "GC",
|
||||
name: "자재단가",
|
||||
},
|
||||
labor: {
|
||||
key: "labor",
|
||||
table: "LaborcDat",
|
||||
linkIndex: 6,
|
||||
unitField: "NU",
|
||||
costField: "NC",
|
||||
name: "노임단가",
|
||||
},
|
||||
};
|
||||
|
||||
function config(view) {
|
||||
if (view === "material" || view === "material-usage" || view === "material-change")
|
||||
return kinds.material;
|
||||
if (view === "labor" || view === "labor-usage" || view === "labor-change") return kinds.labor;
|
||||
return null;
|
||||
}
|
||||
|
||||
function usage(project, kindKey = "material") {
|
||||
const cfg = kinds[kindKey];
|
||||
if (!cfg) throw Error(`알 수 없는 단가 종류: ${kindKey}`);
|
||||
const sources = new Map();
|
||||
for (const row of project.tables[cfg.table] || []) {
|
||||
if (!row.Code) continue;
|
||||
const rows = sources.get(row.Code) || [];
|
||||
rows.push(row);
|
||||
sources.set(row.Code, rows);
|
||||
}
|
||||
return (project.tables.EstMainDat || []).flatMap((row, index) => {
|
||||
if (Number(row.Level) > 0 || Number(row.LinkIndex) !== cfg.linkIndex) return [];
|
||||
const matches = sources.get(row.Code) || [];
|
||||
const source =
|
||||
matches.length === 1 ? EstNative.price(structuredClone(matches[0]), cfg.linkIndex) : null;
|
||||
const appliedPrice =
|
||||
row.TU == null
|
||||
? EstMath.units.reduce((sum, field) => sum + EstMath.num(row[field]), 0)
|
||||
: EstMath.num(row.TU);
|
||||
const sourcePrice = source ? EstMath.num(source.TC) : null;
|
||||
const canApply =
|
||||
matches.length === 1 && sourcePrice != null && Math.abs(appliedPrice - sourcePrice) > 1e-6;
|
||||
return [
|
||||
{
|
||||
index,
|
||||
Code: row.Code || "",
|
||||
Name: row.Name || "",
|
||||
Size: row.Size || "",
|
||||
Unite: row.Unite || "",
|
||||
GN: EstMath.num(row.GN),
|
||||
appliedPrice,
|
||||
sourcePrice,
|
||||
difference: source ? appliedPrice - sourcePrice : null,
|
||||
TC: EstMath.num(row.TC),
|
||||
status: !matches.length ? "연결 없음" : matches.length > 1 ? "코드 중복" : "연결됨",
|
||||
canApply,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function changes(project, kindKey = "material") {
|
||||
const cfg = kinds[kindKey];
|
||||
if (!cfg) throw Error(`알 수 없는 단가 종류: ${kindKey}`);
|
||||
const labels = { new: "신규", deleted: "삭제", changed: "변경", unchanged: "동일" };
|
||||
return EstBusiness.changes(project)
|
||||
.filter(
|
||||
(row) =>
|
||||
Number(row.LinkIndex) === cfg.linkIndex ||
|
||||
Number(row.originalLinkIndex) === cfg.linkIndex,
|
||||
)
|
||||
.map((row) => ({ ...row, status: labels[row.status] || row.status }));
|
||||
}
|
||||
|
||||
function applyRowPrice(project, index, kindKey = "material") {
|
||||
const cfg = kinds[kindKey];
|
||||
if (!cfg) throw Error(`알 수 없는 단가 종류: ${kindKey}`);
|
||||
index = Number(index);
|
||||
const row = project.tables?.EstMainDat?.[index];
|
||||
if (!row || Number(row.Level) > 0 || Number(row.LinkIndex) !== cfg.linkIndex)
|
||||
throw Error(`${cfg.name} 내역 행을 찾을 수 없습니다.`);
|
||||
const sources = (project.tables?.[cfg.table] || []).filter(
|
||||
(s) => s.Code && s.Code === row.Code,
|
||||
);
|
||||
if (!sources.length) throw Error(`${cfg.name} 원천 자료가 없습니다: ${row.Code}`);
|
||||
if (sources.length > 1) throw Error(`${cfg.name} 원천 코드가 중복되었습니다: ${row.Code}`);
|
||||
const source = EstNative.price(structuredClone(sources[0]), cfg.linkIndex);
|
||||
const sourcePrice = EstMath.num(source?.TC);
|
||||
if (!Number.isFinite(sourcePrice)) throw Error("기초 단가를 계산할 수 없습니다.");
|
||||
|
||||
// SCheck 재분류가 설정되어 있는 경우 비목 보존
|
||||
const scheck = Number(row.SCheck);
|
||||
if (scheck > 0) {
|
||||
const targetUnit = { 1: "GU", 2: "NU", 3: "JU" }[scheck];
|
||||
for (const u of EstMath.units) row[u] = u === targetUnit ? sourcePrice : 0;
|
||||
} else {
|
||||
// 자재단가는 GU, 노임단가는 NU에 단가 할당
|
||||
for (const u of EstMath.units) row[u] = u === cfg.unitField ? sourcePrice : 0;
|
||||
}
|
||||
|
||||
// AutoCalc가 비활성화된 수동계산 모드에서는 기존 금액을 보존하고, 활성화된 경우에만 재계산
|
||||
if (project.options?.AutoCalc !== false) {
|
||||
EstMath.amounts(row, project.options?.EstDigits || 0, project.options?.TotalFit);
|
||||
} else {
|
||||
row.TU = EstMath.units.reduce((sum, key) => sum + EstMath.num(row[key]), 0);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAllPrices(project, kindKey = "material") {
|
||||
const cfg = kinds[kindKey];
|
||||
if (!cfg) throw Error(`알 수 없는 단가 종류: ${kindKey}`);
|
||||
const list = usage(project, kindKey).filter((item) => item.canApply);
|
||||
if (!list.length) throw Error("적용할 기초 단가 차이가 없습니다.");
|
||||
for (const item of list) applyRowPrice(project, item.index, kindKey);
|
||||
return list.length;
|
||||
}
|
||||
|
||||
function render() {
|
||||
const { state, esc, number, byId } = EstApp;
|
||||
const cfg = config(state.view);
|
||||
if (!cfg) return;
|
||||
const isChange = state.view.endsWith("-change");
|
||||
const rows = isChange ? changes(state.project, cfg.key) : usage(state.project, cfg.key);
|
||||
page = Math.max(0, Math.min(page, Math.ceil(rows.length / 100) - 1));
|
||||
const visible = rows.slice(page * 100, page * 100 + 100),
|
||||
value = (v) => (v == null ? "" : number(v));
|
||||
|
||||
if (isChange) {
|
||||
byId("work-content").innerHTML =
|
||||
`<div class="business-tools"><b>${cfg.name} 설계변경</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-basic-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-basic-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>${value(row.originalGN)}</td><td>${value(row.GN)}</td><td>${value(row.originalTC)}</td><td>${value(row.TC)}</td><td>${value(row.difference)}</td><td>${row.index == null ? "" : `<button data-basic-row="${row.index}">내역 열기</button>`}</td></tr>`).join("")}</tbody></table>`;
|
||||
return;
|
||||
}
|
||||
const canApplyCount = rows.filter((r) => r.canApply).length;
|
||||
byId("work-content").innerHTML =
|
||||
`<div class="business-tools"><b>${cfg.name} 사용 내역</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-basic-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-basic-page="1" ${(page + 1) * 100 >= rows.length ? "disabled" : ""}>다음</button><button data-basic-apply-all="${cfg.key}" ${canApplyCount ? "" : "disabled"}>기초단가 일괄 적용 (${canApplyCount}건)</button></div><p>내역에 적용된 단가와 현재 ${cfg.name} 자료를 비교합니다.</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></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.GN)}</td><td>${value(row.appliedPrice)}</td><td>${value(row.sourcePrice)}</td><td>${value(row.difference)}</td><td>${value(row.TC)}</td><td>${esc(row.status)}</td><td><button data-basic-row="${row.index}">내역 열기</button>${row.canApply ? ` <button data-basic-apply-row="${row.index}" data-kind="${cfg.key}">단가 적용</button>` : ""}</td></tr>`).join("")}</tbody></table>`;
|
||||
}
|
||||
|
||||
function install() {
|
||||
document.addEventListener("click", (event) => {
|
||||
const applyAll = event.target.closest("[data-basic-apply-all]");
|
||||
if (applyAll) {
|
||||
const kindKey = applyAll.dataset.basicApplyAll;
|
||||
const cfg = kinds[kindKey];
|
||||
try {
|
||||
EstApp.change((p) => {
|
||||
const count = applyAllPrices(p, kindKey);
|
||||
EstApp.state.notice = `${cfg?.name || "기초단가"} ${count}건의 단가를 일괄 적용했습니다.`;
|
||||
});
|
||||
} catch (err) {
|
||||
EstApp.state.notice = err.message;
|
||||
EstApp.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const applyRow = event.target.closest("[data-basic-apply-row]");
|
||||
if (applyRow) {
|
||||
const index = Number(applyRow.dataset.basicApplyRow);
|
||||
const kindKey = applyRow.dataset.kind;
|
||||
try {
|
||||
EstApp.change((p) => {
|
||||
applyRowPrice(p, index, kindKey);
|
||||
EstApp.state.notice = `행 ${index + 1}에 기초단가를 적용했습니다.`;
|
||||
});
|
||||
} catch (err) {
|
||||
EstApp.state.notice = err.message;
|
||||
EstApp.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const step = event.target.closest("[data-basic-page]");
|
||||
if (step) {
|
||||
page += Number(step.dataset.basicPage);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
const node = event.target.closest("[data-basic-row]");
|
||||
if (!node) return;
|
||||
const index = Number(node.dataset.basicRow);
|
||||
if (!EstApp.state.project.tables.EstMainDat[index]) return;
|
||||
EstApp.navigate("EstMainDat");
|
||||
EstApp.state.page = Math.floor(index / 100);
|
||||
EstApp.state.selection = new Set([index]);
|
||||
EstApp.render();
|
||||
});
|
||||
}
|
||||
|
||||
return { usage, changes, render, install, applyRowPrice, applyAllPrices, kinds };
|
||||
})();
|
||||
Reference in New Issue
Block a user