wip(est-test): 개발 중 저장 — 전역 깃 싱크(요구 686-2) · tools/est-test · resources/tester
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
const EstIssues = (() => {
|
||||
let page = 0;
|
||||
let page = 0,
|
||||
tableFilter = "all",
|
||||
typeFilter = "all";
|
||||
const grouped = new Set(["HeadMainDat", "CostMainDat", "MachMainDat"]);
|
||||
const kinds = {
|
||||
5: "StuffcDat",
|
||||
@@ -26,10 +28,26 @@ const EstIssues = (() => {
|
||||
if (list.length > 1)
|
||||
for (const { index } of list) issues.push({ table, index, code, message: "코드 중복" });
|
||||
}
|
||||
for (const table of ["EstMainDat", "HeadMainDat", "MachMainDat"]) {
|
||||
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(Number(row.LinkIndex));
|
||||
const codes = sources.get(link);
|
||||
if (!codes) return;
|
||||
const matches = codes.get(String(row.Code)) || [];
|
||||
if (!matches.length)
|
||||
@@ -37,7 +55,7 @@ const EstIssues = (() => {
|
||||
else if (matches.length > 1)
|
||||
issues.push({ table, index, code: row.Code, message: "연결 코드 중복" });
|
||||
else
|
||||
for (const field of ["Name", "Size"]) {
|
||||
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))
|
||||
@@ -45,13 +63,95 @@ const EstIssues = (() => {
|
||||
table,
|
||||
index,
|
||||
code: row.Code,
|
||||
message: field === "Name" ? "연결 품명 차이" : "연결 규격 차이",
|
||||
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+) /),
|
||||
@@ -69,15 +169,108 @@ const EstIssues = (() => {
|
||||
}
|
||||
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,
|
||||
issues = scan(state.project);
|
||||
page = Math.max(0, Math.min(page, Math.ceil(issues.length / 100) - 1));
|
||||
const rows = issues.slice(page * 100, page * 100 + 100);
|
||||
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><span>${issues.length}건 · ${page + 1}/${Math.max(1, Math.ceil(issues.length / 100))}쪽</span><button data-issue-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-issue-page="1" ${(page + 1) * 100 >= issues.length ? "disabled" : ""}>다음</button></div><p>연결 품명·규격 차이는 확인이 필요한 항목으로 표시합니다.</p><table class="sheet"><thead><tr><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.table ? `<button data-issue-table="${esc(item.table)}" data-issue-row="${item.index}">행 열기</button>` : ""}</td></tr>`).join("")}</tbody></table>`;
|
||||
`<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) {
|
||||
@@ -85,6 +278,72 @@ const EstIssues = (() => {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user