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 = `
연결 품명·규격 차이는 확인 후 개별 또는 일괄로 반영할 수 있으며, 단위 차이는 수량·단가 환산 보호를 위해 개별 확인 시에만 반영됩니다.
| 자료 | 행 | 코드 | 내용 | 원천 실제 명칭 | 이동 및 조작 |
|---|---|---|---|---|---|
| ${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) ? ` ` : "" }` : "" } |