const EstExtra = (() => { function field(section, index, key, value, type = "number") { return ``; } function button(action, text, extra = "") { return ``; } function table(headers, rows) { return `
| ${h} | `).join("")}
|---|
| ${cell} | `).join("")}
${note}
` : ""}`; } function render(state) { if (state.view === "review") { EstReview.render(state); return; } if (state.view === "schedule") { EstSchedule.render(); return; } if (state.view === "issues") { EstIssues.render(); return; } if (state.view === "attachments") { EstAttachments.render(); return; } if (["standard", "standard-change"].includes(state.view)) { EstStandard.render(); return; } if (["estimate-usage", "estimate-change"].includes(state.view)) { EstEstimate.render(); return; } if (["material-usage", "material-change", "labor-usage", "labor-change"].includes(state.view)) { EstBasicPrices.render(); return; } if (["machine-usage", "machine-change"].includes(state.view)) { EstMachine.render(); return; } const { project, view } = state, { esc, number, byId } = EstApp; let html = ""; if (view === "quantity") { if (project.quantitySheets?.length && state.quantitySheet !== "manual") { EstQuantity.render(state); return; } const rows = EstQuantity.manual(project.quantities).rows.map((row, index) => { return [ field("quantities", index, "Code", row.Code, "text"), field("quantities", index, "Name", row.Name, "text"), field("quantities", index, "TargetCode", row.TargetCode, "text") + EstQuantity.targetSelect(project, row, "manual", index), field("quantities", index, "formula", row.formula, "text"), field("quantities", index, "digits", row.digits ?? 2), esc(row.Error || number(row.GN)), button( "move-quantity", "↑", `data-row="${index}" data-direction="-1" ${index === 0 ? "disabled" : ""}`, ) + button( "move-quantity", "↓", `data-row="${index}" data-direction="1" ${index === project.quantities.length - 1 ? "disabled" : ""}`, ) + button("delete-extra", "삭제", `data-section="quantities" data-row="${index}"`), ]; }); html = EstQuantity.selector(state) + box( "수량산출서", button("add-quantity", "산출 항목 추가") + button("apply-quantity", "내역에 수량 반영"), table(["산출코드", "품명", "연결 내역코드", "수식", "소수자리", "결과", ""], rows), "앞선 산출 항목을 [산출코드]로 참조합니다. 내역 반영은 연결된 코드의 산출값을 합산하여 수량에 반영합니다.", ); } else if (view === "change") { const changes = EstBusiness.changes(project); const page = EstBlockActions.changePage(state, changes); html = box( "설계변경", button("capture-baseline", "현재 내역을 당초로 확정") + button("load-contract", "당초·계약 내역 불러오기") + button("recalculate-original", "당초 단가 재계산") + button("bid-rates", "낙찰률 적용") + button("restore-bid", "당초단가(100%) 복원") + button("change-prev", "이전", state.changePage === 0 ? "disabled" : "") + `${state.changePage + 1} / ${page.pages}쪽 · ${page.total}행` + button("change-next", "다음", state.changePage + 1 >= page.pages ? "disabled" : ""), table( ["구분", "코드", "품명", "당초 수량", "변경 수량", "당초 금액", "변경 금액", "증감"], page.rows.map(({ row }) => [ esc({ new: "신규", deleted: "삭제", changed: "변경", unchanged: "동일" }[row.status]), esc(row.Code), esc(row.Name), number(row.originalGN), number(row.GN), number(row.originalTC), number(row.TC), number(row.difference), ]), ) + table( ["원가 구분", "비목", "당초 금액", "변경 금액", "증감"], EstBusiness.costChanges(project).map((row) => [ esc(row.Gubun), esc(row.Name), number(row.originalCost), number(row.changedCost), number(row.difference), ]), ), project.baseline ? `당초: ${esc(project.baseline.name)}. 당초 자료는 별도 사본으로 보존합니다.` : changes.some((row) => row.originalSource === "file") ? "원본 파일에 저장된 당초 수량·금액과 비교합니다." : "현재 작업을 저장한 뒤 당초 내역을 확정하세요.", ); } else if (view === "progress") { state.roundId ||= project.progress[0]?.id; const round = project.progress.find((r) => r.id === state.roundId); let body = '회차를 추가하세요.
', tools = button("add-round", "기성 회차 추가") + (project.progress.length ? button("clear-progress", "기성 전체 초기화") : ""); if (round) { tools += `` + button( round.locked ? "unlock-round" : "lock-round", round.locked ? "확정 해제(이후 포함)" : "회차 확정·저장", ) + button("fill-progress", "전량·비율 일괄 입력") + button("rename-round", "회차 이름") + button("delete-round", "회차 삭제") + button("reset-round-inputs", "회차 입력 초기화") + `` + ``; let output; try { output = round.locked && round.result ? round.result : EstBusiness.progress(project, round.id); } catch (e) { output = { rows: [], errors: [e.message] }; } state.business = output; const page = EstBlockActions.progressPage(state, output.rows); tools += button("progress-prev", "이전", state.progressPage === 0 ? "disabled" : "") + `${state.progressPage + 1} / ${page.pages}쪽 · ${page.total}행` + button("progress-next", "다음", state.progressPage + 1 >= page.pages ? "disabled" : ""); body = table( [ "코드", "공종", "도급수량", "전회수량", "금회 입력", "입력방법", "사정 입력", "입력방법", "금회금액", "사정금액", "누계수량", "누계금액", ], page.rows.map(({ row, index }) => { const editable = ![54, 55, 58, 59, 62, 67].includes(Number(row.LinkIndex)); const key = EstBusiness.rowKey(row, index), input = round.inputs?.[key] || {}; const edit = (prefix) => `` + (input[prefix + "Mode"] === "components" ? EstMath.components .map( (field, i) => ``, ) .join("") : ""); const method = (prefix) => !editable ? "자동계산" : ``; return [ esc(row.Code), esc(row.Name), number(row.GN), number(row.JunGN), edit("Gum"), method("Gum"), edit("Sa"), method("Sa"), number(row.GumTC), number(row.SaTC), number(row.TotalGN), number(row.TotalTC), ]; }), ) + `${esc((output.errors || []).join(" / "))} 금회 도급액 ${number(output.total)} · 기성비율 ${number(output.ratio)}%
`; body += table( ["원가 비목", "도급금액", "전회", "금회", "사정", "누계"], (output.costs || []).map((row) => [ esc(row.Name || row.Gubun), number(row.ContractCost), number(row.JunCost), number(row.GumCost), number(row.SaCost), number(row.TotalCost), ]), ); } html = box( "기성고", tools, body, "전회는 직전 확정 회차 누계를 읽습니다. 금회·사정은 수량·금액·비율 중 하나를 입력해 계산합니다.", ); } else if (view === "escalation") { const output = EstBusiness.escalation(project); state.business = output; const comp = state.escalationComponent || "NU"; const choices = EstMath.units .map( (key, i) => ``, ) .join(""); const page = EstBlockActions.escalationPage(state, project.tables.EstMainDat); const rows = page.rows.map(({ row, index }) => { const result = output.rows[index]; return [ esc(row.Code), esc(row.Name), EstViews.cell("EstMainDat", index, `A${comp}`, row[`A${comp}`]), number(result[`Contract${comp}`]), EstViews.cell("EstMainDat", index, comp, row[comp]), number(result[`D${comp}`]), number(result[`E${comp}`]), number(result[`F${comp}`]), EstViews.cell("EstMainDat", index, "EscPlanned", row.EscPlanned || 0), EstViews.cell("EstMainDat", index, "EscActual", row.EscActual || 0), EstViews.cell("EstMainDat", index, "EscProgress", row.EscProgress || 0), number(result.EscExcluded), number(result.EscApplied), ``, EstViews.cell("EstMainDat", index, "EscManualQuantity", row.EscManualQuantity ?? 0), EstViews.cell("EstMainDat", index, "EscConstruction", row.EscConstruction ?? 0), EstViews.cell("EstMainDat", index, "EscPS", row.EscPS ?? 0), EstViews.cell("EstMainDat", index, "EscBefore", row.EscBefore ?? 0), ]; }); html = box( "물가변동 품목조정", `` + button("capture-bid", "현재 단가를 입찰단가로 기록") + button("load-bid", "입찰단가 불러오기") + button("load-contract", "계약 내역 불러오기") + button("load-progress-exclusion", "기성 누계수량 불러오기") + button("reset-escalation", "물가변동 입력 초기화") + button("capture-baseline", "계약 내역 확정") + button("escalation-prev", "이전", state.escalationPage === 0 ? "disabled" : "") + `${state.escalationPage + 1} / ${page.pages}쪽 · ${page.total}행` + button( "escalation-next", "다음", state.escalationPage + 1 >= page.pages ? "disabled" : "", ) + ``, table( [ "코드", "품명", "입찰 당시", "계약 단가", "변동 당시", "등락률 %", "등락폭", "조정단가", "예정공정수량", "실행공정수량", "기성수량", "적용제외", "적용수량", "제외 자동", "수동 제외수량", "시공중 수량(참고)", "PS 수량(참고)", "선시공 수량(참고)", ], rows, ), `조사 도급액 ${number(output.designTotal)} · 계약 도급액 ${number(output.contractTotal)} · 조정 도급액 ${number(output.adjustedTotal)} · 적용제외(간접비 포함) ${number(output.excludedTotal)} · 적용대가 ${number(output.applied)} · 조정률 ${number(output.rate)}% · 조정금액 ${number(output.adjustment)}. 계약·입찰 파일은 동일 행 순서로 불러옵니다. ${esc(output.errors.join(" / "))}`, ); } else if (view === "annual") { const output = EstBusiness.annual(project, false); state.business = output; html = box( "연차공사", button("add-annual", "차수 추가") + button("reset-annual", "연차 차수 초기화(웹보조)"), table( ["차수", "수량·금액 배분 수식", "순공사비", "도급액", "작업", ""], project.annual.map((year, index) => [ field("annual", index, "name", year.name, "text"), field("annual", index, "formula", year.formula, "text"), number(output.results[index]?.project.calculation.direct.TC), number(output.results[index]?.project.calculation.total), button("open-annual", "차수 내역 열기", `data-row="${index}"`), button( "move-annual", "↑", `data-row="${index}" data-direction="-1" ${index === 0 ? "disabled" : ""}`, ) + button( "move-annual", "↓", `data-row="${index}" data-direction="1" ${index === project.annual.length - 1 ? "disabled" : ""}`, ) + button("delete-extra", "삭제", `data-section="annual" data-row="${index}"`), ]), ), `수식 예: [기준]*0.5, [기준]-[1차]. 같은 수식을 수량과 각 성분 금액에 따로 적용하고 금액은 원 미만 절사합니다. ${esc(output.errors.join(" / "))}`, ); } else if (view === "resources") { state.resourceKind = state.resourceKind || "all"; state.resourceByWork = state.resourceByWork ?? false; const byWork = Boolean(state.resourceByWork); const output = EstResources.calculate(project, byWork); const filtered = output.rows.filter( (r) => state.resourceKind === "all" || String(r.kind) === String(state.resourceKind), ); const kinds = [ ["all", "전체"], ["6", "노임"], ["5", "자재"], ["105", "기계"], ["21", "표준"], ["101", "견적"], ]; const kindSelect = ``; const byWorkCheck = ``; const headers = byWork ? [ "공종코드", "공종명", "종류", "코드", "품명", "규격", "단위", "소요량 / 환산시간", "금액", ] : ["종류", "코드", "품명", "규격", "단위", "소요량 / 환산시간", "금액"]; const rows = filtered.map((row) => { const baseCells = [ esc({ 5: "자재", 6: "노임", 21: "표준", 101: "견적", 105: "기계" }[row.kind]), esc(row.Code), esc(row.Name), esc(row.Size), esc(row.Unite), number(row.quantity), number(row.amount), ]; return byWork ? [esc(row.WorkCode), esc(row.WorkName), ...baseCells] : baseCells; }); html = box( "동원인원·자재·장비", `${kindSelect} ${byWorkCheck} (총 ${filtered.length}건)`, table(headers, rows), esc(output.errors.join(" / ")), ); } else if (view === "drawing") { const rows = project.tables.CadMainDat || []; html = box( "원본 도면·문서 자료", "", table( ["번호", "코드", "이름", "보존 필드"], rows.map((row, index) => [ index + 1, esc(row.Code), esc(row.Name), esc( Object.keys(row) .filter((key) => row[key]?.binary) .join(", "), ), ]), ), "가져온 도면 바이너리는 프로젝트에 보존됩니다. VEC0650/CAD 편집·출력의 재현 근거는 현재 자료에 없어 동작을 구현하지 않았습니다.", ); } byId("work-content").innerHTML = html; } function findDataUsages(project, target) { if (!project || !target) return []; const code = target.Code ? String(target.Code).trim() : ""; const name = target.Name ? EstNative.normalize(target.Name) : ""; const size = target.Size ? EstNative.normalize(target.Size) : ""; if (!code && !name) return []; const results = []; const tables = [ { id: "EstMainDat", label: "내역서" }, { id: "HeadMainDat", label: "일위대가" }, { id: "CostMainDat", label: "단가산출" }, { id: "MachMainDat", label: "기계경비" }, ]; for (const info of tables) { const rows = project.tables[info.id] || []; const grouped = ["HeadMainDat", "CostMainDat", "MachMainDat"].includes(info.id); let curGroup = ""; for (let i = 0; i < rows.length; i++) { const r = rows[i]; if (grouped && Number(r.Index) > 0 && r.Code) curGroup = r.Code; // 대상 자기 자신의 대표 행은 검색 결과에서 제외 if (r === target) continue; let matched = false; if (code && r.Code === code) { matched = true; } else if (!code && name) { const rName = r.Name ? EstNative.normalize(r.Name) : ""; const rSize = r.Size ? EstNative.normalize(r.Size) : ""; if (rName === name && (!size || rSize === size)) { matched = true; } } if (matched) { const rowName = r.Name || (r.LineData ? EstText.clean(r.LineData) : ""); results.push({ table: info.id, tableLabel: info.label, index: i, group: curGroup, code: r.Code || "", name: rowName, size: r.Size || "", unite: r.Unite || "", quantity: r.GN != null ? Number(r.GN) : null, price: r.TC != null ? Number(r.TC) : null, }); } } } return results; } function showUsesSearchDialog(targetRow) { const { state, esc, number, showDialog, byId } = EstApp; if (!targetRow) throw Error("사용위치를 검색할 행을 먼저 선택하세요."); const rowName = targetRow.Name || (targetRow.LineData ? EstText.clean(targetRow.LineData) : ""); const results = findDataUsages(state.project, targetRow); const title = `데이터 사용위치 검색: ${targetRow.Code ? `[${targetRow.Code}] ` : ""}${rowName || "선택 항목"}`; let bodyHtml = ""; if (!results.length) { bodyHtml = '내역서, 일위대가, 단가산출, 기계경비에서 사용된 위치를 찾지 못했습니다.
'; } else { bodyHtml = `| 구분 | 코드 | 품명 / 산출근거 | 규격 | 단위 | 수량 | 금액 | 이동 |
|---|---|---|---|---|---|---|---|
| ${esc(res.tableLabel)} | ${esc(res.code)} | ${esc(res.name)} | ${esc(res.size)} | ${esc(res.unite)} | ${res.quantity != null ? number(res.quantity) : "-"} | ${res.price != null ? number(res.price) : "-"} |