const EstActions = (() => { let pending = null, clipboard = [], clipboardTable = "", restoring = false; function form(title, fields, callback) { const { esc, showDialog } = EstApp; showDialog( title, `
${esc(p.modified)}
`, ) .join("") : "저장된 작업이 없습니다.", ); } else if (name === "load-project") { if (confirm("현재 작업을 저장한 작업으로 바꾸시겠습니까?")) { EstApp.setProject(await api(`/api/projects/${node.dataset.id}`)); EstApp.byId("dialog").close(); } } else if (name === "parity") { const result = await api("/api/parity"); showDialog( "REA 샘플 대조", `${result.checked}개 중 ${result.passed}개 일치 · 불일치 ${result.failed}개
보관된 EST 샘플의 저장값과 계산 결과를 비교했습니다. 원본 실행 비교는 포함하지 않습니다.
${result.errors.length ? `${esc(result.errors.join(" / "))}
` : ""}| 영역 | 검사 수 | 일치 |
|---|---|---|
| ${esc(group)} | ${result.results.filter((r) => r.group === group).length} | ${result.results.filter((r) => r.group === group && r.pass).length} |
표지는 짝으로 지정합니다. 구간지정 A~Z는 결과 앞에, 구간합계는 합산할 같은 문자를 감쌉니다.
현재 원가 표를 바꾸므로 작업을 저장한 뒤 선택하세요.
${templates.map((t, i) => ``).join("")}`, ); } else if (name === "use-template") { if (confirm("현재 원가 표를 선택한 서식으로 바꾸시겠습니까?")) EstApp.change( () => (project.tables.Wonga = structuredClone( project.wongaTemplates[Number(node.dataset.row)].rows, )), ); EstApp.byId("dialog").close(); } else if (name === "basic-wonga") { if (confirm("현재 원가 표를 기본 뼈대로 바꾸시겠습니까?")) EstApp.change(() => { project.options.TotalFix = false; project.tables.Wonga = [ { Gubun: "N", Name: "직접 노무비", BaseField: "NC", Cost: 0 }, { Gubun: "G", Name: "직접 재료비", BaseField: "GC", Cost: 0 }, { Gubun: "J", Name: "직접 경비", BaseField: "JC", Cost: 0 }, { Gubun: "S", Name: "표준시장단가", BaseField: "SC", Cost: 0 }, { Gubun: "T", Name: "순공사비", VSusic: "N+G+J+S" }, { Gubun: "R", Name: "도급액", VSusic: "T", LinkIndex: 55 }, ]; }); EstApp.byId("dialog").close(); } else if (name === "capture-baseline") { if ( confirm( "현재 계산값을 당초·계약 내역으로 보존하시겠습니까? 기존 당초 사본이 있으면 교체됩니다.", ) ) EstApp.change(() => (state.project = EstBusiness.baseline(project))); } else if (name === "capture-bid") { if (confirm("현재 단가를 입찰 당시 단가로 기록하시겠습니까?")) EstApp.change(() => project.tables.EstMainDat.forEach((row) => EstMath.units.forEach((key) => (row[`A${key}`] = EstMath.num(row[key]))), ), ); } else if (name === "recalculate-original") form( "당초분 재계산", [ { name: "method", value: String(project.options.OldRecalcMethod || 0), label: "계산 방법", choices: [ ["0", "신규공종만 계산"], ["1", "변경된 단가만"], ["2", "당초분 전체"], ], }, ], (values) => EstApp.change(() => { project.baseline = EstBusiness.original(project, Number(values.method)); project.options.OldRecalcMethod = Number(values.method); }), ); else if (name === "restore-bid") { if (confirm("당초단가(100%)로 복원하시겠습니까?")) EstApp.change(() => { state.project = EstBusiness.restoreBid(project); }); } else if (name === "add-quantity") EstApp.change(() => { project.quantities.push({ Code: EstQuantity.newCode(project.quantities), Name: "새 수량", TargetCode: "", formula: "1", digits: 2, }); }, false); else if (name === "apply-quantity") { const totals = EstQuantity.totals(project, state.quantitySheet); if (confirm("연결된 내역 수량을 산출 결과로 바꾸시겠습니까?")) EstApp.change(() => EstQuantity.applyTotals(project, totals)); } else if (name === "add-round") { const next = { id: EstApp.uid(), name: EstBusiness.newProgressName(project.progress), inputs: {}, approved: false, locked: false, }; EstApp.change(() => { project.progress.push(next); state.roundId = next.id; }, false); } else if (name === "lock-round") { if ( confirm("이 회차를 확정하고 저장하시겠습니까? 전회 누계로 보존되고 직접 편집이 잠깁니다.") ) { EstApp.change(() => (state.project = EstProgress.lock(project, state.roundId)), false); await EstApp.save(); } } else if (name === "add-annual") EstApp.change( () => project.annual.push({ name: EstBusiness.newAnnualName(project.annual), formula: "[기준]", }), false, ); else if (name === "move-annual") { const index = Number(node.dataset.row), next = index + Number(node.dataset.direction); if (index >= 0 && index < project.annual.length && next >= 0 && next < project.annual.length) EstApp.change(() => { [project.annual[index], project.annual[next]] = [ project.annual[next], project.annual[index], ]; }, false); } else if (name === "open-annual") { const result = EstBusiness.annualCopy(project, Number(node.dataset.row)); if (confirm("현재 화면을 차수 내역 사본으로 바꾸시겠습니까?")) { EstApp.setProject(result); EstApp.navigate("EstMainDat"); } } else if (name === "delete-extra") { if (confirm("이 항목을 삭제하시겠습니까?")) EstApp.change( () => project[node.dataset.section].splice(Number(node.dataset.row), 1), false, ); } else if (name === "dialog-apply" && pending) { await pending(); pending = null; EstApp.byId("dialog").close(); } else if (name === "close-dialog") EstApp.byId("dialog").close(); else await EstBusinessActions.handle(name, node); } function install() { EstSchedule.install(); EstExchange.install(); EstBusinessActions.install(); EstIssues.install(); document.addEventListener("click", async (event) => { try { const marker = event.target.closest("[data-marker]"); if (marker) { const editor = EstApp.byId("line-editor"), start = editor.selectionStart, end = editor.selectionEnd, tag = `⟦${marker.dataset.marker}⟧`; editor.setRangeText(tag + editor.value.slice(start, end) + tag, start, end, "select"); editor.focus(); return; } const view = event.target.closest("[data-view]"); if (view) { EstApp.navigate(view.dataset.view); return; } const group = event.target.closest("[data-group]"); if (group) { EstApp.navigate(EstApp.state.view, group.dataset.group); return; } const node = event.target.closest("[data-action]"); if (node) { await action(node.dataset.action, node); node.closest("details")?.removeAttribute("open"); } } catch (error) { EstApp.state.notice = error.message; EstApp.render(); } }); document.addEventListener("change", (event) => { const node = event.target, { state } = EstApp, project = state.project; state.notice = ""; try { if (EstBusinessActions.change(node)) return; if (node.matches("[data-select]")) { const index = Number(node.dataset.select); node.checked ? state.selection.add(index) : state.selection.delete(index); EstApp.render(); return; } if (node.id === "project-name") { EstApp.change(() => (project.name = node.value), false); return; } if (node.id === "automatic" || node.id === "refresh-sources") { EstApp.change( () => (project.options[node.id === "automatic" ? "AutoCalc" : "SourceRefresh"] = node.checked), ); return; } if (node.id === "round-choice") { state.roundId = node.value; EstApp.render(); return; } if (node.id === "escalation-component") { state.escalationComponent = node.value; EstApp.render(); return; } if (node.id === "use-approved") { const round = project.progress.find((r) => r.id === state.roundId); if (round.locked) throw Error("확정 회차입니다."); EstApp.change(() => (round.approved = node.checked), false); return; } if (node.matches("[data-progress]")) { const round = project.progress.find((r) => r.id === state.roundId); if (round.locked) throw Error("확정 회차입니다."); if (EstProgress.ambiguousKeys(project.tables.EstMainDat).has(node.dataset.key)) throw Error("기성 내역 식별자가 중복되어 입력할 수 없습니다."); const value = node.type === "number" ? Number(node.value) : node.value; if (node.type === "number" && !Number.isFinite(value)) throw Error("수치 입력 오류"); EstApp.change(() => { round.inputs[node.dataset.key] ||= {}; round.inputs[node.dataset.key][node.dataset.progress] = value; }, false); return; } if (node.matches("[data-extra]")) { const value = node.type === "number" ? Number(node.value) : node.value; if (node.type === "number" && !Number.isFinite(value)) throw Error("수치 입력 오류"); EstApp.change(() => { const row = project[node.dataset.extra][Number(node.dataset.row)]; row[node.dataset.field] = value; if (node.dataset.extra === "quantities" && node.dataset.field === "TargetCode") row.TargetKey = ""; }, false); return; } if (node.matches("[data-table][data-field]")) { const row = project.tables[node.dataset.table][Number(node.dataset.row)], key = node.dataset.field; const numeric = EstModel.numeric.has(key), value = numeric ? node.value === "" ? null : Number(node.value) : key === "LineData" ? EstText.encode(node.value) : node.value; if ( numeric && value !== null && (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER) ) throw Error("유효한 수치를 입력하세요."); if (key === "Level" && (value < 0 || value > 9 || !Number.isInteger(value))) throw Error("레벨은 0~9 정수입니다."); if (key === "Precision" && (value < 0 || value > 6 || !Number.isInteger(value))) throw Error("소수 자릿수는 0~6 정수입니다."); EstApp.remember(); if (key === "Code") { const previous = row.Code; EstModel.editCode(project, node.dataset.table, Number(node.dataset.row), value); if (state.group === previous && Number(row.Index) > 0) state.group = row.Code; } else if (key === "SCheck") EstBusinessActions.setClassification(project, row, node, value); else row[key] = value; if (state.view === "escalation" && EstMath.units.includes(key)) project.options.SourceRefresh = false; if (["StuffcDat", "LaborcDat", "EstiMateDat", "SilJukDat"].includes(node.dataset.table)) { if (key === "TC") { row.InputTC = value; row.ValueIndex = 4; } if (["NC", "GC", "JC", "SC"].includes(key)) row[`Input${key}`] = value; project.options.SourceRefresh = true; } if (project.options.AutoCalc !== false) EstApp.calculate(); EstApp.cache(); setTimeout(() => EstApp.render(), 0); } } catch (error) { state.notice = error.message; EstApp.render(); } }); EstApp.byId("open-file").addEventListener("change", async (event) => { try { await EstApp.open(event.target.files[0]); } catch (error) { EstApp.state.notice = error.message; EstApp.render(); } finally { event.target.value = ""; } }); EstApp.byId("search").addEventListener("input", (event) => { EstApp.state.query = event.target.value; EstApp.state.page = 0; EstApp.render(); }); EstApp.byId("previous-page").addEventListener("click", () => { EstApp.state.page--; EstApp.render(); }); EstApp.byId("next-page").addEventListener("click", () => { EstApp.state.page++; EstApp.render(); }); document.addEventListener("keydown", (event) => { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") { event.preventDefault(); EstApp.save().catch((error) => { EstApp.state.notice = error.message; EstApp.render(); }); } }); document.addEventListener("dblclick", (event) => { const input = event.target.closest('[data-field="Code"]'); if (!input) return; const row = EstApp.state.project.tables[input.dataset.table][Number(input.dataset.row)], kind = Number(row.LinkIndex); const table = { 103: "HeadMainDat", 104: "CostMainDat", 105: "MachMainDat" }[kind] || EstNative.sourceTables[kind]; if (table) { EstApp.navigate(table, row.Code); if (![103, 104, 105].includes(kind)) { EstApp.state.query = row.Code; EstApp.render(); } } }); EstAttachments.install(); EstStandard.install(); } const clip = () => ({ table: clipboardTable, rows: clipboard }); const setClip = (table, rows) => ((clipboardTable = table), (clipboard = structuredClone(rows))); return { install, form, undo, clip, setClip }; })();