(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 '| ' + 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(" / ") || "-") + ' |
';
}).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 '";
}
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 '' + 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 = '";
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);
});
})();