Update WEHAGO comparison data and tools
This commit is contained in:
@@ -217,6 +217,157 @@
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.snapshot-panel {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 10px 12px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.snapshot-panel[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.snapshot-panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.snapshot-panel-title {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.snapshot-panel-title strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.snapshot-policy {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.snapshot-summary-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.snapshot-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #dbe3eb;
|
||||
background: #f8fafc;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.snapshot-badge strong {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.snapshot-years {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.snapshot-year-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 8px 9px;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.snapshot-year-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.snapshot-year-head strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.snapshot-state-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.snapshot-state-pill.ready { background: #ecfdf5; border-color: #bbf7d0; color: #166534; }
|
||||
.snapshot-state-pill.queued { background: #eff6ff; border-color: #bfdbfe; color: #1d4ed8; }
|
||||
.snapshot-state-pill.running { background: #eff6ff; border-color: #93c5fd; color: #1d4ed8; }
|
||||
.snapshot-state-pill.stale { background: #fff7ed; border-color: #fed7aa; color: #c2410c; }
|
||||
.snapshot-state-pill.failed { background: #fef2f2; border-color: #fecaca; color: #b91c1c; }
|
||||
.snapshot-state-pill.missing { background: #f8fafc; border-color: #cbd5e1; color: #475569; }
|
||||
|
||||
.snapshot-year-meta,
|
||||
.snapshot-job-meta {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.snapshot-jobs {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.snapshot-job-row {
|
||||
border-top: 1px solid #eef2f7;
|
||||
padding-top: 6px;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.snapshot-job-row:first-child {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.snapshot-job-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.icon-button-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-button-label svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
@@ -1048,7 +1199,14 @@
|
||||
</select>
|
||||
<button type="submit" class="button-secondary">적용</button>
|
||||
</form>
|
||||
<button type="button" class="button-secondary" id="undoLastActionBtn">직전 작업 취소</button>
|
||||
<button type="button" class="button-secondary" id="snapshotToggleBtn" aria-expanded="false" aria-controls="snapshotPanel">스냅샷 상태</button>
|
||||
<button type="button" class="button-secondary button-icon" id="undoLastActionBtn" aria-label="직전 작업 취소" title="직전 작업 취소">
|
||||
<span class="icon-button-label" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M15 18l-6-6 6-6"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<span class="panel-meta" id="lastActionMeta">
|
||||
{% if wehago_compare.last_action %}
|
||||
최근 작업: {{ wehago_compare.last_action.action_type }} / {{ "{:,}".format(wehago_compare.last_action.count or 0) }}건
|
||||
@@ -1155,6 +1313,22 @@
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<section class="snapshot-panel" id="snapshotPanel" hidden>
|
||||
<div class="snapshot-panel-head">
|
||||
<div class="snapshot-panel-title">
|
||||
<strong>스냅샷 상태</strong>
|
||||
<div class="snapshot-policy" id="snapshotPolicyText">전표 스냅샷 상태를 불러오는 중입니다.</div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
|
||||
<div class="panel-meta" id="snapshotSummaryMeta"></div>
|
||||
<button type="button" class="button-secondary" id="snapshotRebuildBtn">현재 기간 다시 준비</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="snapshot-summary-badges" id="snapshotSummaryBadges"></div>
|
||||
<div class="snapshot-years" id="snapshotYears"></div>
|
||||
<div class="snapshot-jobs" id="snapshotJobs"></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1440,6 +1614,15 @@
|
||||
return payload;
|
||||
};
|
||||
|
||||
const stateLabelMap = {
|
||||
ready: '준비됨',
|
||||
queued: '대기중',
|
||||
running: '생성중',
|
||||
stale: '구버전',
|
||||
failed: '실패',
|
||||
missing: '없음',
|
||||
};
|
||||
|
||||
const applySummaryPayload = (payload) => {
|
||||
const sections = Array.isArray(payload?.metric_sections) ? payload.metric_sections : [];
|
||||
sections.forEach((section) => {
|
||||
@@ -1462,6 +1645,84 @@
|
||||
}
|
||||
};
|
||||
|
||||
const renderSnapshotStatus = (payload) => {
|
||||
const snapshotPolicyText = document.getElementById('snapshotPolicyText');
|
||||
const snapshotSummaryMeta = document.getElementById('snapshotSummaryMeta');
|
||||
const snapshotSummaryBadges = document.getElementById('snapshotSummaryBadges');
|
||||
const snapshotYears = document.getElementById('snapshotYears');
|
||||
const snapshotJobs = document.getElementById('snapshotJobs');
|
||||
if (!snapshotPolicyText || !snapshotSummaryMeta || !snapshotSummaryBadges || !snapshotYears || !snapshotJobs) {
|
||||
return;
|
||||
}
|
||||
const aggregate = payload?.aggregate || payload?.snapshot_aggregate || {};
|
||||
const policy = payload?.snapshot_policy || {};
|
||||
const years = Array.isArray(payload?.years || payload?.snapshot_years) ? (payload.years || payload.snapshot_years) : [];
|
||||
const recentJobs = Array.isArray(payload?.recent_jobs) ? payload.recent_jobs : [];
|
||||
const modeSummary = Array.isArray(policy.year_modes)
|
||||
? policy.year_modes.map((item) => `${item.year}:${item.mode}`).join(', ')
|
||||
: '';
|
||||
snapshotPolicyText.textContent = `백필 시작 ${policy.backfill_hint_start_year || '-'} / 운영 연도 ${policy.rolling_update_start_year || '-'}+ / 월별 반영 기준 익월 ${policy.monthly_expected_ready_day || '-'}일 이후`;
|
||||
snapshotSummaryMeta.textContent = modeSummary || '연도별 스냅샷 정책 정보가 없습니다.';
|
||||
const badgeOrder = ['ready', 'queued', 'running', 'stale', 'failed', 'missing'];
|
||||
snapshotSummaryBadges.innerHTML = badgeOrder.map((key) => {
|
||||
const label = stateLabelMap[key] || key;
|
||||
const value = Number(aggregate[key] || 0).toLocaleString();
|
||||
return `<span class="snapshot-badge"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></span>`;
|
||||
}).join('');
|
||||
snapshotYears.innerHTML = years.length
|
||||
? years.map((item) => {
|
||||
const stateKey = String(item.state || 'missing');
|
||||
const counts = item.row_counts || {};
|
||||
const summaryBits = [
|
||||
`M ${Number(counts.matched || 0).toLocaleString()}`,
|
||||
`U ${Number(counts.ledger_only || 0).toLocaleString()}`,
|
||||
`R ${Number(counts.amount_mismatch || 0).toLocaleString()}`,
|
||||
`E ${Number(counts.voucher_only || 0).toLocaleString()}`,
|
||||
].join(' / ');
|
||||
const metaBits = [
|
||||
item.mode ? `mode ${item.mode}` : '',
|
||||
item.last_built_at ? `built ${item.last_built_at}` : '',
|
||||
item.last_requested_at ? `req ${item.last_requested_at}` : '',
|
||||
item.cache_state ? `cache ${item.cache_state}` : '',
|
||||
].filter(Boolean).join(' / ');
|
||||
return `
|
||||
<article class="snapshot-year-card">
|
||||
<div class="snapshot-year-head">
|
||||
<strong>${escapeHtml(item.fiscal_year)}</strong>
|
||||
<span class="snapshot-state-pill ${escapeHtml(stateKey)}">${escapeHtml(stateLabelMap[stateKey] || stateKey)}</span>
|
||||
</div>
|
||||
<div class="snapshot-year-meta">${escapeHtml(summaryBits)}</div>
|
||||
<div class="snapshot-year-meta">${escapeHtml(metaBits)}</div>
|
||||
${item.error_message ? `<div class="snapshot-year-meta" style="color:#b91c1c;">${escapeHtml(item.error_message)}</div>` : ''}
|
||||
</article>
|
||||
`;
|
||||
}).join('')
|
||||
: '<div class="panel-meta">표시할 연도 상태가 없습니다.</div>';
|
||||
snapshotJobs.innerHTML = recentJobs.length
|
||||
? recentJobs.slice(0, 8).map((job) => {
|
||||
const bits = [];
|
||||
if (job.job_type === 'year_snapshot_rebuild' && job.payload?.fiscal_year) {
|
||||
bits.push(`연도 ${job.payload.fiscal_year}`);
|
||||
}
|
||||
if (job.job_type === 'metric_counts_rebuild') {
|
||||
bits.push(`범위 ${job.payload?.start_year ?? '-'}~${job.payload?.end_year ?? '-'}`);
|
||||
}
|
||||
bits.push(`우선순위 ${job.priority ?? 0}`);
|
||||
return `
|
||||
<div class="snapshot-job-row">
|
||||
<div class="snapshot-job-head">
|
||||
<strong>${escapeHtml(job.job_type)}</strong>
|
||||
<span class="snapshot-state-pill ${escapeHtml(job.state || 'missing')}">${escapeHtml(stateLabelMap[job.state] || job.state || '')}</span>
|
||||
</div>
|
||||
<div class="snapshot-job-meta">${escapeHtml(bits.join(' / '))}</div>
|
||||
<div class="snapshot-job-meta">${escapeHtml([job.created_at, job.started_at, job.finished_at].filter(Boolean).join(' / '))}</div>
|
||||
${job.error_message ? `<div class="snapshot-job-meta" style="color:#b91c1c;">${escapeHtml(job.error_message)}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('')
|
||||
: '<div class="panel-meta">최근 재생성 이력이 없습니다.</div>';
|
||||
};
|
||||
|
||||
const loadDashboardSummary = async (attempt = 0) => {
|
||||
const params = new URLSearchParams();
|
||||
if (startYearValue) params.set('start_year', startYearValue);
|
||||
@@ -1469,6 +1730,7 @@
|
||||
try {
|
||||
const payload = await fetchJson(`/wehago-compare/api/summary?${params.toString()}`);
|
||||
applySummaryPayload(payload);
|
||||
renderSnapshotStatus(payload);
|
||||
if (payload?.pending && attempt < 6) {
|
||||
window.setTimeout(() => loadDashboardSummary(attempt + 1), 1200);
|
||||
}
|
||||
@@ -1477,6 +1739,30 @@
|
||||
}
|
||||
};
|
||||
|
||||
const loadSnapshotStatus = async () => {
|
||||
const params = new URLSearchParams();
|
||||
if (startYearValue) params.set('start_year', startYearValue);
|
||||
if (endYearValue) params.set('end_year', endYearValue);
|
||||
try {
|
||||
const payload = await fetchJson(`/wehago-compare/api/snapshot-status?${params.toString()}`);
|
||||
renderSnapshotStatus(payload);
|
||||
snapshotStatusLoaded = true;
|
||||
return payload;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const setSnapshotPanelOpen = async (open) => {
|
||||
if (!snapshotPanel || !snapshotToggleBtn) return;
|
||||
snapshotPanel.hidden = !open;
|
||||
snapshotToggleBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
snapshotToggleBtn.textContent = open ? '스냅샷 상태 숨기기' : '스냅샷 상태';
|
||||
if (open && !snapshotStatusLoaded) {
|
||||
await loadSnapshotStatus();
|
||||
}
|
||||
};
|
||||
|
||||
const buildQuery = (form, extra = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (startYearValue) params.set('start_year', startYearValue);
|
||||
@@ -1509,11 +1795,15 @@
|
||||
const recommendClose = document.getElementById('recommendClose');
|
||||
const recommendSortTools = document.getElementById('recommendSortTools');
|
||||
const recommendSortButtons = Array.from(document.querySelectorAll('[data-recommend-sort]'));
|
||||
const snapshotToggleBtn = document.getElementById('snapshotToggleBtn');
|
||||
const snapshotPanel = document.getElementById('snapshotPanel');
|
||||
const undoLastActionBtn = document.getElementById('undoLastActionBtn');
|
||||
const lastActionMeta = document.getElementById('lastActionMeta');
|
||||
const snapshotRebuildBtn = document.getElementById('snapshotRebuildBtn');
|
||||
const recommendSelections = new Map();
|
||||
let recommendSortKey = 'score';
|
||||
let recommendMode = 'batch';
|
||||
let snapshotStatusLoaded = false;
|
||||
const individualState = {
|
||||
sourceStatus: '',
|
||||
sourceRow: null,
|
||||
@@ -2551,6 +2841,45 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshotRebuildBtn) {
|
||||
snapshotRebuildBtn.addEventListener('click', async () => {
|
||||
const confirmed = window.confirm('현재 기간의 전표 스냅샷과 현황 캐시를 다시 준비할까요?');
|
||||
if (!confirmed) return;
|
||||
snapshotRebuildBtn.disabled = true;
|
||||
try {
|
||||
const response = await fetch('/wehago-compare/api/snapshot-rebuild', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
start_year: startYearValue || null,
|
||||
end_year: endYearValue || null,
|
||||
include_metric_counts: true,
|
||||
}),
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || '스냅샷 재생성 요청 중 오류가 발생했습니다.');
|
||||
}
|
||||
await loadSnapshotStatus();
|
||||
window.setTimeout(() => loadDashboardSummary(), 1200);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
} finally {
|
||||
snapshotRebuildBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshotToggleBtn) {
|
||||
snapshotToggleBtn.addEventListener('click', async () => {
|
||||
const nextOpen = snapshotPanel ? snapshotPanel.hidden : true;
|
||||
await setSnapshotPanelOpen(nextOpen);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-table-wrap]').forEach((wrap) => {
|
||||
const statusKey = wrap.dataset.tableWrap;
|
||||
if (!statusKey) {
|
||||
|
||||
Reference in New Issue
Block a user