(function () { window.__mhLedgerEnhancementLoaded = false; if (typeof S === "undefined" || typeof E === "undefined" || typeof render !== "function") return; window.__mhLedgerEnhancementLoaded = true; if (!S.dashboard) S.dashboard = { year: "", section: "active" }; if (!S.collapsedGroups) S.collapsedGroups = {}; function bgToday() { var now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate()); } function bgNormalizeText(value) { return String(value || "").replace(/\s+/g, " ").trim(); } function bgParseDate(value) { var text = String(value || "").trim(); if (!text) return null; var match = text.match(/(20\d{2})\D?(\d{1,2})\D?(\d{1,2})/); if (match) { var parsed = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); return isNaN(parsed.getTime()) ? null : parsed; } var fallback = new Date(text); if (isNaN(fallback.getTime())) return null; return new Date(fallback.getFullYear(), fallback.getMonth(), fallback.getDate()); } function bgYearFromText(value) { var match = String(value || "").trim().match(/(20\d{2})/); return match ? match[1] : ""; } function bgStartYear(row) { return bgYearFromText(row && row.sDate); } function bgEndYear(row) { return bgYearFromText(row && row.eDate); } function normalizedCategory(row) { var category = bgNormalizeText(row && row.cat); if (category.indexOf("가족사") >= 0) return "가족사"; var corp = bgNormalizeText(row && row.corp); if (corp && corp !== "바론") return "가족사"; return "바론"; } function isSupportServiceRow(row) { return bgNormalizeText(row && row.name).indexOf("경영 및 기술지원 서비스") >= 0; } function projectTypeLabel(row) { if (isSupportServiceRow(row)) return "기술지원서비스"; return normalizedCategory(row); } function projectTypeRank(row) { var label = projectTypeLabel(row); if (label === "바론") return 0; if (label === "가족사") return 1; return 2; } function normalizeStatusLabel(status) { var value = bgNormalizeText(status); if (!value) return "-"; if (value === "완료") return "준공"; if (value === "진행") return "과업진행중"; if (value === "대기") return "계약대기"; if (value === "중지") return "과업중지"; return value; } function rowStatusLabel(row) { return normalizeStatusLabel(row && row.status); } function bgDisplayYear(row) { var start = bgStartYear(row); if (start) return start; var contractMatch = String((row && row.cDate) || "").trim().match(/(20\d{2})/); if (contractMatch) return contractMatch[1]; var nameMatch = String((row && row.name) || "").trim().match(/^(20\d{2})/); if (nameMatch) return nameMatch[1]; return bgEndYear(row) || "미지정"; } function bgCompletionYear(row) { return bgEndYear(row) || bgDisplayYear(row); } function bgDateOrYearStart(row) { var yearText = bgDisplayYear(row); return bgParseDate(row && row.sDate) || bgParseDate(row && row.cDate) || (/^20\d{2}$/.test(yearText) ? new Date(Number(yearText), 0, 1) : null); } function bgDateOrYearEnd(row) { var completionYear = bgCompletionYear(row); return bgParseDate(row && row.eDate) || (/^20\d{2}$/.test(completionYear) ? new Date(Number(completionYear), 11, 31) : null); } function bgYearCutoff(year) { var targetYear = Number(year || 0); if (!targetYear) return null; var today = bgToday(); if (targetYear < today.getFullYear()) return new Date(targetYear, 11, 31); if (targetYear === today.getFullYear()) return today; return null; } function bgYearStartDate(year) { var targetYear = Number(year || 0); return targetYear ? new Date(targetYear, 0, 1) : null; } function bgActiveInYear(row, year) { var cutoff = bgYearCutoff(year); var yearStart = bgYearStartDate(year); var startDate = bgDateOrYearStart(row); var endDate = bgDateOrYearEnd(row); if (!(cutoff && yearStart && startDate)) return false; if (startDate > cutoff) return false; if (endDate && endDate < yearStart) return false; return rowStatusLabel(row) === "과업진행중"; } function bgStartedInYear(row, year) { var cutoff = bgYearCutoff(year); var startDate = bgDateOrYearStart(row); if (!(cutoff && startDate)) return false; return startDate.getFullYear() === Number(year || 0) && startDate <= cutoff; } function bgCompletedInYear(row, year) { var cutoff = bgYearCutoff(year); var endDate = bgDateOrYearEnd(row); if (!(cutoff && endDate)) return false; return rowStatusLabel(row) === "준공" && endDate.getFullYear() === Number(year || 0) && endDate <= cutoff; } function bgYearRange(row) { var years = []; var startYear = Number(bgDisplayYear(row) || 0); var endYear = Number(bgCompletionYear(row) || 0); if (startYear && endYear && endYear >= startYear) { for (var year = startYear; year <= endYear; year += 1) years.push(String(year)); } else if (startYear) { years.push(String(startYear)); } return years; } function bgYears(rows) { var currentYear = new Date().getFullYear(); var years = Array.from(new Set((Array.isArray(rows) ? rows : []).flatMap(bgYearRange).filter(function (year) { return /^20\d{2}$/.test(year); }))).sort(function (a, b) { return Number(b) - Number(a); }); years = years.filter(function (year) { var numericYear = Number(year); return numericYear >= 2018 && numericYear <= currentYear; }); return years.length ? years : [String(currentYear)]; } function bgEnsureYear(rows) { var years = bgYears(rows); if (!years.includes(S.dashboard.year)) S.dashboard.year = years[0]; return years; } function bgTotals(targetRows) { return (Array.isArray(targetRows) ? targetRows : []).reduce(function (acc, row) { acc.c += Number((row && row.cSup) || 0); acc.col += Number((row && row.col) || 0); acc.recv += Number((row && row.recv) || 0); return acc; }, { c: 0, col: 0, recv: 0 }); } function isBaronProjectRow(row) { return projectTypeLabel(row) === "바론"; } function isSoftwareProjectRow(row) { var name = bgNormalizeText(row && row.name).toLowerCase(); if (!name) return false; return [ "프로그램", "소프트웨어", "software", " sw", "sw ", "erp", "tova", "ipipe", "eg-bim", "cad" ].some(function (keyword) { return name.indexOf(keyword) >= 0; }); } function shouldSinkProjectName(row) { var name = bgNormalizeText(row && row.name); return name.indexOf("프로그램") >= 0 || name.indexOf("사용") >= 0; } function bgSummarize(rows, selectedYear) { var items = Array.isArray(rows) ? rows : []; var targetYear = selectedYear || bgEnsureYear(items)[0]; var activeRows = items.filter(function (row) { return bgActiveInYear(row, targetYear); }); var newProjectRows = items.filter(function (row) { return bgStartedInYear(row, targetYear); }); var completedRows = items.filter(function (row) { return bgCompletedInYear(row, targetYear); }); var managementRows = activeRows.filter(isSupportServiceRow); var baronActiveRows = activeRows.filter(isBaronProjectRow); return { targetYear: targetYear, activeRows: activeRows, newProjectRows: newProjectRows, completedRows: completedRows, managementRows: managementRows, managementTotals: bgTotals(managementRows), baronActiveRows: baronActiveRows, baronProjectTotals: bgTotals(baronActiveRows), baronSoftwareCount: baronActiveRows.filter(isSoftwareProjectRow).length }; } function bgMatches(row) { var section = S.dashboard.section || "active"; var selectedYear = S.dashboard.year || bgEnsureYear(S.all)[0]; if (section === "new") return bgStartedInYear(row, selectedYear); if (section === "completed") return bgCompletedInYear(row, selectedYear); return bgActiveInYear(row, selectedYear); } function formatSplitPercent(split) { var numeric = parseFloat(String(split || "").replace(/[^0-9.\-]/g, "")); if (!Number.isFinite(numeric) || numeric === 0) return "분담율 -%"; return "분담율 " + numeric.toFixed(2) + "%"; } function projectYear(row) { var start = String((row && row.sDate) || "").trim(); var startMatch = start.match(/(20\d{2})/); if (startMatch) return startMatch[1]; var name = String((row && row.name) || "").trim(); var nameMatch = name.match(/^(20\d{2})/); if (nameMatch) return nameMatch[1]; var end = String((row && row.eDate) || "").trim(); var endMatch = end.match(/(20\d{2})/); if (endMatch) return endMatch[1]; return "미지정"; } function groupSortRank(row) { var startYear = Number(projectYear(row) || 0); if (!startYear) return 9998; return startYear; } function tableGroupLabel(row) { var startYear = projectYear(row); if (/^20\d{2}$/.test(startYear)) return startYear + " " + projectTypeLabel(row); return "미지정 " + projectTypeLabel(row); } function compareDashboardRows(a, b) { var typeRankDiff = projectTypeRank(a) - projectTypeRank(b); if (typeRankDiff !== 0) return typeRankDiff; var groupDiff = groupSortRank(a) - groupSortRank(b); if (groupDiff !== 0) return groupDiff; var sinkDiff = Number(shouldSinkProjectName(a)) - Number(shouldSinkProjectName(b)); if (sinkDiff !== 0) return sinkDiff; return bgNormalizeText(a && a.name).localeCompare(bgNormalizeText(b && b.name), "ko"); } function filterCategoryLabel(row) { return projectTypeLabel(row); } function filterClientLabel(row) { if (typeof normalizeClientDisplay === "function") { return normalizeClientDisplay(row && row.client); } return bgNormalizeText(row && row.client) || "-"; } function filterOrderLabel(row) { return bgNormalizeText(row && row.order) || "-"; } function receivableFilterLabel(row) { var amount = Number((row && row.recv) || 0); if (amount <= 0) return "미수 없음"; if (amount < 10000000) return "1천만 미만"; if (amount < 100000000) return "1천만 이상"; return "1억 이상"; } function refreshFilterDom() { E.filterButtons = Object.fromEntries(Array.from(document.querySelectorAll(".th-trigger")).map(function (el) { return [el.dataset.filter, el]; })); E.filterMenus = Object.fromEntries(Array.from(document.querySelectorAll(".th-menu")).map(function (el) { return [el.dataset.filter, el]; })); } function renderLedgerTable() { var table = document.querySelector(".panel table"); if (!table || !E.tbody) return; var thead = table.querySelector("thead"); if (thead) { thead.innerHTML = '' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + ""; } var rows = (Array.isArray(S.viewRows) ? S.viewRows : []).slice().sort(compareDashboardRows); S.viewRows = rows; var lastGroupLabel = ""; E.tbody.innerHTML = rows.map(function (r) { var groupLabel = tableGroupLabel(r); var isCollapsed = !!S.collapsedGroups[groupLabel]; var groupRow = ""; if (groupLabel !== lastGroupLabel) { groupRow = '"; lastGroupLabel = groupLabel; } if (isCollapsed) return groupRow; return groupRow + '' + '
= 0 ? 'badge-baron' : 'badge-family') + '">' + esc(r.cat || "-") + '
' + '
' + esc(r.code || "-") + '
' + '
' + esc(r.periodText || "-") + '
' + '
' + esc((r.client || "").trim() || "-") + '
' + esc(formatSplitPercent(r.split)) + '
' + '
' + esc(r.order || "-") + '
' + '
' + esc(rowStatusLabel(r)) + '
' + '' + esc(won(r.cSup || 0)) + '' + '' + esc(r.outsourceCost ? won(r.outsourceCost) : "-") + '' + '' + esc(won(r.recv || 0)) + '' + '' + esc(won(r.col || 0)) + '' + '' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '' + ''; }).join(""); refreshFilterDom(); if (typeof syncColumnFilters === "function") syncColumnFilters(S.all); } function renderCollectionBoard(r) { var payments = Array.isArray(r.payments) && r.payments.length ? r.payments : [{ pay: r.pay || "-", issueDate: r.issueDate || "", collectDate: r.collectDateSummary || r.colDate || "", collected: r.col || 0, receivable: r.recv || Math.max(0, Number(r.sTot || 0) - Number(r.col || 0)), note: r.note || "", status: r.status || "" }]; return '
C
수금 및 기성 현황
기성 차수별 세금계산서 발행 및 수금 내역
총 수금 ' + esc(won(r.col || 0)) + '
' + payments.map(function (payment, index) { var noteParts = []; if (payment.status) noteParts.push(payment.status); if (payment.note) noteParts.push(payment.note); return ''; }).join("") + "
기성 차수세금계산서 발행일수금일수금금액미수금액비고
' + esc((index + 1) + "차") + '' + esc(payment.pay || "-") + '' + esc(payment.issueDate ? d(payment.issueDate) : "-") + '' + esc(payment.collectDate ? d(payment.collectDate) : "-") + '' + esc(won(payment.collected || 0)) + '' + esc(won(payment.receivable || 0)) + '' + esc(noteParts.join(" / ") || "-") + '
"; } function renderContactCard(label, name, company, department, phone, email) { var hasValue = [name, company, department, phone, email].some(function (value) { return String(value || "").trim() !== ""; }); if (!hasValue) { return '
' + esc(label) + '
등록된 담당자 정보가 없습니다.
'; } return '
' + esc(label) + '
' + '
이름
' + esc(name || "-") + '
' + '
소속
' + esc(company || "-") + '
' + esc(department || "-") + '
' + '
연락처
' + esc(phone || "-") + '
' + '
이메일
' + esc(email || "-") + '
' + "
"; } function renderProjectInline(r) { var payments = Array.isArray(r.payments) ? r.payments : []; var latestCollect = d(r.collectDateSummary || r.colDate); var hasOutsource = (Array.isArray(r.outsourceItems) && r.outsourceItems.length > 0) || Number(r.outsourceCost || 0) > 0 || Number(r.outsourcePaid || 0) > 0 || Number(r.outsourceRemaining || 0) > 0; var clientDisplay = typeof normalizeClientDisplay === "function" ? normalizeClientDisplay(r.client) : (String(r.client || "").trim() || "-"); var splitDisplay = typeof formatSplitDisplay === "function" ? formatSplitDisplay(r.split) : formatSplitPercent(r.split).replace("분담율 ", ""); var summaryCards = [ '
계약금
' + esc(won(r.cSup || 0)) + '
', '
수금액
' + esc(won(r.col || 0)) + '
' + esc(latestCollect === "-" ? "수금일 없음" : "최종 수금일 " + latestCollect) + '
', '
수금률
' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '
' + esc(payments.length ? "기성 " + payments.length + "차까지 반영" : "차수 정보 없음") + '
', '
미수금액
' + esc(won(r.recv || 0)) + '
잔여 수금 필요 금액
' ].join(""); var boards = [ hasOutsource && typeof renderOutsourceBoard === "function" ? renderOutsourceBoard(r) : "", renderCollectionBoard(r) ].filter(Boolean).join(""); return '
계약법인
' + esc(r.corp || "-") + '
발주처
' + esc(clientDisplay) + '
' + esc(splitDisplay ? "분담율 " + splitDisplay : "분담율 -") + '
발주방법
' + esc(r.order || "-") + '
PM
' + esc(r.pm || "-") + '
' + summaryCards + '
' + renderContactCard("계약 / 청구 담당자", r.cmNm, r.cmCo, r.cmDp, r.cmPh, r.cmEm) + renderContactCard("부서 담당자", r.dmNm, r.dmCo, r.dmDp, r.dmPh, r.dmEm) + '
' + boards + '
'; } function openProjectWindow(r) { var popupKey = typeof rowKey === "function" ? rowKey(r).replace(/[^0-9a-zA-Z]/g, "_") : String((r.code || "project") + "_" + (r.name || "")).replace(/[^0-9a-zA-Z_]/g, "_"); var popup = window.open("", "business_project_" + popupKey, "width=1600,height=980,resizable=yes,scrollbars=yes"); if (!popup) return; var styleText = Array.from(document.querySelectorAll("style")).map(function (el) { return el.textContent || ""; }).join("\n"); var detailHtml = renderProjectInline(r); var pageHtml = '' + esc(r.name || "사업 상세") + '"; popup.document.open(); popup.document.write(pageHtml); popup.document.close(); popup.focus(); } async function tryLoadDbDefaultBusinessLedger() { if (window.__mhBusinessDefaultLoaded) return; window.__mhBusinessDefaultLoaded = true; try { var response = await fetch("/api/integration/business-ledger-default"); if (!response.ok) throw new Error("기본 사업관리대장 원본을 불러오지 못했습니다."); var fileName = response.headers.get("x-source-filename") || "사업관리대장-1.xlsx"; var buffer = await response.arrayBuffer(); if (!buffer || !buffer.byteLength) throw new Error("기본 사업관리대장 원본 데이터가 비어 있습니다."); await loadLedgerFile(buffer, fileName); } catch (error) { console.error(error); } } function applyDashboardChrome() { if (!E.cards) return; document.body.setAttribute("data-mh-ledger-enhanced", "true"); var wrap = document.querySelector(".wrap"); var panel = document.querySelector(".panel"); if (wrap && panel) { var shell = wrap.querySelector(".business-shell"); if (!shell) { shell = document.createElement("div"); shell.className = "business-shell"; wrap.insertBefore(shell, E.cards); } if (E.cards.parentNode !== shell) shell.appendChild(E.cards); if (panel.parentNode !== shell) shell.appendChild(panel); } var years = bgEnsureYear(S.all); var summary = bgSummarize(S.all, S.dashboard.year); var totals = summary.baronProjectTotals; var totalRate = totals.c > 0 ? (totals.col / totals.c) * 100 : 0; var toolbarHtml = '
' + '
' + years.map(function (year) { return '"; }).join("") + '' + "
" + '
' + '' + '' + '' + "
"; var cards = [ { label: summary.targetYear + "년 프로젝트", value: summary.baronActiveRows.length.toLocaleString("ko-KR") + "건 (" + summary.baronSoftwareCount.toLocaleString("ko-KR") + "건)", note: "바론 수행중 프로젝트 / SW" }, { label: "계약금 (VAT별도)", value: won(totals.c), note: "" }, { label: "수금액", value: won(totals.col), note: "" }, { label: "미수금", value: won(totals.recv), note: "" }, { label: "수금율", value: totalRate.toFixed(2) + "%", note: "계약금 대비 수금액" }, { label: "경영지원서비스 금액", value: won(summary.managementTotals.c), note: "", className: "management" } ]; E.cards.innerHTML = toolbarHtml + cards.map(function (card) { return '
' + esc(card.label) + '
' + esc(card.value) + '
' + esc(card.note || "") + "
"; }).join(""); var searchWrap = E.cards.querySelector(".cards-toolbar-search"); if (searchWrap && E.search) { searchWrap.appendChild(E.search); E.search.placeholder = "전체 검색"; } } var originalRender = render; render = function () { originalRender(); applyDashboardChrome(); renderLedgerTable(); }; filter = function () { bgEnsureYear(S.all); var q = String(E.search.value || "").trim().toLowerCase(); var searched = !q ? S.all.slice() : S.all.filter(function (r) { return [r.code, r.name, r.client, r.pm, r.status, r.cat, r.corp, r.pay, (r.payments || []).map(function (p) { return p.pay; }).join(" "), r.periodText].join(" ").toLowerCase().includes(q); }); S.rows = searched.filter(function (r) { return bgMatches(r) && matchesColumnFilters(r); }); S.rows.sort(compareDashboardRows); render(); }; filterDefinitions = function () { return [ { key: "cat", map: filterCategoryLabel }, { key: "code", map: function (r) { return r.code || "-"; } }, { key: "name", map: function (r) { return r.name || "-"; } }, { key: "client", map: filterClientLabel }, { key: "order", map: filterOrderLabel }, { key: "status", map: rowStatusLabel }, { key: "amount", map: amountFilterLabel }, { key: "outsource", map: outsourceFilterLabel }, { key: "receivable", map: receivableFilterLabel }, { key: "collected", map: collectedFilterLabel }, { key: "rate", map: rateFilterLabel } ]; }; updateFilterButtons = function () { Object.keys(E.filterButtons || {}).forEach(function (key) { var btn = E.filterButtons[key]; if (!btn) return; var active = !!S.filters[key]; btn.classList.toggle("active", active); btn.title = active ? ((btn.dataset.label || "") + ": " + S.filters[key]) : (btn.dataset.label || ""); var mark = btn.querySelector(".th-mark"); if (mark) mark.textContent = active ? "•" : ""; }); }; syncColumnFilters = function (rows) { filterDefinitions().forEach(function (def) { var values = uniqueFilterValues(rows, def.map); if (S.filters[def.key] && !values.includes(S.filters[def.key])) delete S.filters[def.key]; renderFilterMenu(def.key, values); }); updateFilterButtons(); }; matchesColumnFilters = function (r) { if (S.filters.cat && filterCategoryLabel(r) !== S.filters.cat) return false; if (S.filters.code && (r.code || "-") !== S.filters.code) return false; if (S.filters.name && (r.name || "-") !== S.filters.name) return false; if (S.filters.client && filterClientLabel(r) !== S.filters.client) return false; if (S.filters.order && filterOrderLabel(r) !== S.filters.order) return false; if (S.filters.status && rowStatusLabel(r) !== S.filters.status) return false; if (S.filters.amount && amountFilterLabel(r) !== S.filters.amount) return false; if (S.filters.outsource && outsourceFilterLabel(r) !== S.filters.outsource) return false; if (S.filters.receivable && receivableFilterLabel(r) !== S.filters.receivable) return false; if (S.filters.collected && collectedFilterLabel(r) !== S.filters.collected) return false; if (S.filters.rate && rateFilterLabel(r) !== S.filters.rate) return false; return true; }; if (E.cards && !E.cards.dataset.dashboardBound) { E.cards.dataset.dashboardBound = "true"; E.cards.addEventListener("click", function (event) { var yearButton = event.target && event.target.closest ? event.target.closest("[data-dashboard-year]") : null; if (yearButton) { S.dashboard.year = yearButton.getAttribute("data-dashboard-year") || S.dashboard.year; filter(); return; } var sectionButton = event.target && event.target.closest ? event.target.closest("[data-dashboard-section]") : null; if (sectionButton) { S.dashboard.section = sectionButton.getAttribute("data-dashboard-section") || "active"; filter(); } }); } if (E.tbody && !E.tbody.dataset.projectBound) { E.tbody.dataset.projectBound = "true"; E.tbody.addEventListener("click", function (event) { var groupButton = event.target && event.target.closest ? event.target.closest("[data-group-label]") : null; if (groupButton) { var label = groupButton.getAttribute("data-group-label") || ""; if (label) { S.collapsedGroups[label] = !S.collapsedGroups[label]; render(); } return; } var trigger = event.target && event.target.closest ? event.target.closest(".project-link") : null; if (!trigger) return; var key = trigger.getAttribute("data-project-key") || ""; var rows = Array.isArray(S.viewRows) ? S.viewRows : []; var row = rows.find(function (item) { return (String(item.code || "") + "|" + String(item.name || "")) === key; }); if (row) openProjectWindow(row); }); } var panel = document.querySelector(".panel"); if (panel && !panel.dataset.ledgerFilterBound) { panel.dataset.ledgerFilterBound = "true"; panel.addEventListener("click", function (event) { var trigger = event.target && event.target.closest ? event.target.closest(".th-trigger") : null; if (trigger) { refreshFilterDom(); event.stopPropagation(); toggleFilterMenu(trigger.dataset.filter); return; } var option = event.target && event.target.closest ? event.target.closest("button[data-filter-value]") : null; var menu = event.target && event.target.closest ? event.target.closest(".th-menu") : null; if (option && menu) { event.stopPropagation(); setFilterValue(menu.dataset.filter, option.getAttribute("data-filter-value") || ""); } }); } setTimeout(function () { try { filter(); if (typeof loadLedgerFile === "function") { tryLoadDbDefaultBusinessLedger(); } } catch (error) { console.error(error); } }, 0); window.addEventListener("message", function (event) { var data = event.data || {}; if (data.source !== "total-upload" || data.type !== "business") return; setTimeout(function () { try { applyDashboardChrome(); renderLedgerTable(); } catch (error) { console.error(error); } }, 50); }); })();