Files
Aislo/tools/est-test/native-cleanup.js
eomsangdonandClaude Opus 5.5 27869d7b81 feat(est-test): 무관자료 삭제와 내역서순 정리
- 원본 처리 근거대로 내역서가 쓰는 자료만 하위까지 따라가 남기고 나머지 삭제
- 내역서 첫 등장 순서 재배열, 묶음 앞 빈 줄 정리, 미연결 수량산출서 삭제

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

148 lines
6.2 KiB
JavaScript

// 원본 무관자료 삭제(TfrmEstMain mnuNotLinkDelete 0x10cd2a8·mnuNotLinkDelSort 0x10cd2b8 → f039f0,
// mnuNotLinkMulDelete 0x10cd2c8 → 1116db0). 내역서가 쓰는 자료(LinkIndex 5·6·21·101·103·104·105, Code 일치)를
// 일위대가·단가산출·기계경비 묶음 안까지 따라가 표시하고 나머지 기초·묶음 자료를 지운다. 내역서·원가·적용기준은 지우지 않는다.
// 내역서순: 내역서 첫 등장 순서(중첩은 깊이 우선)로 표를 다시 늘어놓고, 코드는 바꾸지 않는다.
// 설계변경 당초 코드(Code_)도 함께 따라간다. 원본은 내역서→일위→단가→기계 차례 한 번 훑기라 기계경비 안에서만
// 쓰인 일위대가의 하위를 놓칠 수 있으나, 웹은 끝까지 따라가 쓰는 자료를 지우지 않는다.
const EstCleanup = (() => {
const flat = { 5: "StuffcDat", 6: "LaborcDat", 21: "SilJukDat", 101: "EstiMateDat" };
const grouped = {
103: ["HeadTitleDat", "HeadMainDat"],
104: ["CostTitleDat", "CostMainDat"],
105: ["MachTitleDat", "MachMainDat"],
};
const num = (value) => Number(value) || 0;
const text = (value) => String(value ?? "").trim();
function used(project) {
const order = new Map(Object.values(flat).map((t) => [t, []])),
seen = new Set(),
groups = {};
for (const [, [, main]] of Object.entries(grouped)) {
order.set(main, []);
groups[main] = EstNative.groups(project.tables[main] || []);
}
function visit(rows) {
for (const row of rows)
for (const code of new Set([row.Code, row.Code_].filter(Boolean))) {
const kind = num(row.LinkIndex),
id = `${kind}:${code}`;
if (seen.has(id)) continue;
if (flat[kind]) {
if ((project.tables[flat[kind]] || []).some((r) => r.Code === code)) {
seen.add(id);
order.get(flat[kind]).push(code);
}
} else if (grouped[kind]) {
const [, main] = grouped[kind],
group = groups[main].get(code);
if (!group) continue;
seen.add(id);
order.get(main).push(code);
visit(group.rows.slice(1));
}
}
}
visit(project.tables.EstMainDat || []);
return order;
}
function blankRow(table, row) {
if (table === "CostMainDat") return !text(row.LineData);
return (
!text(row.Name) &&
!text(row.Size) &&
!text(row.Unite) &&
!(typeof row.Bigo === "string" && text(row.Bigo)) &&
!num(row.GN) &&
!num(row.TC)
);
}
// 빈 줄 정리(f038ac): 묶음 머리 바로 앞의 빈 행을 지운다.
function trimBeforeHeads(rows, table) {
return rows.filter((row, i) => {
if (!blankRow(table, row) || num(row.Index) > 0) return true;
let next = i + 1;
while (next < rows.length && blankRow(table, rows[next]) && !(num(rows[next].Index) > 0))
next++;
return !(next < rows.length && num(rows[next].Index) > 0);
});
}
function clean(project, sorted) {
const order = used(project),
removed = {};
for (const table of Object.values(flat)) {
const rows = project.tables[table] || [],
keep = order.get(table),
byCode = new Map(rows.map((r) => [r.Code, r]));
const next = sorted
? keep.map((code) => byCode.get(code))
: rows.filter((r) => keep.includes(r.Code));
removed[table] = rows.length - next.length;
project.tables[table] = next;
}
for (const [title, main] of Object.values(grouped)) {
const keep = order.get(main),
groups = EstNative.groups(project.tables[main] || []),
before = groups.size;
const codes = sorted ? keep : [...groups.keys()].filter((code) => keep.includes(code));
project.tables[main] = trimBeforeHeads(
codes.flatMap((code) => groups.get(code)?.rows || []),
main,
);
const titles = project.tables[title] || [],
byCode = new Map(titles.map((r) => [r.Code, r]));
project.tables[title] = sorted
? codes.map((code) => byCode.get(code)).filter(Boolean)
: titles.filter((r) => keep.includes(r.Code));
removed[main] = before - codes.length;
}
return removed;
}
// 수량산출서: 내역 행이 쓰는(MullangIndex 9999·MulCode 앞 조각) 산출서와, 항목이 내역에 연결된 산출서만 남긴다.
function cleanQuantity(project) {
const sheets = project.quantitySheets || [],
estimate = project.tables.EstMainDat || [],
codes = new Set(estimate.map((r) => r.Code).filter(Boolean)),
keys = new Set(estimate.map((r) => r.PkID != null && `id:${r.PkID}`).filter(Boolean)),
named = new Set(
estimate
.filter((r) => num(r.MullangIndex) === 9999 && typeof r.MulCode === "string")
.map((r) => r.MulCode.split("\t")[0]),
);
const keep = sheets.filter(
(sheet) =>
named.has(sheet.Code) ||
(sheet.items || []).some(
(item) =>
(item.TargetKey && keys.has(item.TargetKey)) ||
(item.TargetCode && codes.has(item.TargetCode)),
),
);
project.quantitySheets = keep;
return sheets.length - keep.length;
}
const WARN =
"내역서상에 사용한 데이타(수량산출)만 남기고 나머지는 삭제합니다.\n삭제된 자료는 되살리기로만 복구할 수 있습니다.";
function handle(name, state) {
if (name === "notlink-delete" || name === "notlink-sort") {
if (!confirm(WARN)) return true;
let removed;
EstApp.change((project) => (removed = clean(project, name === "notlink-sort")));
const total = Object.values(removed).reduce((a, b) => a + b, 0);
state.notice = `안 쓰는 자료 ${total}건을 지웠습니다${name === "notlink-sort" ? "(내역서 순서로 정리)" : ""}.`;
} else if (name === "notlink-quantity") {
if (!confirm(WARN)) return true;
let removed;
EstApp.change((project) => (removed = cleanQuantity(project)), false);
state.notice = `내역에 연결되지 않은 수량산출서 ${removed}개를 지웠습니다.`;
} else return false;
return true;
}
return { used, clean, cleanQuantity, trimBeforeHeads, handle };
})();