Refactor data loading and query projections
This commit is contained in:
+189
-18
@@ -16,7 +16,7 @@
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.voucher-title-wrap h2 {
|
||||
@@ -947,6 +947,38 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.voucher-detail-wrap {
|
||||
max-height: 65vh;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.voucher-detail-wrap table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.voucher-detail-wrap th,
|
||||
.voucher-detail-wrap td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.voucher-detail-wrap td.cell-desc,
|
||||
.voucher-detail-wrap th.cell-desc {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.voucher-summary-action {
|
||||
min-width: 64px;
|
||||
}
|
||||
|
||||
.bridge-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
@@ -1439,10 +1471,7 @@
|
||||
|
||||
<section class="panel-shell">
|
||||
<div class="panel-header">
|
||||
<div><h2>현황</h2></div>
|
||||
<div class="panel-meta">
|
||||
기간 <strong>{{ wehago_compare.selected_start_year or '-' }} ~ {{ wehago_compare.selected_end_year or '-' }}</strong>
|
||||
</div>
|
||||
<div><h2>기간 {{ wehago_compare.selected_start_year or '-' }} ~ {{ wehago_compare.selected_end_year or '-' }}</h2></div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="status-grid">
|
||||
@@ -1613,6 +1642,21 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="bridge-modal" id="voucherDetailModal" aria-hidden="true">
|
||||
<div class="bridge-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="voucherDetailModalTitle">
|
||||
<div class="bridge-modal-head">
|
||||
<div class="bridge-modal-title">
|
||||
<strong id="voucherDetailModalTitle">전표 상세</strong>
|
||||
<p id="voucherDetailModalMeta">선택한 전표의 세부 행입니다.</p>
|
||||
</div>
|
||||
<button type="button" class="button-secondary" id="voucherDetailCloseBtn">닫기</button>
|
||||
</div>
|
||||
<div class="voucher-detail-wrap" id="voucherDetailTableWrap">
|
||||
<div class="table-placeholder">상세 데이터를 불러오지 않았습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bridge-modal" id="bridgeReviewModal" aria-hidden="true">
|
||||
<div class="bridge-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="bridgeReviewModalTitle">
|
||||
<div class="bridge-modal-head">
|
||||
@@ -1718,6 +1762,14 @@
|
||||
const recheckRowStore = new Map();
|
||||
const voucherRecheckSelections = new Map();
|
||||
const voucherRecheckRowStore = new Map();
|
||||
const voucherGroupDetailStore = new Map();
|
||||
const summaryRefreshState = { timerId: null, pending: false };
|
||||
const detailRetryTimers = new Map();
|
||||
const voucherDetailModal = document.getElementById('voucherDetailModal');
|
||||
const voucherDetailCloseBtn = document.getElementById('voucherDetailCloseBtn');
|
||||
const voucherDetailTableWrap = document.getElementById('voucherDetailTableWrap');
|
||||
const voucherDetailModalTitle = document.getElementById('voucherDetailModalTitle');
|
||||
const voucherDetailModalMeta = document.getElementById('voucherDetailModalMeta');
|
||||
const pairSelections = new Map();
|
||||
const pairLedgerStore = new Map();
|
||||
const pairVoucherStore = new Map();
|
||||
@@ -1754,10 +1806,83 @@
|
||||
|
||||
const getColumnClass = (field) => `col-${String(field || '').replace(/[^a-zA-Z0-9_]/g, '_')}`;
|
||||
|
||||
const renderVoucherDetailTable = (group) => {
|
||||
if (!voucherDetailTableWrap) return;
|
||||
const rows = Array.isArray(group?.rows) ? group.rows : [];
|
||||
const lineColumns = [
|
||||
['fiscal_year', '연도'],
|
||||
['status_label', '구분'],
|
||||
['ledger_date', 'WEHAGO 일자'],
|
||||
['voucher_no', '전표번호'],
|
||||
['draft_no', '가전표번호'],
|
||||
['ledger_account_name', 'WEHAGO 계정'],
|
||||
['voucher_account_name', 'ERP 계정'],
|
||||
['ledger_vendor', 'WEHAGO 거래처'],
|
||||
['voucher_vendor', 'ERP 거래처'],
|
||||
['ledger_debit', 'WEHAGO 차변'],
|
||||
['ledger_credit', 'WEHAGO 대변'],
|
||||
['voucher_debit', 'ERP 차변'],
|
||||
['voucher_credit', 'ERP 대변'],
|
||||
['ledger_desc', 'WEHAGO 적요'],
|
||||
['voucher_desc', 'ERP 적요'],
|
||||
];
|
||||
if (!rows.length) {
|
||||
voucherDetailTableWrap.innerHTML = '<div class="table-placeholder">표시할 상세 행이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
const head = lineColumns.map(([field, label]) => `<th class="${getColumnClass(field)} ${getCellClass(field, ['voucher_no'])}">${escapeHtml(label)}</th>`).join('');
|
||||
const body = rows.map((row) => {
|
||||
const cells = lineColumns.map(([field]) => {
|
||||
const raw = row[field];
|
||||
const display = typeof raw === 'number'
|
||||
? (Number.isInteger(raw) ? raw.toLocaleString() : raw.toLocaleString(undefined, { maximumFractionDigits: 2 }))
|
||||
: String(raw ?? '');
|
||||
return `<td class="${getColumnClass(field)} ${getCellClass(field, ['voucher_no'])}" title="${escapeHtml(display)}">${escapeHtml(display)}</td>`;
|
||||
}).join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
}).join('');
|
||||
voucherDetailTableWrap.innerHTML = `<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
|
||||
};
|
||||
|
||||
const openVoucherDetailModal = (groupKey) => {
|
||||
const group = voucherGroupDetailStore.get(groupKey);
|
||||
if (!group || !voucherDetailModal) return;
|
||||
const summary = group.summary || {};
|
||||
if (voucherDetailModalTitle) {
|
||||
voucherDetailModalTitle.textContent = `${summary.voucher_no || '전표'} 상세`;
|
||||
}
|
||||
if (voucherDetailModalMeta) {
|
||||
const bits = [
|
||||
summary.ledger_date || '',
|
||||
summary.draft_no || '',
|
||||
`WEHAGO ${Number(summary.ledger_row_count || 0).toLocaleString()}행`,
|
||||
`ERP ${Number(summary.voucher_row_count || 0).toLocaleString()}행`,
|
||||
].filter(Boolean);
|
||||
voucherDetailModalMeta.textContent = bits.join(' / ');
|
||||
}
|
||||
renderVoucherDetailTable(group);
|
||||
voucherDetailModal.classList.add('active');
|
||||
voucherDetailModal.setAttribute('aria-hidden', 'false');
|
||||
};
|
||||
|
||||
const closeVoucherDetailModal = () => {
|
||||
if (!voucherDetailModal) return;
|
||||
voucherDetailModal.classList.remove('active');
|
||||
voucherDetailModal.setAttribute('aria-hidden', 'true');
|
||||
};
|
||||
|
||||
voucherDetailCloseBtn?.addEventListener('click', closeVoucherDetailModal);
|
||||
voucherDetailModal?.addEventListener('click', (event) => {
|
||||
if (event.target === voucherDetailModal) {
|
||||
closeVoucherDetailModal();
|
||||
}
|
||||
});
|
||||
|
||||
const renderVoucherGroups = (container, payload, append = false, statusKey = '') => {
|
||||
if (!container) return;
|
||||
const groups = payload.groups || [];
|
||||
const includeReviewCheckbox = statusKey === 'voucher_recheck';
|
||||
const pendingPlaceholder = String(payload?.notice || '').trim() || '조건에 맞는 항목이 없습니다.';
|
||||
if (includeReviewCheckbox && !append) {
|
||||
voucherRecheckSelections.clear();
|
||||
voucherRecheckRowStore.clear();
|
||||
@@ -1815,8 +1940,18 @@
|
||||
['ledger_desc', 'WEHAGO 적요'],
|
||||
['voucher_desc', 'ERP 적요'],
|
||||
];
|
||||
const stripAccountCodePrefix = (value) => {
|
||||
const text = String(value ?? '');
|
||||
return text
|
||||
.split(',')
|
||||
.map((part) => part.trim().replace(/^\d+\s+/, ''))
|
||||
.join(', ');
|
||||
};
|
||||
const formatValue = (field, raw) => {
|
||||
if (field === 'fiscal_year') return String(raw ?? '');
|
||||
if (['ledger_account_name', 'voucher_account_name', 'ledger_accounts', 'voucher_accounts'].includes(field)) {
|
||||
return stripAccountCodePrefix(raw);
|
||||
}
|
||||
if (typeof raw === 'number') {
|
||||
return Number.isInteger(raw)
|
||||
? raw.toLocaleString()
|
||||
@@ -1825,16 +1960,16 @@
|
||||
return String(raw ?? '');
|
||||
};
|
||||
if (!groups.length) {
|
||||
const summaryRows = Array.isArray(payload.rows) ? payload.rows : [];
|
||||
if (!summaryRows.length && !append) {
|
||||
container.innerHTML = '<div class="table-placeholder">조건에 맞는 항목이 없습니다.</div>';
|
||||
const fallbackRows = Array.isArray(payload.rows) ? payload.rows : [];
|
||||
if (!fallbackRows.length && !append) {
|
||||
container.innerHTML = `<div class="table-placeholder">${escapeHtml(pendingPlaceholder)}</div>`;
|
||||
return;
|
||||
}
|
||||
const lineHead = [
|
||||
includeReviewCheckbox ? '<th class="selection-col">선택</th>' : '',
|
||||
...lineColumns.map(([field, label]) => `<th class="${getColumnClass(field)}">${escapeHtml(label)}</th>`),
|
||||
].join('');
|
||||
const summaryBody = summaryRows.map((row, summaryIndex) => {
|
||||
const fallbackBody = fallbackRows.map((row, summaryIndex) => {
|
||||
const groupKey = `${row.fiscal_year || ''}|${row.voucher_no || ''}|${row.draft_no || ''}|${row.ledger_date || ''}|${row.proof_date || ''}|${summaryIndex}`;
|
||||
const cells = lineColumns.map(([field]) => {
|
||||
const display = formatValue(field, row[field]);
|
||||
@@ -1848,20 +1983,20 @@
|
||||
}
|
||||
return `<tr>${checkboxCell}${cells}</tr>`;
|
||||
}).join('');
|
||||
const summaryTable = `
|
||||
const fallbackTable = `
|
||||
<table>
|
||||
<thead><tr>${lineHead}</tr></thead>
|
||||
<tbody>${summaryBody}</tbody>
|
||||
<tbody>${fallbackBody}</tbody>
|
||||
</table>
|
||||
`;
|
||||
if (!append || !container.querySelector('.voucher-group-lines')) {
|
||||
container.innerHTML = `<div class="voucher-group-lines">${summaryTable}</div>`;
|
||||
container.innerHTML = `<div class="voucher-group-lines">${fallbackTable}</div>`;
|
||||
} else {
|
||||
const tbody = container.querySelector('.voucher-group-lines tbody');
|
||||
if (tbody) {
|
||||
tbody.insertAdjacentHTML('beforeend', summaryBody);
|
||||
tbody.insertAdjacentHTML('beforeend', fallbackBody);
|
||||
} else {
|
||||
container.innerHTML = `<div class="voucher-group-lines">${summaryTable}</div>`;
|
||||
container.innerHTML = `<div class="voucher-group-lines">${fallbackTable}</div>`;
|
||||
}
|
||||
}
|
||||
bindVoucherRecheckCheckboxes();
|
||||
@@ -1930,7 +2065,7 @@
|
||||
const columns = payload.columns || [];
|
||||
const rows = payload.rows || [];
|
||||
if (!rows.length && !append) {
|
||||
container.innerHTML = '<div class="table-placeholder">조건에 맞는 항목이 없습니다.</div>';
|
||||
container.innerHTML = `<div class="table-placeholder">${escapeHtml(String(payload?.notice || '').trim() || '조건에 맞는 항목이 없습니다.')}</div>`;
|
||||
return;
|
||||
}
|
||||
const includeReviewCheckbox = statusKey === 'amount_mismatch';
|
||||
@@ -2021,6 +2156,14 @@
|
||||
const shownText = shownCount
|
||||
? ` / 현재 ${Number(shownCount || 0).toLocaleString()}건`
|
||||
: '';
|
||||
const readyYears = Array.isArray(stats.ready_years) ? stats.ready_years : [];
|
||||
const pendingYears = Array.isArray(stats.pending_years) ? stats.pending_years : [];
|
||||
const readyText = readyYears.length
|
||||
? ` / 준비 연도 ${readyYears.join(', ')}`
|
||||
: '';
|
||||
const pendingText = pendingYears.length
|
||||
? ` / 갱신 중 ${pendingYears.join(', ')}`
|
||||
: '';
|
||||
const noticeText = notice ? ` / ${notice}` : '';
|
||||
const bankPayableCount = Number(stats.bank_payable_case_count || 0);
|
||||
const bankPayableText = key === 'matched' && bankPayableCount
|
||||
@@ -2030,7 +2173,7 @@
|
||||
const boundaryText = key === 'ledger_only' && boundaryCount
|
||||
? ` <button type="button" class="case-stat" data-case-filter="boundary_excluded">연초/연말 대체·이월 : ${boundaryCount.toLocaleString()}</button>`
|
||||
: '';
|
||||
target.innerHTML = `${escapeHtml(`${countText}${shownText}${noticeText}`)}${bankPayableText}${boundaryText}`;
|
||||
target.innerHTML = `${escapeHtml(`${countText}${shownText}${readyText}${pendingText}${noticeText}`)}${bankPayableText}${boundaryText}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2193,6 +2336,17 @@
|
||||
if (undoLastActionBtn) {
|
||||
undoLastActionBtn.disabled = !(lastAction && lastAction.id);
|
||||
}
|
||||
const pending = Boolean(payload?.pending);
|
||||
summaryRefreshState.pending = pending;
|
||||
if (summaryRefreshState.timerId) {
|
||||
clearTimeout(summaryRefreshState.timerId);
|
||||
summaryRefreshState.timerId = null;
|
||||
}
|
||||
if (pending) {
|
||||
summaryRefreshState.timerId = window.setTimeout(() => {
|
||||
loadDashboardSummary();
|
||||
}, 4000);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSnapshotStatus = (payload) => {
|
||||
@@ -3140,6 +3294,10 @@
|
||||
const loadStatusRows = async (statusKey, form = null, append = false) => {
|
||||
const wrap = document.querySelector(`[data-table-wrap="${statusKey}"]`);
|
||||
if (!wrap) return;
|
||||
if (detailRetryTimers.has(statusKey)) {
|
||||
clearTimeout(detailRetryTimers.get(statusKey));
|
||||
detailRetryTimers.delete(statusKey);
|
||||
}
|
||||
const current = detailState.get(statusKey) || { offset: 0, totalCount: 0, loading: false };
|
||||
if (current.loading) return;
|
||||
if (!append && form) {
|
||||
@@ -3147,20 +3305,26 @@
|
||||
cumulativeFiltersByStatus.set(statusKey, selected);
|
||||
renderActiveFilterCards(statusKey);
|
||||
}
|
||||
const usesCursorPaging = ['voucher_matched', 'erp_voucher_matched', 'voucher_unmatched', 'erp_voucher_unmatched', 'voucher_recheck'].includes(statusKey);
|
||||
const nextOffset = append ? (current.nextOffset || 0) : 0;
|
||||
const nextCursor = append ? String(current.nextCursor || '') : '';
|
||||
detailState.set(statusKey, { ...current, loading: true, form });
|
||||
setLoadMoreState(statusKey, true, true);
|
||||
if (!append) {
|
||||
wrap.innerHTML = '<div class="table-placeholder">조회 중입니다...</div>';
|
||||
}
|
||||
try {
|
||||
const pageLimit = ['voucher_matched', 'erp_voucher_matched', 'voucher_unmatched', 'erp_voucher_unmatched', 'voucher_recheck'].includes(statusKey) ? 24 : 60;
|
||||
const payload = await fetchJson(`/wehago-compare/api/status-rows?${buildStatusQuery(form, statusKey, { offset: nextOffset, limit: pageLimit })}`);
|
||||
const pageLimit = usesCursorPaging ? 24 : 60;
|
||||
const extra = usesCursorPaging
|
||||
? { offset: append ? nextOffset : 0, cursor: nextCursor, limit: pageLimit }
|
||||
: { offset: nextOffset, limit: pageLimit };
|
||||
const payload = await fetchJson(`/wehago-compare/api/status-rows?${buildStatusQuery(form, statusKey, extra)}`);
|
||||
renderTable(wrap, payload, ['voucher_no'], append, statusKey);
|
||||
const shownCount = append ? nextOffset + payload.shown_count : payload.shown_count;
|
||||
detailState.set(statusKey, {
|
||||
offset: payload.offset,
|
||||
nextOffset: payload.next_offset,
|
||||
nextCursor: payload.next_cursor || '',
|
||||
totalCount: payload.total_count,
|
||||
hasMore: payload.has_more,
|
||||
loading: false,
|
||||
@@ -3169,6 +3333,13 @@
|
||||
});
|
||||
setMeta(statusKey, payload.total_count, shownCount, payload.notice, payload);
|
||||
setLoadMoreState(statusKey, payload.has_more, false);
|
||||
const rebuildingNotice = Boolean(payload?.pending) || String(payload.notice || '').includes('최신 전표 스냅샷을 갱신 중');
|
||||
if (!append && rebuildingNotice) {
|
||||
detailRetryTimers.set(statusKey, window.setTimeout(() => {
|
||||
detailRetryTimers.delete(statusKey);
|
||||
loadStatusRows(statusKey, form, false);
|
||||
}, 4000));
|
||||
}
|
||||
} catch (error) {
|
||||
if (!append) {
|
||||
wrap.innerHTML = `<div class="table-placeholder">${escapeHtml(error.message)}</div>`;
|
||||
|
||||
Reference in New Issue
Block a user