Improve ledger filters and dev sync checks

This commit is contained in:
hyunho
2026-04-02 11:17:01 +09:00
parent f8ea345882
commit c0564ee326
4 changed files with 455 additions and 105 deletions

View File

@@ -10,6 +10,10 @@
return new Date(now.getFullYear(), now.getMonth(), now.getDate()); return new Date(now.getFullYear(), now.getMonth(), now.getDate());
} }
function bgNormalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
}
function bgParseDate(value) { function bgParseDate(value) {
var text = String(value || "").trim(); var text = String(value || "").trim();
if (!text) return null; if (!text) return null;
@@ -36,6 +40,44 @@
return bgYearFromText(row && row.eDate); 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) { function bgDisplayYear(row) {
var start = bgStartYear(row); var start = bgStartYear(row);
if (start) return start; if (start) return start;
@@ -82,7 +124,7 @@
if (!(cutoff && yearStart && startDate)) return false; if (!(cutoff && yearStart && startDate)) return false;
if (startDate > cutoff) return false; if (startDate > cutoff) return false;
if (endDate && endDate < yearStart) return false; if (endDate && endDate < yearStart) return false;
return !(endDate && endDate <= cutoff); return rowStatusLabel(row) === "과업진행중";
} }
function bgStartedInYear(row, year) { function bgStartedInYear(row, year) {
@@ -96,7 +138,7 @@
var cutoff = bgYearCutoff(year); var cutoff = bgYearCutoff(year);
var endDate = bgDateOrYearEnd(row); var endDate = bgDateOrYearEnd(row);
if (!(cutoff && endDate)) return false; if (!(cutoff && endDate)) return false;
return endDate.getFullYear() === Number(year || 0) && endDate <= cutoff; return rowStatusLabel(row) === "준공" && endDate.getFullYear() === Number(year || 0) && endDate <= cutoff;
} }
function bgYearRange(row) { function bgYearRange(row) {
@@ -140,16 +182,32 @@
}, { c: 0, col: 0, recv: 0 }); }, { c: 0, col: 0, recv: 0 });
} }
function isSupportServiceRow(row) { function isBaronProjectRow(row) {
var category = String((row && row.cat) || "").trim(); return projectTypeLabel(row) === "바론";
return category.indexOf("경영지원") >= 0 || category.indexOf("서비스") >= 0;
} }
function isBaronProjectRow(row) { function isSoftwareProjectRow(row) {
var category = String((row && row.cat) || "").trim(); var name = bgNormalizeText(row && row.name).toLowerCase();
if (category.indexOf("바론") < 0) return false; if (!name) return false;
if (isSupportServiceRow(row)) return false; return [
return true; "프로그램",
"소프트웨어",
"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) { function bgSummarize(rows, selectedYear) {
@@ -158,14 +216,18 @@
var activeRows = items.filter(function (row) { return bgActiveInYear(row, targetYear); }); var activeRows = items.filter(function (row) { return bgActiveInYear(row, targetYear); });
var newProjectRows = items.filter(function (row) { return bgStartedInYear(row, targetYear); }); var newProjectRows = items.filter(function (row) { return bgStartedInYear(row, targetYear); });
var completedRows = items.filter(function (row) { return bgCompletedInYear(row, targetYear); }); var completedRows = items.filter(function (row) { return bgCompletedInYear(row, targetYear); });
var managementRows = newProjectRows.filter(isSupportServiceRow); var managementRows = activeRows.filter(isSupportServiceRow);
var baronActiveRows = activeRows.filter(isBaronProjectRow);
return { return {
targetYear: targetYear, targetYear: targetYear,
activeRows: activeRows, activeRows: activeRows,
newProjectRows: newProjectRows, newProjectRows: newProjectRows,
completedRows: completedRows, completedRows: completedRows,
managementRows: managementRows, managementRows: managementRows,
managementTotals: bgTotals(managementRows) managementTotals: bgTotals(managementRows),
baronActiveRows: baronActiveRows,
baronProjectTotals: bgTotals(baronActiveRows),
baronSoftwareCount: baronActiveRows.filter(isSoftwareProjectRow).length
}; };
} }
@@ -177,13 +239,6 @@
return bgActiveInYear(row, selectedYear); return bgActiveInYear(row, selectedYear);
} }
function normalizeStatusLabel(status) {
var value = String(status || "").trim();
if (!value) return "-";
if (value.indexOf("진행") >= 0) return "과업 진행중";
return value;
}
function formatSplitPercent(split) { function formatSplitPercent(split) {
var numeric = parseFloat(String(split || "").replace(/[^0-9.\-]/g, "")); var numeric = parseFloat(String(split || "").replace(/[^0-9.\-]/g, ""));
if (!Number.isFinite(numeric) || numeric === 0) return "분담율 -%"; if (!Number.isFinite(numeric) || numeric === 0) return "분담율 -%";
@@ -204,17 +259,57 @@
} }
function groupSortRank(row) { function groupSortRank(row) {
var selectedYear = Number((S.dashboard && S.dashboard.year) || projectYear(row) || 0);
var startYear = Number(projectYear(row) || 0); var startYear = Number(projectYear(row) || 0);
if (typeof bgCompletedInYear === "function" && bgCompletedInYear(row, String(selectedYear))) return 9999;
if (!startYear) return 9998; if (!startYear) return 9998;
return startYear; return startYear;
} }
function tableGroupLabel(row) { function tableGroupLabel(row) {
var startYear = projectYear(row); var startYear = projectYear(row);
if (/^20\d{2}$/.test(startYear)) return startYear + "년"; if (/^20\d{2}$/.test(startYear)) return startYear + " " + projectTypeLabel(row);
return "미지정"; 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() { function renderLedgerTable() {
@@ -236,12 +331,7 @@
+ '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="rate" data-label="수금률"><span class="th-title">수금률</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterRateMenu" class="th-menu" data-filter="rate"></div></div></th>' + '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="rate" data-label="수금률"><span class="th-title">수금률</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterRateMenu" class="th-menu" data-filter="rate"></div></div></th>'
+ "</tr>"; + "</tr>";
} }
var rows = (Array.isArray(S.viewRows) ? S.viewRows : []).slice().sort(function (a, b) { var rows = (Array.isArray(S.viewRows) ? S.viewRows : []).slice().sort(compareDashboardRows);
var ar = groupSortRank(a);
var br = groupSortRank(b);
if (ar !== br) return ar - br;
return Number(b.recv || 0) - Number(a.recv || 0);
});
S.viewRows = rows; S.viewRows = rows;
var lastGroupLabel = ""; var lastGroupLabel = "";
E.tbody.innerHTML = rows.map(function (r) { E.tbody.innerHTML = rows.map(function (r) {
@@ -259,7 +349,7 @@
+ '<td><button type="button" class="project-link" data-project-key="' + escAttr(String(r.code || "") + "|" + String(r.name || "")) + '">' + esc(r.name || "-") + '</button><div class="subline">' + esc(r.periodText || "-") + '</div></td>' + '<td><button type="button" class="project-link" data-project-key="' + escAttr(String(r.code || "") + "|" + String(r.name || "")) + '">' + esc(r.name || "-") + '</button><div class="subline">' + esc(r.periodText || "-") + '</div></td>'
+ '<td><div class="client-main">' + esc((r.client || "").trim() || "-") + '</div><div class="subline">' + esc(formatSplitPercent(r.split)) + '</div></td>' + '<td><div class="client-main">' + esc((r.client || "").trim() || "-") + '</div><div class="subline">' + esc(formatSplitPercent(r.split)) + '</div></td>'
+ '<td><div>' + esc(r.order || "-") + '</div></td>' + '<td><div>' + esc(r.order || "-") + '</div></td>'
+ '<td><div class="badge ' + (String(r.status || "").indexOf("완료") >= 0 ? 'ok' : '') + '">' + esc(normalizeStatusLabel(r.status)) + '</div></td>' + '<td><div class="badge ' + (rowStatusLabel(r) === "준공" ? 'ok' : '') + '">' + esc(rowStatusLabel(r)) + '</div></td>'
+ '<td class="num"><strong>' + esc(won(r.cSup || 0)) + '</strong></td>' + '<td class="num"><strong>' + esc(won(r.cSup || 0)) + '</strong></td>'
+ '<td class="num"><strong>' + esc(r.outsourceCost ? won(r.outsourceCost) : "-") + '</strong></td>' + '<td class="num"><strong>' + esc(r.outsourceCost ? won(r.outsourceCost) : "-") + '</strong></td>'
+ '<td class="num"><strong>' + esc(won(r.recv || 0)) + '</strong></td>' + '<td class="num"><strong>' + esc(won(r.recv || 0)) + '</strong></td>'
@@ -267,6 +357,8 @@
+ '<td class="num"><strong style="color:' + (isSettledRow(r) ? '#b7aa93' : '#1a5645') + '">' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '</strong></td>' + '<td class="num"><strong style="color:' + (isSettledRow(r) ? '#b7aa93' : '#1a5645') + '">' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '</strong></td>'
+ '</tr>'; + '</tr>';
}).join(""); }).join("");
refreshFilterDom();
if (typeof syncColumnFilters === "function") syncColumnFilters(S.all);
} }
function renderCollectionBoard(r) { function renderCollectionBoard(r) {
@@ -379,10 +471,8 @@
} }
var years = bgEnsureYear(S.all); var years = bgEnsureYear(S.all);
var summary = bgSummarize(S.all, S.dashboard.year); var summary = bgSummarize(S.all, S.dashboard.year);
var rows = Array.isArray(S.rows) ? S.rows : []; var totals = summary.baronProjectTotals;
var visibleBaronProjectRows = rows.filter(isBaronProjectRow); var totalRate = totals.c > 0 ? (totals.col / totals.c) * 100 : 0;
var totals = bgTotals(visibleBaronProjectRows);
var totalRate = typeof rate === "function" ? rate("", totals.col, totals.col + totals.recv) : 0;
var toolbarHtml = '<div class="cards-toolbar">' var toolbarHtml = '<div class="cards-toolbar">'
+ '<div class="cards-toolbar-row">' + '<div class="cards-toolbar-row">'
+ years.map(function (year) { + years.map(function (year) {
@@ -393,14 +483,14 @@
+ '<div class="cards-toolbar-metrics">' + '<div class="cards-toolbar-metrics">'
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "active" ? "active" : "") + '" data-dashboard-section="active"><span class="label">' + esc(summary.targetYear) + '년 진행과업</span><span class="count">' + summary.activeRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">전년도 이월 사업 포함</span></button>' + '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "active" ? "active" : "") + '" data-dashboard-section="active"><span class="label">' + esc(summary.targetYear) + '년 진행과업</span><span class="count">' + summary.activeRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">전년도 이월 사업 포함</span></button>'
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "new" ? "active" : "") + '" data-dashboard-section="new"><span class="label">' + esc(summary.targetYear) + '년 신규프로젝트</span><span class="count">' + summary.newProjectRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">계약기간 시작년도 기준</span></button>' + '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "new" ? "active" : "") + '" data-dashboard-section="new"><span class="label">' + esc(summary.targetYear) + '년 신규프로젝트</span><span class="count">' + summary.newProjectRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">계약기간 시작년도 기준</span></button>'
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "completed" ? "active" : "") + '" data-dashboard-section="completed"><span class="label">' + esc(summary.targetYear) + '년 완료과업</span><span class="count">' + summary.completedRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">해당년도 종료 사업 기준</span></button>' + '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "completed" ? "active" : "") + '" data-dashboard-section="completed"><span class="label">' + esc(summary.targetYear) + '년 완료과업</span><span class="count">' + summary.completedRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">진행상태 준공 기준</span></button>'
+ "</div></div>"; + "</div></div>";
var cards = [ var cards = [
{ label: summary.targetYear + "년 프로젝트", value: visibleBaronProjectRows.length.toLocaleString("ko-KR") + " 건", note: "" }, { label: summary.targetYear + "년 프로젝트", value: summary.baronActiveRows.length.toLocaleString("ko-KR") + "건 (" + summary.baronSoftwareCount.toLocaleString("ko-KR") + "건)", note: "바론 수행중 프로젝트 / SW" },
{ label: "계약금", value: won(totals.c), note: "" }, { label: "계약금 (VAT별도)", value: won(totals.c), note: "" },
{ label: "수금액", value: won(totals.col), note: "" }, { label: "수금액", value: won(totals.col), note: "" },
{ label: "미수금", value: won(totals.recv), note: "" }, { label: "미수금", value: won(totals.recv), note: "" },
{ label: "수금률(%)", value: totalRate.toFixed(2) + "%", note: "" }, { label: "수금", value: totalRate.toFixed(2) + "%", note: "계약금 대비 수금액" },
{ label: "경영지원서비스 금액", value: won(summary.managementTotals.c), note: "", className: "management" } { label: "경영지원서비스 금액", value: won(summary.managementTotals.c), note: "", className: "management" }
]; ];
E.cards.innerHTML = toolbarHtml + cards.map(function (card) { E.cards.innerHTML = toolbarHtml + cards.map(function (card) {
@@ -429,9 +519,62 @@
S.rows = searched.filter(function (r) { S.rows = searched.filter(function (r) {
return bgMatches(r) && matchesColumnFilters(r); return bgMatches(r) && matchesColumnFilters(r);
}); });
S.rows.sort(compareDashboardRows);
render(); 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) { if (E.cards && !E.cards.dataset.dashboardBound) {
E.cards.dataset.dashboardBound = "true"; E.cards.dataset.dashboardBound = "true";
E.cards.addEventListener("click", function (event) { E.cards.addEventListener("click", function (event) {
@@ -472,6 +615,26 @@
}); });
} }
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 () { setTimeout(function () {
try { try {
filter(); filter();

View File

@@ -10,6 +10,10 @@
return new Date(now.getFullYear(), now.getMonth(), now.getDate()); return new Date(now.getFullYear(), now.getMonth(), now.getDate());
} }
function bgNormalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
}
function bgParseDate(value) { function bgParseDate(value) {
var text = String(value || "").trim(); var text = String(value || "").trim();
if (!text) return null; if (!text) return null;
@@ -36,6 +40,44 @@
return bgYearFromText(row && row.eDate); 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) { function bgDisplayYear(row) {
var start = bgStartYear(row); var start = bgStartYear(row);
if (start) return start; if (start) return start;
@@ -82,7 +124,7 @@
if (!(cutoff && yearStart && startDate)) return false; if (!(cutoff && yearStart && startDate)) return false;
if (startDate > cutoff) return false; if (startDate > cutoff) return false;
if (endDate && endDate < yearStart) return false; if (endDate && endDate < yearStart) return false;
return !(endDate && endDate <= cutoff); return rowStatusLabel(row) === "과업진행중";
} }
function bgStartedInYear(row, year) { function bgStartedInYear(row, year) {
@@ -96,7 +138,7 @@
var cutoff = bgYearCutoff(year); var cutoff = bgYearCutoff(year);
var endDate = bgDateOrYearEnd(row); var endDate = bgDateOrYearEnd(row);
if (!(cutoff && endDate)) return false; if (!(cutoff && endDate)) return false;
return endDate.getFullYear() === Number(year || 0) && endDate <= cutoff; return rowStatusLabel(row) === "준공" && endDate.getFullYear() === Number(year || 0) && endDate <= cutoff;
} }
function bgYearRange(row) { function bgYearRange(row) {
@@ -140,16 +182,32 @@
}, { c: 0, col: 0, recv: 0 }); }, { c: 0, col: 0, recv: 0 });
} }
function isSupportServiceRow(row) { function isBaronProjectRow(row) {
var category = String((row && row.cat) || "").trim(); return projectTypeLabel(row) === "바론";
return category.indexOf("경영지원") >= 0 || category.indexOf("서비스") >= 0;
} }
function isBaronProjectRow(row) { function isSoftwareProjectRow(row) {
var category = String((row && row.cat) || "").trim(); var name = bgNormalizeText(row && row.name).toLowerCase();
if (category.indexOf("바론") < 0) return false; if (!name) return false;
if (isSupportServiceRow(row)) return false; return [
return true; "프로그램",
"소프트웨어",
"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) { function bgSummarize(rows, selectedYear) {
@@ -158,14 +216,18 @@
var activeRows = items.filter(function (row) { return bgActiveInYear(row, targetYear); }); var activeRows = items.filter(function (row) { return bgActiveInYear(row, targetYear); });
var newProjectRows = items.filter(function (row) { return bgStartedInYear(row, targetYear); }); var newProjectRows = items.filter(function (row) { return bgStartedInYear(row, targetYear); });
var completedRows = items.filter(function (row) { return bgCompletedInYear(row, targetYear); }); var completedRows = items.filter(function (row) { return bgCompletedInYear(row, targetYear); });
var managementRows = newProjectRows.filter(isSupportServiceRow); var managementRows = activeRows.filter(isSupportServiceRow);
var baronActiveRows = activeRows.filter(isBaronProjectRow);
return { return {
targetYear: targetYear, targetYear: targetYear,
activeRows: activeRows, activeRows: activeRows,
newProjectRows: newProjectRows, newProjectRows: newProjectRows,
completedRows: completedRows, completedRows: completedRows,
managementRows: managementRows, managementRows: managementRows,
managementTotals: bgTotals(managementRows) managementTotals: bgTotals(managementRows),
baronActiveRows: baronActiveRows,
baronProjectTotals: bgTotals(baronActiveRows),
baronSoftwareCount: baronActiveRows.filter(isSoftwareProjectRow).length
}; };
} }
@@ -177,13 +239,6 @@
return bgActiveInYear(row, selectedYear); return bgActiveInYear(row, selectedYear);
} }
function normalizeStatusLabel(status) {
var value = String(status || "").trim();
if (!value) return "-";
if (value.indexOf("진행") >= 0) return "과업 진행중";
return value;
}
function formatSplitPercent(split) { function formatSplitPercent(split) {
var numeric = parseFloat(String(split || "").replace(/[^0-9.\-]/g, "")); var numeric = parseFloat(String(split || "").replace(/[^0-9.\-]/g, ""));
if (!Number.isFinite(numeric) || numeric === 0) return "분담율 -%"; if (!Number.isFinite(numeric) || numeric === 0) return "분담율 -%";
@@ -204,17 +259,57 @@
} }
function groupSortRank(row) { function groupSortRank(row) {
var selectedYear = Number((S.dashboard && S.dashboard.year) || projectYear(row) || 0);
var startYear = Number(projectYear(row) || 0); var startYear = Number(projectYear(row) || 0);
if (typeof bgCompletedInYear === "function" && bgCompletedInYear(row, String(selectedYear))) return 9999;
if (!startYear) return 9998; if (!startYear) return 9998;
return startYear; return startYear;
} }
function tableGroupLabel(row) { function tableGroupLabel(row) {
var startYear = projectYear(row); var startYear = projectYear(row);
if (/^20\d{2}$/.test(startYear)) return startYear + "년"; if (/^20\d{2}$/.test(startYear)) return startYear + " " + projectTypeLabel(row);
return "미지정"; 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() { function renderLedgerTable() {
@@ -236,12 +331,7 @@
+ '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="rate" data-label="수금률"><span class="th-title">수금률</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterRateMenu" class="th-menu" data-filter="rate"></div></div></th>' + '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="rate" data-label="수금률"><span class="th-title">수금률</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterRateMenu" class="th-menu" data-filter="rate"></div></div></th>'
+ "</tr>"; + "</tr>";
} }
var rows = (Array.isArray(S.viewRows) ? S.viewRows : []).slice().sort(function (a, b) { var rows = (Array.isArray(S.viewRows) ? S.viewRows : []).slice().sort(compareDashboardRows);
var ar = groupSortRank(a);
var br = groupSortRank(b);
if (ar !== br) return ar - br;
return Number(b.recv || 0) - Number(a.recv || 0);
});
S.viewRows = rows; S.viewRows = rows;
var lastGroupLabel = ""; var lastGroupLabel = "";
E.tbody.innerHTML = rows.map(function (r) { E.tbody.innerHTML = rows.map(function (r) {
@@ -259,7 +349,7 @@
+ '<td><button type="button" class="project-link" data-project-key="' + escAttr(String(r.code || "") + "|" + String(r.name || "")) + '">' + esc(r.name || "-") + '</button><div class="subline">' + esc(r.periodText || "-") + '</div></td>' + '<td><button type="button" class="project-link" data-project-key="' + escAttr(String(r.code || "") + "|" + String(r.name || "")) + '">' + esc(r.name || "-") + '</button><div class="subline">' + esc(r.periodText || "-") + '</div></td>'
+ '<td><div class="client-main">' + esc((r.client || "").trim() || "-") + '</div><div class="subline">' + esc(formatSplitPercent(r.split)) + '</div></td>' + '<td><div class="client-main">' + esc((r.client || "").trim() || "-") + '</div><div class="subline">' + esc(formatSplitPercent(r.split)) + '</div></td>'
+ '<td><div>' + esc(r.order || "-") + '</div></td>' + '<td><div>' + esc(r.order || "-") + '</div></td>'
+ '<td><div class="badge ' + (String(r.status || "").indexOf("완료") >= 0 ? 'ok' : '') + '">' + esc(normalizeStatusLabel(r.status)) + '</div></td>' + '<td><div class="badge ' + (rowStatusLabel(r) === "준공" ? 'ok' : '') + '">' + esc(rowStatusLabel(r)) + '</div></td>'
+ '<td class="num"><strong>' + esc(won(r.cSup || 0)) + '</strong></td>' + '<td class="num"><strong>' + esc(won(r.cSup || 0)) + '</strong></td>'
+ '<td class="num"><strong>' + esc(r.outsourceCost ? won(r.outsourceCost) : "-") + '</strong></td>' + '<td class="num"><strong>' + esc(r.outsourceCost ? won(r.outsourceCost) : "-") + '</strong></td>'
+ '<td class="num"><strong>' + esc(won(r.recv || 0)) + '</strong></td>' + '<td class="num"><strong>' + esc(won(r.recv || 0)) + '</strong></td>'
@@ -267,6 +357,8 @@
+ '<td class="num"><strong style="color:' + (isSettledRow(r) ? '#b7aa93' : '#1a5645') + '">' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '</strong></td>' + '<td class="num"><strong style="color:' + (isSettledRow(r) ? '#b7aa93' : '#1a5645') + '">' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '</strong></td>'
+ '</tr>'; + '</tr>';
}).join(""); }).join("");
refreshFilterDom();
if (typeof syncColumnFilters === "function") syncColumnFilters(S.all);
} }
function renderCollectionBoard(r) { function renderCollectionBoard(r) {
@@ -379,10 +471,8 @@
} }
var years = bgEnsureYear(S.all); var years = bgEnsureYear(S.all);
var summary = bgSummarize(S.all, S.dashboard.year); var summary = bgSummarize(S.all, S.dashboard.year);
var rows = Array.isArray(S.rows) ? S.rows : []; var totals = summary.baronProjectTotals;
var visibleBaronProjectRows = rows.filter(isBaronProjectRow); var totalRate = totals.c > 0 ? (totals.col / totals.c) * 100 : 0;
var totals = bgTotals(visibleBaronProjectRows);
var totalRate = typeof rate === "function" ? rate("", totals.col, totals.col + totals.recv) : 0;
var toolbarHtml = '<div class="cards-toolbar">' var toolbarHtml = '<div class="cards-toolbar">'
+ '<div class="cards-toolbar-row">' + '<div class="cards-toolbar-row">'
+ years.map(function (year) { + years.map(function (year) {
@@ -393,14 +483,14 @@
+ '<div class="cards-toolbar-metrics">' + '<div class="cards-toolbar-metrics">'
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "active" ? "active" : "") + '" data-dashboard-section="active"><span class="label">' + esc(summary.targetYear) + '년 진행과업</span><span class="count">' + summary.activeRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">전년도 이월 사업 포함</span></button>' + '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "active" ? "active" : "") + '" data-dashboard-section="active"><span class="label">' + esc(summary.targetYear) + '년 진행과업</span><span class="count">' + summary.activeRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">전년도 이월 사업 포함</span></button>'
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "new" ? "active" : "") + '" data-dashboard-section="new"><span class="label">' + esc(summary.targetYear) + '년 신규프로젝트</span><span class="count">' + summary.newProjectRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">계약기간 시작년도 기준</span></button>' + '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "new" ? "active" : "") + '" data-dashboard-section="new"><span class="label">' + esc(summary.targetYear) + '년 신규프로젝트</span><span class="count">' + summary.newProjectRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">계약기간 시작년도 기준</span></button>'
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "completed" ? "active" : "") + '" data-dashboard-section="completed"><span class="label">' + esc(summary.targetYear) + '년 완료과업</span><span class="count">' + summary.completedRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">해당년도 종료 사업 기준</span></button>' + '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "completed" ? "active" : "") + '" data-dashboard-section="completed"><span class="label">' + esc(summary.targetYear) + '년 완료과업</span><span class="count">' + summary.completedRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">진행상태 준공 기준</span></button>'
+ "</div></div>"; + "</div></div>";
var cards = [ var cards = [
{ label: summary.targetYear + "년 프로젝트", value: visibleBaronProjectRows.length.toLocaleString("ko-KR") + " 건", note: "" }, { label: summary.targetYear + "년 프로젝트", value: summary.baronActiveRows.length.toLocaleString("ko-KR") + "건 (" + summary.baronSoftwareCount.toLocaleString("ko-KR") + "건)", note: "바론 수행중 프로젝트 / SW" },
{ label: "계약금", value: won(totals.c), note: "" }, { label: "계약금 (VAT별도)", value: won(totals.c), note: "" },
{ label: "수금액", value: won(totals.col), note: "" }, { label: "수금액", value: won(totals.col), note: "" },
{ label: "미수금", value: won(totals.recv), note: "" }, { label: "미수금", value: won(totals.recv), note: "" },
{ label: "수금률(%)", value: totalRate.toFixed(2) + "%", note: "" }, { label: "수금", value: totalRate.toFixed(2) + "%", note: "계약금 대비 수금액" },
{ label: "경영지원서비스 금액", value: won(summary.managementTotals.c), note: "", className: "management" } { label: "경영지원서비스 금액", value: won(summary.managementTotals.c), note: "", className: "management" }
]; ];
E.cards.innerHTML = toolbarHtml + cards.map(function (card) { E.cards.innerHTML = toolbarHtml + cards.map(function (card) {
@@ -429,9 +519,62 @@
S.rows = searched.filter(function (r) { S.rows = searched.filter(function (r) {
return bgMatches(r) && matchesColumnFilters(r); return bgMatches(r) && matchesColumnFilters(r);
}); });
S.rows.sort(compareDashboardRows);
render(); 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) { if (E.cards && !E.cards.dataset.dashboardBound) {
E.cards.dataset.dashboardBound = "true"; E.cards.dataset.dashboardBound = "true";
E.cards.addEventListener("click", function (event) { E.cards.addEventListener("click", function (event) {
@@ -472,6 +615,26 @@
}); });
} }
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 () { setTimeout(function () {
try { try {
filter(); filter();

View File

@@ -12,6 +12,7 @@ echo "[smoke] running 8081 endpoint checks"
docker exec mh-dashboard-organization-dev-backend-1 python - <<'PY' docker exec mh-dashboard-organization-dev-backend-1 python - <<'PY'
import sys import sys
import urllib.request import urllib.request
import json
checks = [ checks = [
@@ -43,6 +44,21 @@ for name, url, needle in checks:
except Exception as exc: except Exception as exc:
failed.append(f"{name}: {exc}") failed.append(f"{name}: {exc}")
try:
with urllib.request.urlopen("http://127.0.0.1:8000/api/integration/summary", timeout=8) as response:
payload = json.loads(response.read().decode())
counts = payload.get("counts") or {}
work_logs = int(counts.get("work_logs") or 0)
vouchers = int(counts.get("vouchers") or 0)
if work_logs <= 0:
failed.append(f"analysis-summary: work_logs is {work_logs}")
if vouchers <= 0:
failed.append(f"analysis-summary: vouchers is {vouchers}")
if work_logs > 0 and vouchers > 0:
print(f"[ok] analysis-summary -> work_logs={work_logs}, vouchers={vouchers}")
except Exception as exc:
failed.append(f"analysis-summary: {exc}")
if failed: if failed:
print("[smoke] failures detected:") print("[smoke] failures detected:")
for item in failed: for item in failed:

View File

@@ -9,6 +9,28 @@ DEV_PROJECT_NAME="${DEV_PROJECT_NAME:-mh-dashboard-organization-dev}"
DEV_COMPOSE_FILE="${DEV_COMPOSE_FILE:-${DEV_DIR}/docker-compose.8081.yml}" DEV_COMPOSE_FILE="${DEV_COMPOSE_FILE:-${DEV_DIR}/docker-compose.8081.yml}"
SCOPE="${1:-minimal}" SCOPE="${1:-minimal}"
ANALYSIS_TABLES=(
integration_import_batches
integration_raw_organization_rows
integration_raw_mh_rows
integration_raw_mh_pm_rows
integration_raw_payment_rows
integration_project_aliases
integration_project_category_mappings
integration_project_pm_assignments
integration_projects
integration_work_logs
integration_work_log_segments
integration_vouchers
)
MINIMAL_PRESERVE_TABLES=(
integration_project_pm_assignments
integration_work_logs
integration_work_log_segments
integration_vouchers
)
if [[ ! -f "${PROD_DIR}/docker-compose.yml" ]]; then if [[ ! -f "${PROD_DIR}/docker-compose.yml" ]]; then
echo "Production workspace not found: ${PROD_DIR}" >&2 echo "Production workspace not found: ${PROD_DIR}" >&2
exit 1 exit 1
@@ -38,35 +60,11 @@ case "${SCOPE}" in
) )
;; ;;
analysis) analysis)
TABLES=( TABLES=("${ANALYSIS_TABLES[@]}")
integration_import_batches
integration_raw_organization_rows
integration_raw_mh_rows
integration_raw_mh_pm_rows
integration_raw_payment_rows
integration_project_aliases
integration_project_category_mappings
integration_project_pm_assignments
integration_projects
integration_work_logs
integration_work_log_segments
integration_vouchers
)
;; ;;
full) full)
TABLES=( TABLES=(
integration_import_batches "${ANALYSIS_TABLES[@]}"
integration_raw_organization_rows
integration_raw_mh_rows
integration_raw_mh_pm_rows
integration_raw_payment_rows
integration_project_aliases
integration_project_category_mappings
integration_project_pm_assignments
integration_projects
integration_work_logs
integration_work_log_segments
integration_vouchers
member_aliases member_aliases
member_overrides member_overrides
member_retirements member_retirements
@@ -81,6 +79,16 @@ case "${SCOPE}" in
;; ;;
esac esac
PRESERVE_TABLES=()
if [[ "${SCOPE}" == "minimal" ]]; then
PRESERVE_TABLES=("${MINIMAL_PRESERVE_TABLES[@]}")
fi
DUMP_TABLES=("${TABLES[@]}")
if [[ ${#PRESERVE_TABLES[@]} -gt 0 ]]; then
DUMP_TABLES+=("${PRESERVE_TABLES[@]}")
fi
PROD_COMPOSE=(docker compose --project-directory "${PROD_DIR}") PROD_COMPOSE=(docker compose --project-directory "${PROD_DIR}")
DEV_COMPOSE=(docker compose -p "${DEV_PROJECT_NAME}" --env-file "${DEV_DIR}/.env" -f "${DEV_COMPOSE_FILE}") DEV_COMPOSE=(docker compose -p "${DEV_PROJECT_NAME}" --env-file "${DEV_DIR}/.env" -f "${DEV_COMPOSE_FILE}")
@@ -129,7 +137,7 @@ echo "[4/8] Building truncate script for ${SCOPE} scope"
echo "[5/8] Dumping ${SCOPE} data from 8080 source DB" echo "[5/8] Dumping ${SCOPE} data from 8080 source DB"
TABLE_ARGS=() TABLE_ARGS=()
for table in "${TABLES[@]}"; do for table in "${DUMP_TABLES[@]}"; do
TABLE_ARGS+=(-t "public.${table}") TABLE_ARGS+=(-t "public.${table}")
done done
run_compose "${PROD_DIR}" "${PROD_COMPOSE[@]}" exec -T db \ run_compose "${PROD_DIR}" "${PROD_COMPOSE[@]}" exec -T db \
@@ -193,7 +201,7 @@ echo "[7.8/8] Resetting serial sequences"
echo "SELECT setval(pg_get_serial_sequence('public.member_retirements', 'id'), COALESCE((SELECT MAX(id) FROM public.member_retirements), 1), true);" echo "SELECT setval(pg_get_serial_sequence('public.member_retirements', 'id'), COALESCE((SELECT MAX(id) FROM public.member_retirements), 1), true);"
echo "SELECT setval(pg_get_serial_sequence('public.seat_maps', 'id'), COALESCE((SELECT MAX(id) FROM public.seat_maps), 1), true);" echo "SELECT setval(pg_get_serial_sequence('public.seat_maps', 'id'), COALESCE((SELECT MAX(id) FROM public.seat_maps), 1), true);"
echo "SELECT setval(pg_get_serial_sequence('public.seat_slots', 'id'), COALESCE((SELECT MAX(id) FROM public.seat_slots), 1), true);" echo "SELECT setval(pg_get_serial_sequence('public.seat_slots', 'id'), COALESCE((SELECT MAX(id) FROM public.seat_slots), 1), true);"
if [[ "${SCOPE}" == "analysis" || "${SCOPE}" == "full" ]]; then if [[ "${SCOPE}" == "analysis" || "${SCOPE}" == "full" || "${#PRESERVE_TABLES[@]}" -gt 0 ]]; then
echo "SELECT setval(pg_get_serial_sequence('public.integration_import_batches', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_import_batches), 1), true);" echo "SELECT setval(pg_get_serial_sequence('public.integration_import_batches', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_import_batches), 1), true);"
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_organization_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_organization_rows), 1), true);" echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_organization_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_organization_rows), 1), true);"
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_mh_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_mh_rows), 1), true);" echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_mh_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_mh_rows), 1), true);"
@@ -236,7 +244,7 @@ UNION ALL
SELECT 'auth_users', COUNT(*)::text FROM auth.users SELECT 'auth_users', COUNT(*)::text FROM auth.users
ORDER BY table_name; ORDER BY table_name;
SQL SQL
if [[ "${SCOPE}" == "analysis" || "${SCOPE}" == "full" ]]; then if [[ "${SCOPE}" == "analysis" || "${SCOPE}" == "full" || "${#PRESERVE_TABLES[@]}" -gt 0 ]]; then
cat <<'SQL' cat <<'SQL'
SELECT 'integration_work_logs', COUNT(*)::text FROM public.integration_work_logs SELECT 'integration_work_logs', COUNT(*)::text FROM public.integration_work_logs
UNION ALL UNION ALL