Files

375 lines
16 KiB
JavaScript
Raw Permalink 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.
const EstIssues = (() => {
let page = 0,
tableFilter = "all",
typeFilter = "all";
const grouped = new Set(["HeadMainDat", "CostMainDat", "MachMainDat"]);
const kinds = {
5: "StuffcDat",
6: "LaborcDat",
21: "SilJukDat",
101: "EstiMateDat",
103: "HeadMainDat",
104: "CostMainDat",
105: "MachMainDat",
};
function scan(project) {
const issues = [],
sources = new Map();
for (const [kind, table] of Object.entries(kinds)) {
const codes = new Map();
(project.tables[table] || []).forEach((row, index) => {
if (!row.Code || (grouped.has(table) && !(Number(row.Index) > 0))) return;
const list = codes.get(String(row.Code)) || [];
list.push({ row, index });
codes.set(String(row.Code), list);
});
sources.set(Number(kind), codes);
for (const [code, list] of codes)
if (list.length > 1)
for (const { index } of list) issues.push({ table, index, code, message: "코드 중복" });
}
for (const table of ["EstMainDat", "HeadMainDat", "MachMainDat", "CostMainDat"]) {
(project.tables[table] || []).forEach((row, index) => {
const link = Number(row.LinkIndex);
if (
Number(row.Level || 0) === 0 &&
Number(row.Index || 0) === 0 &&
[52, 57, 58, 62, 67].includes(link) &&
row.Unite &&
!["%", "%"].includes(String(row.Unite).trim())
) {
issues.push({
table,
index,
code: row.Code,
message: "단위 백분율(%) 불일치",
rowUnite: row.Unite,
});
}
if (Number(row.Level) > 0 || Number(row.Index) > 0 || !row.Code) return;
const codes = sources.get(link);
if (!codes) return;
const matches = codes.get(String(row.Code)) || [];
if (!matches.length)
issues.push({ table, index, code: row.Code, message: "연결 자료 없음" });
else if (matches.length > 1)
issues.push({ table, index, code: row.Code, message: "연결 코드 중복" });
else
for (const field of ["Name", "Size", "Unite"]) {
const a = row[field],
b = matches[0].row[field];
if (a && b && EstNative.normalize(a) !== EstNative.normalize(b))
issues.push({
table,
index,
code: row.Code,
message:
field === "Name"
? "연결 품명 차이"
: field === "Size"
? "연결 규격 차이"
: "연결 단위 차이",
sourceName: matches[0].row.Name,
sourceSize: matches[0].row.Size,
sourceUnite: matches[0].row.Unite,
});
}
});
}
// 수량산출서 및 직접 수식표 항목의 내역 연결(TargetKey 또는 TargetCode/LinkCode) 검사:
// 실제 totals() 연결 규칙과 일치하도록:
// 1) item.TargetKey 가 있으면 row.PkID != null && `id:${row.PkID}` === item.TargetKey 우선 매칭
// 2) 없으면 code = item.TargetCode ?? item.LinkCode 로 row.Code === code 매칭
// 3) level > 0 (공종/비고행) 제외
const sourcesToCheck = [];
(project.quantitySheets || []).forEach((sheet) => {
(sheet.items || []).forEach((item, itemIndex) => {
sourcesToCheck.push({ item, sheetCode: sheet.Code, itemIndex });
});
});
(project.quantities || []).forEach((item, itemIndex) => {
sourcesToCheck.push({ item, sheetCode: "manual", itemIndex });
});
const estRows = (project.tables.EstMainDat || []).map((row, index) => ({ row, index }));
sourcesToCheck.forEach(({ item, sheetCode, itemIndex }) => {
const code = item.TargetCode ?? item.LinkCode;
if (!code && !item.TargetKey) {
issues.push({
table: "quantity",
sheetCode,
index: itemIndex,
code: item.Code,
message: "수량 내역 미연결",
unlinked: true,
item,
});
return;
}
const targets = estRows.filter(
({ row }) =>
!Number(row.Level) &&
(item.TargetKey
? row.PkID != null && `id:${row.PkID}` === item.TargetKey
: row.Code === code),
);
if (!targets.length) {
issues.push({
table: "quantity",
sheetCode,
index: itemIndex,
code: item.Code,
message: `수량 대상 내역 없음: ${item.TargetKey || code}`,
unlinked: true,
item,
});
} else if (targets.length > 1) {
issues.push({
table: "quantity",
sheetCode,
index: itemIndex,
code: item.Code,
message: `수량 대상 내역 중복: ${item.TargetKey || code}`,
});
}
});
const costs = project.tables.CostMainDat || [],
costGroups = EstNative.groups(costs);
if (typeof EstWonga !== "undefined" && typeof EstWonga.scan === "function") {
const direct = project.calculation?.direct || {};
const bases = {
NC: Number(direct.NC) || 0,
GC: Number(direct.GC) || 0,
JC: Number(direct.JC) || 0,
SC: Number(direct.SC) || 0,
TC: Number(direct.TC) || 0,
노무비: Number(direct.NC) || 0,
재료비: Number(direct.GC) || 0,
경비: Number(direct.JC) || 0,
실적: Number(direct.SC) || 0,
직접공사비: Number(direct.TC) || 0,
};
const wongaIssues = EstWonga.scan(project.tables?.Wonga, project.options || {}, bases);
issues.push(...wongaIssues);
}
for (const message of project.calculation?.errors || []) {
const text = String(message),
match = text.match(/^(.+):(\d+) /),
code = match?.[1],
group = code && costGroups.get(code),
offset = match ? Number(match[2]) - 1 : -1,
unique = group && sources.get(104)?.get(code)?.length === 1,
index = unique && offset >= 0 && offset < group.rows.length ? group.start + offset : -1;
issues.push({
table: index >= 0 ? "CostMainDat" : "",
index,
code: index >= 0 ? code : "",
message: text,
});
}
return issues;
}
function targetSelect(project, item, sheetCode, itemIndex) {
const { esc } = EstApp;
const rows = (project.tables?.EstMainDat || []).filter(
(row) => row.PkID != null && !Number(row.Level),
);
return `<select data-issue-target-select="${esc(sheetCode)}" data-issue-item-index="${itemIndex}" aria-label="연결 내역 선택"><option value="">내역 행 선택</option>${rows
.map((row) => {
const key = `id:${row.PkID}`;
return `<option value="${esc(key)}" ${item.TargetKey === key ? "selected" : ""}>${esc(row.Name || "내역")} · ${esc(row.Code || "코드 없음")} · ${esc(row.PkID)}</option>`;
})
.join("")}</select>`;
}
function render() {
const { state, esc, byId, titles } = EstApp,
allIssues = scan(state.project);
const tables = [...new Set(allIssues.map((i) => i.table || ""))];
const messages = [...new Set(allIssues.map((i) => i.message))];
const filtered = allIssues.filter((i) => {
if (tableFilter !== "all" && (i.table || "") !== tableFilter) return false;
if (typeFilter !== "all" && i.message !== typeFilter) return false;
return true;
});
page = Math.max(0, Math.min(page, Math.ceil(filtered.length / 100) - 1));
const rows = filtered.slice(page * 100, page * 100 + 100);
const tableSelect = `<label>자료 <select id="issue-table-filter"><option value="all">전체 자료</option>${tables
.map(
(t) =>
`<option value="${esc(t)}" ${tableFilter === t ? "selected" : ""}>${esc(titles[t] || t || "전체 계산")}</option>`,
)
.join("")}</select></label>`;
const typeSelect = `<label>내용 <select id="issue-type-filter"><option value="all">전체 유형</option>${messages
.map(
(m) => `<option value="${esc(m)}" ${typeFilter === m ? "selected" : ""}>${esc(m)}</option>`,
)
.join("")}</select></label>`;
byId("work-content").innerHTML =
`<div class="business-tools"><b>자료 오류 검색</b>${tableSelect} ${typeSelect}<span>검색 ${filtered.length}건 (전체 ${allIssues.length}건) · ${page + 1}/${Math.max(1, Math.ceil(filtered.length / 100))}쪽</span><button data-issue-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-issue-page="1" ${(page + 1) * 100 >= filtered.length ? "disabled" : ""}>다음</button>${
filtered.some(
(i) => i.sourceName && ["연결 품명 차이", "연결 규격 차이"].includes(i.message),
)
? ' <button data-issue-sync-all="1">불일치 원천 명칭/규격 일괄 반영</button>'
: ""
}</div><p>연결 품명·규격 차이는 확인 후 개별 또는 일괄로 반영할 수 있으며, 단위 차이는 수량·단가 환산 보호를 위해 개별 확인 시에만 반영됩니다.</p><table class="sheet"><thead><tr><th>자료</th><th>행</th><th>코드</th><th>내용</th><th>원천 실제 명칭</th><th>이동 및 조작</th></tr></thead><tbody>${rows
.map(
(item) =>
`<tr><td>${esc(titles[item.table] || item.table || "전체 계산")}</td><td>${item.index >= 0 ? item.index + 1 : ""}</td><td>${esc(item.code)}</td><td>${esc(item.message)}</td><td>${
item.sourceName
? `${esc(item.sourceName)}${item.sourceSize ? " · " + esc(item.sourceSize) : ""}${item.sourceUnite ? " · " + esc(item.sourceUnite) : ""}`
: ""
}</td><td>${
item.table === "quantity"
? `<button data-issue-quantity="${esc(item.sheetCode || "")}" data-issue-row="${item.index}">산출서 열기</button>${
item.unlinked
? " " + targetSelect(state.project, item.item, item.sheetCode, item.index)
: ""
}`
: item.table
? `<button data-issue-table="${esc(item.table)}" data-issue-row="${item.index}">행 열기</button>${
item.sourceName &&
["연결 품명 차이", "연결 규격 차이", "연결 단위 차이"].includes(item.message)
? ` <button data-issue-sync-row="${esc(item.table)}" data-issue-sync-index="${item.index}" data-issue-sync-msg="${esc(item.message)}">${
item.message === "연결 단위 차이" ? "원천단위 반영" : "원천명칭 반영"
}</button>`
: ""
}`
: ""
}</td></tr>`,
)
.join("")}</tbody></table>`;
}
function install() {
document.addEventListener("change", (event) => {
if (event.target.id === "issue-table-filter") {
tableFilter = event.target.value;
page = 0;
render();
return;
}
if (event.target.id === "issue-type-filter") {
typeFilter = event.target.value;
page = 0;
render();
return;
}
const select = event.target.closest("[data-issue-target-select]");
if (!select) return;
const { state } = EstApp,
sheetCode = select.dataset.issueTargetSelect,
itemIndex = Number(select.dataset.issueItemIndex);
const item =
sheetCode === "manual"
? state.project.quantities?.[itemIndex]
: state.project.quantitySheets?.find((s) => s.Code === sheetCode)?.items?.[itemIndex];
if (!item) return;
const row = (state.project.tables.EstMainDat || []).find(
(r) => r.PkID != null && `id:${r.PkID}` === select.value && !Number(r.Level),
);
EstApp.change(() => {
item.TargetKey = row ? select.value : "";
item.TargetCode = row?.Code || "";
}, false);
});
document.addEventListener("click", (event) => {
const step = event.target.closest("[data-issue-page]");
if (step) {
page += Number(step.dataset.issuePage);
render();
return;
}
const qNode = event.target.closest("[data-issue-quantity]");
if (qNode) {
const sheetCode = qNode.dataset.issueQuantity;
EstApp.state.view = "quantity";
EstApp.state.quantitySheet = sheetCode || "manual";
EstApp.navigate("quantity");
return;
}
const syncAllBtn = event.target.closest("[data-issue-sync-all]");
if (syncAllBtn) {
const { state } = EstApp;
// 현재 필터링 조건에 일치하는 품명·규격 불일치 항목만 일괄 반영 대상
const currentFiltered = scan(state.project).filter((i) => {
if (tableFilter !== "all" && (i.table || "") !== tableFilter) return false;
if (typeFilter !== "all" && i.message !== typeFilter) return false;
return i.sourceName && (i.message === "연결 품명 차이" || i.message === "연결 규격 차이");
});
if (!currentFiltered.length) return;
EstApp.change(() => {
for (const issue of currentFiltered) {
const row = state.project.tables[issue.table]?.[issue.index];
if (!row) continue;
if (issue.message === "연결 품명 차이" && issue.sourceName) {
row.Name = issue.sourceName;
} else if (issue.message === "연결 규격 차이") {
if (issue.sourceSize && issue.sourceSize.trim() !== "") {
row.Size = issue.sourceSize;
}
}
}
});
render();
return;
}
const syncRowBtn = event.target.closest("[data-issue-sync-row]");
if (syncRowBtn) {
const { state } = EstApp;
const targetTable = syncRowBtn.dataset.issueSyncRow,
targetIndex = Number(syncRowBtn.dataset.issueSyncIndex),
targetMsg = syncRowBtn.dataset.issueSyncMsg;
const issue = scan(state.project).find(
(i) =>
i.table === targetTable &&
i.index === targetIndex &&
(!targetMsg || i.message === targetMsg) &&
i.sourceName,
);
if (!issue) return;
EstApp.change(() => {
const row = state.project.tables[targetTable]?.[targetIndex];
if (!row) return;
if (issue.message === "연결 품명 차이" && issue.sourceName) {
row.Name = issue.sourceName;
} else if (issue.message === "연결 규격 차이") {
if (issue.sourceSize && issue.sourceSize.trim() !== "") {
row.Size = issue.sourceSize;
}
} else if (issue.message === "연결 단위 차이") {
if (issue.sourceUnite !== undefined) {
row.Unite = issue.sourceUnite;
}
}
});
render();
return;
}
const node = event.target.closest("[data-issue-table]");
if (!node) return;
const table = node.dataset.issueTable,
index = Number(node.dataset.issueRow),
rows = EstApp.state.project.tables[table] || [];
if (!rows[index]) return;
let group = "";
if (grouped.has(table))
for (const [code, item] of EstNative.groups(rows))
if (index >= item.start && index < item.start + item.rows.length) {
group = code;
break;
}
if (grouped.has(table) && !group) {
EstApp.state.notice = "묶음 코드가 중복되거나 해당 행의 묶음이 없어 이동할 수 없습니다.";
EstApp.render();
return;
}
EstApp.navigate(table, group);
const visible = EstApp.visibleRows(),
position = visible.findIndex((item) => item.index === index);
if (position >= 0) EstApp.state.page = Math.floor(position / 100);
EstApp.state.selection = new Set([index]);
EstApp.render();
});
}
return { scan, render, install };
})();