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 ``; } 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 = ``; const typeSelect = ``; byId("work-content").innerHTML = `
자료 오류 검색${tableSelect} ${typeSelect}검색 ${filtered.length}건 (전체 ${allIssues.length}건) · ${page + 1}/${Math.max(1, Math.ceil(filtered.length / 100))}쪽${ filtered.some( (i) => i.sourceName && ["연결 품명 차이", "연결 규격 차이"].includes(i.message), ) ? ' ' : "" }

연결 품명·규격 차이는 확인 후 개별 또는 일괄로 반영할 수 있으며, 단위 차이는 수량·단가 환산 보호를 위해 개별 확인 시에만 반영됩니다.

${rows .map( (item) => ``, ) .join("")}
자료행코드내용원천 실제 명칭이동 및 조작
${esc(titles[item.table] || item.table || "전체 계산")}${item.index >= 0 ? item.index + 1 : ""}${esc(item.code)}${esc(item.message)}${ item.sourceName ? `${esc(item.sourceName)}${item.sourceSize ? " · " + esc(item.sourceSize) : ""}${item.sourceUnite ? " · " + esc(item.sourceUnite) : ""}` : "" }${ item.table === "quantity" ? `${ item.unlinked ? " " + targetSelect(state.project, item.item, item.sheetCode, item.index) : "" }` : item.table ? `${ item.sourceName && ["연결 품명 차이", "연결 규격 차이", "연결 단위 차이"].includes(item.message) ? ` ` : "" }` : "" }
`; } 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 }; })();