Update intranet tools and voucher comparison

This commit is contained in:
b17301
2026-06-04 09:00:51 +09:00
parent 2ab74bac88
commit b93289bfa8
55 changed files with 32809 additions and 1063 deletions
+159
View File
@@ -25,6 +25,49 @@
align-items: end;
}
.summary-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.page-job-status {
display: inline-flex;
align-items: center;
min-height: 32px;
max-width: 320px;
padding: 0 10px;
border: 1px solid #d8e2ec;
border-radius: 999px;
background: #f8fafc;
color: #475569;
font-size: 12px;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.page-job-status[data-state="queued"],
.page-job-status[data-state="running"] {
border-color: #bae6fd;
background: #f0f9ff;
color: #075985;
}
.page-job-status[data-state="done"] {
border-color: #bbf7d0;
background: #f0fdf4;
color: #166534;
}
.page-job-status[data-state="failed"] {
border-color: #fecaca;
background: #fef2f2;
color: #991b1b;
}
.legend {
display: flex;
flex-wrap: wrap;
@@ -207,6 +250,10 @@
<section class="panel">
<div class="section-title" style="margin-bottom: 14px;">
<h2>수익/비용 현황</h2>
<div class="summary-actions">
<button type="button" class="button-secondary" id="annualRebuildCacheBtn">캐시 재계산</button>
<span class="page-job-status" id="annualJobStatus" data-state="">작업 상태 확인 전</span>
</div>
</div>
<div class="summary-panel">
<div class="summary-overview">
@@ -253,6 +300,7 @@
let yearlySeries = [];
let monthlySeries = [];
let availableYears = [];
let annualJobPollTimer = null;
const annualMetricCards = {{ annual_metric_cards | tojson }};
const annualExpenseChartMetrics = {{ annual_expense_chart_metrics | tojson }};
const annualBalanceChartMetrics = {{ annual_balance_chart_metrics | tojson }};
@@ -356,6 +404,89 @@
return String(availableYears[availableYears.length - 1] || "recent10");
}
async function fetchAnnualJson(url, options = {}) {
const response = await fetch(url, {
...options,
cache: "no-store",
credentials: "same-origin",
headers: {
"Accept": "application/json",
"Cache-Control": "no-cache",
"X-Requested-With": "XMLHttpRequest",
...(options.headers || {}),
},
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.error || payload?.ok === false) {
throw new Error(payload?.error || `HTTP ${response.status}`);
}
return payload;
}
function describeAnnualJob(job) {
if (!job) return { text: "최근 작업 없음", state: "" };
const status = String(job.status || "");
const message = job.error_message || job.message || "";
if (status === "queued") return { text: "작업 대기 중", state: status };
if (status === "running") return { text: `계산 중... ${message}`.trim(), state: status };
if (status === "done") return { text: "계산 완료", state: status };
if (status === "failed") return { text: `실패: ${message || "오류"}`, state: status };
return { text: message || status || "작업 상태 확인 전", state: status };
}
function renderAnnualJob(job) {
const jobStatus = document.getElementById("annualJobStatus");
const rebuildButton = document.getElementById("annualRebuildCacheBtn");
if (!jobStatus) return;
const view = describeAnnualJob(job);
jobStatus.textContent = view.text;
jobStatus.dataset.state = view.state || "";
if (rebuildButton) {
rebuildButton.disabled = view.state === "queued" || view.state === "running";
}
}
async function loadLatestAnnualJob() {
const params = new URLSearchParams({
page_key: "annual_summary",
job_type: "annual_summary_bootstrap",
});
const payload = await fetchAnnualJson(`/api/system-jobs/latest?${params.toString()}`);
renderAnnualJob(payload.job || null);
return payload.job || null;
}
function pollAnnualJob(jobId) {
if (!jobId) return;
if (annualJobPollTimer) window.clearInterval(annualJobPollTimer);
annualJobPollTimer = window.setInterval(async () => {
try {
const payload = await fetchAnnualJson(`/api/system-jobs/${encodeURIComponent(jobId)}`);
const job = payload.job || null;
renderAnnualJob(job);
if (!job || ["done", "failed", "cancelled"].includes(String(job.status || ""))) {
window.clearInterval(annualJobPollTimer);
annualJobPollTimer = null;
if (job && job.status === "done") {
window.setTimeout(() => window.location.reload(), 600);
}
}
} catch (_error) {
// Keep existing charts visible during transient lock or network delays.
}
}, 2500);
}
async function requestAnnualCacheRebuild() {
const rebuildButton = document.getElementById("annualRebuildCacheBtn");
if (rebuildButton) rebuildButton.disabled = true;
const payload = await fetchAnnualJson("/annual-summary/api/rebuild-cache", {
method: "POST",
});
renderAnnualJob(payload.job || null);
pollAnnualJob(payload.job?.id);
}
async function loadAnnualSummaryBootstrapData() {
const response = await fetch("/annual-summary/bootstrap-data", {
method: "GET",
@@ -590,6 +721,21 @@
document.getElementById("granularity").addEventListener("change", renderAll);
document.getElementById("yearFilter").addEventListener("change", renderAll);
document.getElementById("annualRebuildCacheBtn")?.addEventListener("click", async () => {
const confirmed = window.confirm("연도별 수익/비용 데이터를 서버에서 다시 계산할까요? 계산 중에도 다른 화면을 사용할 수 있습니다.");
if (!confirmed) return;
try {
await requestAnnualCacheRebuild();
} catch (error) {
const jobStatus = document.getElementById("annualJobStatus");
if (jobStatus) {
jobStatus.textContent = error.message || "작업 등록 실패";
jobStatus.dataset.state = "failed";
}
const rebuildButton = document.getElementById("annualRebuildCacheBtn");
if (rebuildButton) rebuildButton.disabled = false;
}
});
const granularitySelect = document.getElementById("granularity");
if (granularitySelect) {
granularitySelect.value = "yearly";
@@ -605,6 +751,19 @@
console.error("연도별 수익/비용 부트스트랩 데이터 조회 에러", error);
}
renderAll();
loadLatestAnnualJob()
.then((job) => {
if (job && ["queued", "running"].includes(String(job.status || ""))) {
pollAnnualJob(job.id);
}
})
.catch(() => {
const jobStatus = document.getElementById("annualJobStatus");
if (jobStatus) {
jobStatus.textContent = "작업 상태 조회 실패";
jobStatus.dataset.state = "failed";
}
});
})();
</script>
{% endblock %}