Files
Aislo/tools/est-test/native-dongwon-check.js
T
eomsangdonandClaude Opus 5.5 b1c7d3dbc3 feat(est-test): 동원 확인 탭(장비·단산·일위 대조)·내역서 줄별 펼침
- 호표 1 단위 펼친 항목 합과 호표 금액 대조, 10원 넘게 어긋나면 표시
- 당초값·계산값(노무·재료·경비·실적)과 항목 목록
- 동원 계산에 호표 단위 펼치기(roots) 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 19:33:32 +09:00

123 lines
6.4 KiB
JavaScript
Raw 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.
// 원본 동원인원·자재·장비 「확인」·「내역서」 탭(TfrmDongWon Button1~4 — spec_part §4): 동원 계산이 펼친 중간 결과 보기.
// 장비·단산·일위 = 호표마다 1 단위를 동원 항목으로 펼쳐 금액 합을 내고, 호표 금액(TC)과 10원 넘게 어긋나면 표시(1095cdc
// Checked := |TC − Trunc(Σ Cost)| > 10). 행을 고르면 당초값(호표 TC·NC·GC·JC)과 계산값(항목 금액을 노무·재료·경비·실적으로
// 나눠 더한 값)·항목 목록. 내역서 = 내역 줄마다 펼친 항목(점검 없음). 웹 동원 계산은 기계경비를 더 쪼개지 않아 장비는 늘 맞음.
const EstDongwonCheck = (() => {
const num = (v) => Number(v) || 0;
const TITLE = { 103: "HeadTitleDat", 104: "CostTitleDat", 105: "MachTitleDat" };
const KIND = { 5: "자재", 6: "노임", 21: "표준", 101: "견적", 105: "기계" };
// 항목 → 비목: 노임·노무비 금액분 = 노무, 자재·견적·재료비 금액분 = 재료, 기계·경비 = 경비, 표준 = 실적
const part = (item) =>
item.Code === "LZZZZZZZZZZ" || item.kind === 6
? "NC"
: item.Code === "EZZZZZZZZZZ" || item.kind === 105
? "JC"
: item.Code === "SZZZZZZZZZZ" || item.kind === 21
? "SC"
: "GC";
function check(project, kind) {
const titles = (project.tables[TITLE[kind]] || []).filter((t) => t.Code);
const out = EstResources.calculate(
project,
true,
titles.map((t) => ({ kind, code: t.Code, WorkCode: t.Code, WorkName: t.Name })),
);
const items = new Map();
for (const row of out.rows)
(items.get(row.WorkCode) || items.set(row.WorkCode, []).get(row.WorkCode)).push(row);
return {
errors: out.errors,
rows: titles.map((t) => {
const list = items.get(t.Code) || [],
sum = list.reduce((s, r) => s + num(r.amount), 0),
calc = { NC: 0, GC: 0, JC: 0, SC: 0 };
for (const r of list) calc[part(r)] += num(r.amount);
return {
Code: t.Code,
Name: t.Name,
Size: t.Size,
TC: num(t.TC),
NC: num(t.NC),
GC: num(t.GC),
JC: num(t.JC),
sum,
calc,
checked: Math.abs(num(t.TC) - Math.trunc(sum)) > 10,
items: list,
};
}),
};
}
function estimate(project) {
const out = EstResources.calculate(project, true),
groups = new Map();
for (const row of out.rows) {
const key = `${row.WorkCode}\u0000${row.WorkName}`;
if (!groups.has(key)) groups.set(key, { Code: row.WorkCode, Name: row.WorkName, items: [] });
groups.get(key).items.push(row);
}
return { errors: out.errors, rows: [...groups.values()] };
}
let shown = null;
function draw(state) {
const { esc, number } = EstApp,
{ kind, data, pick, only } = shown;
const list = data.rows.map((r, i) => [r, i]).filter(([r]) => !only || r.checked);
const chosen = data.rows[pick];
const head =
kind === "est"
? "<tr><th>코드</th><th>공종</th><th>항목 수</th></tr>"
: "<tr><th>코드</th><th>품명</th><th>규격</th><th>호표 금액</th><th>항목 합</th><th>어긋남</th></tr>";
const body = list
.map(([r, i]) =>
kind === "est"
? `<tr data-dw-pick="${i}" style="cursor:pointer${i === pick ? ";background:#dde8ff" : ""}"><td>${esc(r.Code)}</td><td style="text-align:left">${esc(r.Name)}</td><td>${r.items.length}</td></tr>`
: `<tr data-dw-pick="${i}" style="cursor:pointer${i === pick ? ";background:#dde8ff" : ""}${r.checked ? ";color:#b00" : ""}"><td>${esc(r.Code)}</td><td style="text-align:left">${esc(r.Name)}</td><td>${esc(r.Size ?? "")}</td><td>${number(r.TC)}</td><td>${number(Math.trunc(r.sum))}</td><td>${r.checked ? "✔" : ""}</td></tr>`,
)
.join("");
const detail = chosen
? (kind === "est"
? ""
: `<p>당초값 계 ${number(chosen.TC)} · 노무 ${number(chosen.NC)} · 재료 ${number(chosen.GC)} · 경비 ${number(chosen.JC)}<br>계산값 계 ${number(Math.trunc(chosen.sum))} · 노무 ${number(Math.trunc(chosen.calc.NC))} · 재료 ${number(Math.trunc(chosen.calc.GC))} · 경비 ${number(Math.trunc(chosen.calc.JC))}${chosen.calc.SC ? ` · 실적 ${number(Math.trunc(chosen.calc.SC))}` : ""}</p>`) +
`<table class="sheet"><thead><tr><th>종류</th><th>코드</th><th>품명</th><th>규격</th><th>소요량</th><th>금액</th></tr></thead><tbody>${chosen.items.map((r) => `<tr><td>${esc(KIND[r.kind] || r.kind)}</td><td>${esc(r.Code)}</td><td style="text-align:left">${esc(r.Name)}</td><td>${esc(r.Size ?? "")}</td><td>${number(r.quantity)}</td><td>${number(r.amount)}</td></tr>`).join("")}</tbody></table>`
: "<p>위에서 행을 고르세요.</p>";
const title = { 105: "확인 — 장비", 104: "확인 — 단산", 103: "확인 — 일위", est: "내역서" }[
kind
];
EstApp.showDialog(
`동원인원,자재,장비 · ${title}`,
`${kind === "est" ? "" : `<label><input type="checkbox" data-dw-only ${only ? "checked" : ""}> 어긋난 것만 (${data.rows.filter((r) => r.checked).length}건)</label>`} <span>${data.rows.length}건</span><div style="max-height:260px;overflow:auto"><table class="sheet"><thead>${head}</thead><tbody>${body}</tbody></table></div><div style="max-height:260px;overflow:auto;margin-top:6px">${detail}</div>${data.errors.length ? `<p class="business-note">${esc(data.errors.slice(0, 5).join(" / "))}</p>` : ""}`,
);
}
function install() {
document.addEventListener("click", (event) => {
const tr = event.target.closest?.("[data-dw-pick]");
if (!tr || !shown) return;
shown.pick = Number(tr.dataset.dwPick);
draw(EstApp.state);
});
document.addEventListener("change", (event) => {
if (event.target.dataset?.dwOnly === undefined || !shown) return;
shown.only = event.target.checked;
draw(EstApp.state);
});
}
if (typeof document !== "undefined" && document.addEventListener) install();
function handle(name, state, node) {
if (name !== "dongwon-check") return false;
const kind = node.dataset.kind === "est" ? "est" : Number(node.dataset.kind);
shown = {
kind,
data: kind === "est" ? estimate(state.project) : check(state.project, kind),
pick: -1,
only: false,
};
draw(state);
return true;
}
return { check, estimate, handle };
})();