Fix wehago compare loading and matching workflow

This commit is contained in:
b17301
2026-05-08 23:38:16 +09:00
parent 37f422b493
commit 99137e5f97
5 changed files with 5925 additions and 547 deletions
BIN
View File
Binary file not shown.
+646 -118
View File
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -4,6 +4,7 @@ set -euo pipefail
cd "$(dirname "$0")/.."
PORT="${1:-${INTRANET_PORT:-8010}}"
HEALTH_URL="http://127.0.0.1:${PORT}/health"
if [ ! -x ".venv/bin/python" ]; then
echo "가상환경이 없습니다. 먼저 아래를 실행하세요."
@@ -13,7 +14,19 @@ if [ ! -x ".venv/bin/python" ]; then
exit 1
fi
export INTRANET_AUTO_RELOAD="${INTRANET_AUTO_RELOAD:-1}"
if curl -fsS --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then
echo "이미 실행 중입니다: $HEALTH_URL"
exit 0
fi
if ss -ltn "( sport = :${PORT} )" | tail -n +2 | grep -q .; then
echo "포트 ${PORT} 는 이미 다른 프로세스가 사용 중입니다."
echo "기존 서버가 멈춘 상태라면 먼저 종료한 뒤 다시 실행하세요."
ss -ltnp "( sport = :${PORT} )" || true
exit 1
fi
export INTRANET_AUTO_RELOAD="${INTRANET_AUTO_RELOAD:-0}"
export INTRANET_PORT="$PORT"
exec ./.venv/bin/python main.py
+530 -59
View File
@@ -211,21 +211,27 @@
}
.status-grid {
display: flex;
gap: 10px;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 4px;
align-items: stretch;
}
.status-card {
min-width: 130px;
width: auto;
border: 1px solid rgba(217, 221, 227, 0.9);
border-radius: 999px;
background: rgba(255, 255, 255, 0.76);
padding: 10px 12px;
min-width: 0;
width: 100%;
border: 1px solid rgba(217, 221, 227, 0.92);
border-radius: 12px;
background: #fbfcfd;
padding: 7px 6px 8px;
text-align: left;
cursor: pointer;
transition: background-color 0.16s ease, color 0.16s ease, border-color 0.16s ease;
display: grid;
gap: 4px;
align-content: start;
justify-items: start;
box-shadow: none !important;
}
.status-card:hover,
@@ -233,14 +239,23 @@
background: #111827;
border-color: #111827;
color: #fff;
box-shadow: none !important;
}
.status-label {
display: block;
font-size: 12px;
width: 100%;
font-size: 9px;
font-weight: 700;
color: var(--muted);
margin-bottom: 4px;
color: #475569;
margin-bottom: 0;
text-transform: none;
letter-spacing: 0;
text-align: left;
line-height: 1.1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-card:hover .status-label,
@@ -250,10 +265,53 @@
.status-value {
display: block;
font-size: 26px;
width: 100%;
font-size: 18px;
font-weight: 800;
line-height: 1;
line-height: 1.05;
color: #111;
text-align: left;
}
.status-card[data-status="matched"] {
background: #f7faf8;
border-color: #d2ddd6;
}
.status-card[data-status="ledger_only"],
.status-card[data-status="voucher_only"],
.status-card[data-status="amount_mismatch"] {
background: #f7faf8;
border-color: #d2ddd6;
}
.status-card[data-status="voucher_matched"],
.status-card[data-status="erp_voucher_matched"],
.status-card[data-status="voucher_unmatched"],
.status-card[data-status="voucher_recheck"] {
background: #f5f8fb;
border-color: #c8d7e6;
}
.status-card[data-status="matched"]:hover,
.status-card[data-status="matched"][aria-expanded="true"],
.status-card[data-status="ledger_only"]:hover,
.status-card[data-status="ledger_only"][aria-expanded="true"],
.status-card[data-status="voucher_only"]:hover,
.status-card[data-status="voucher_only"][aria-expanded="true"],
.status-card[data-status="amount_mismatch"]:hover,
.status-card[data-status="amount_mismatch"][aria-expanded="true"],
.status-card[data-status="voucher_matched"]:hover,
.status-card[data-status="voucher_matched"][aria-expanded="true"],
.status-card[data-status="erp_voucher_matched"]:hover,
.status-card[data-status="erp_voucher_matched"][aria-expanded="true"],
.status-card[data-status="voucher_unmatched"]:hover,
.status-card[data-status="voucher_unmatched"][aria-expanded="true"],
.status-card[data-status="voucher_recheck"]:hover,
.status-card[data-status="voucher_recheck"][aria-expanded="true"] {
background: #111827;
border-color: #111827;
color: #fff;
}
.status-card:hover .status-value,
@@ -342,7 +400,8 @@
.table-wrap {
width: 100%;
max-width: 100%;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
border-top: 1px solid rgba(217, 221, 227, 0.9);
background: transparent;
max-height: 460px;
@@ -351,7 +410,7 @@
.detail-panel .table-wrap {
max-height: 360px;
overflow-x: auto;
overflow-x: hidden;
overflow-y: auto;
}
@@ -368,8 +427,8 @@
}
.table-wrap table {
width: max-content;
min-width: max(100%, 1040px);
width: 100%;
min-width: 0;
border-collapse: collapse;
table-layout: fixed;
}
@@ -384,6 +443,10 @@
line-height: 1.35;
}
.table-wrap td {
background: #fff;
}
.table-wrap th {
position: sticky;
top: 0;
@@ -391,10 +454,162 @@
background: #f8fafc;
font-size: 13px;
letter-spacing: 0.01em;
white-space: nowrap;
}
.table-wrap tr.bank-payable-case td,
.table-wrap tr.bank-payable-case:hover td,
.table-wrap tr.boundary-excluded-row td,
.table-wrap tr.boundary-excluded-row:hover td {
background: #fff;
color: #111827;
}
.voucher-group-list {
display: grid;
gap: 12px;
}
.voucher-group {
border: 1px solid rgba(217, 221, 227, 0.92);
border-radius: 12px;
overflow: hidden;
background: rgba(255, 255, 255, 0.86);
}
.voucher-group-head {
display: grid;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid rgba(217, 221, 227, 0.92);
background: #f8fafc;
}
.voucher-group-title {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.voucher-group-title strong {
font-size: 18px;
color: #111827;
}
.voucher-group-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.voucher-group-meta span {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid #d7e0ea;
background: #fff;
font-size: 12px;
color: #334155;
}
.voucher-group-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 8px;
font-size: 12px;
color: #475569;
}
.voucher-group-summary strong {
display: block;
margin-bottom: 4px;
color: #111827;
font-size: 12px;
}
.voucher-group-lines {
overflow: visible;
max-height: none;
}
.voucher-group-lines table {
width: 100%;
min-width: 0;
border-collapse: collapse;
table-layout: fixed;
}
.voucher-group-lines th,
.voucher-group-lines td {
padding: 7px 8px;
border-bottom: 1px solid rgba(148, 163, 184, 0.18);
font-size: 13px;
vertical-align: middle;
text-align: left;
line-height: 1.35;
}
/* Voucher body rows stay white; voucher grouping is expressed only by borders. */
.voucher-group-lines td {
background: #fff !important;
}
.voucher-group-lines th {
position: sticky;
top: 0;
z-index: 2;
background: #f8fafc;
color: inherit;
font-size: 13px;
letter-spacing: 0.01em;
white-space: nowrap;
}
.voucher-group-lines tr:last-child td {
border-bottom: 0;
}
.voucher-group-lines tr.bank-payable-case td,
.voucher-group-lines tr.bank-payable-case:hover td,
.voucher-group-lines tr.boundary-excluded-row td,
.voucher-group-lines tr.boundary-excluded-row:hover td {
background: #fff !important;
}
.voucher-table-group-start td {
border-top: 2px solid rgba(148, 163, 184, 0.62);
}
.case-stat {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 2px 8px;
border: 1px solid #bfd7c8;
border-radius: 999px;
background: #edf6f0;
color: #173522;
font-size: 12px;
font-weight: 800;
white-space: nowrap;
cursor: pointer;
box-shadow: none;
transform: none;
}
.case-stat:hover {
background: #dfeee4;
border-color: #9fc4ad;
}
.cell-ellipsis {
max-width: 180px;
width: 100%;
min-width: 0;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@@ -402,19 +617,67 @@
}
.cell-tight {
max-width: 86px;
max-width: clamp(56px, 5vw, 86px);
}
.cell-code {
max-width: 108px;
max-width: clamp(72px, 7vw, 108px);
}
.cell-equal {
max-width: 128px;
max-width: clamp(88px, 9vw, 148px);
}
.cell-desc {
max-width: 220px;
max-width: clamp(116px, 14vw, 220px);
}
.col-fiscal_year {
width: 4.5%;
}
.col-ledger_date,
.col-proof_date {
width: 6%;
}
.col-voucher_no,
.col-confirmed_no {
width: 5.5%;
}
.col-draft_no,
.col-account_code {
width: 7%;
}
.col-ledger_account_name,
.col-voucher_account_name {
width: 8%;
}
.col-ledger_vendor,
.col-voucher_vendor {
width: 10%;
}
.col-ledger_debit,
.col-ledger_credit,
.col-voucher_debit,
.col-voucher_credit,
.col-amount {
width: 7%;
}
.col-ledger_desc,
.col-voucher_desc,
.col-desc1,
.col-desc2,
.col-description,
.col-notes,
.col-review_memo,
.col-review_reason {
width: 12%;
}
.mono {
@@ -560,10 +823,8 @@
min-width: 106px;
}
.detail-panel[data-status-panel="matched"] .filter-form .suggest-field:has(input[name="wehago_vendor"]),
.detail-panel[data-status-panel="matched"] .filter-form .suggest-field:has(input[name="erp_vendor"]),
.detail-panel[data-status-panel="amount_mismatch"] .filter-form .suggest-field:has(input[name="wehago_vendor"]),
.detail-panel[data-status-panel="amount_mismatch"] .filter-form .suggest-field:has(input[name="erp_vendor"]) {
.detail-panel[data-status-panel="matched"] .filter-form .suggest-field-wide,
.detail-panel[data-status-panel="amount_mismatch"] .filter-form .suggest-field-wide {
flex: 1 1 220px;
width: auto;
min-width: 220px;
@@ -660,22 +921,22 @@
font-size: 13px;
}
body[data-view-mode="dual"] .voucher-page:has(#pairRecommendPanel.active) {
body[data-view-mode="dual"] .voucher-page.has-recommend-panel-open {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-items: start;
column-gap: 12px;
}
body[data-view-mode="dual"] .voucher-page:has(#pairRecommendPanel.active) > .voucher-topbar {
body[data-view-mode="dual"] .voucher-page.has-recommend-panel-open > .voucher-topbar {
grid-column: 1 / -1;
}
body[data-view-mode="dual"] .voucher-page:has(#pairRecommendPanel.active) > .panel-shell:not(#pairRecommendPanel) {
body[data-view-mode="dual"] .voucher-page.has-recommend-panel-open > .panel-shell:not(#pairRecommendPanel) {
grid-column: 1;
min-width: 0;
}
body[data-view-mode="dual"] .voucher-page:has(#pairRecommendPanel.active) > #pairRecommendPanel.active {
body[data-view-mode="dual"] .voucher-page.has-recommend-panel-open > #pairRecommendPanel.active {
grid-column: 2;
grid-row: 2;
display: grid;
@@ -694,6 +955,34 @@
max-height: clamp(300px, 44vh, 520px);
}
body[data-view-mode="dual"] .cell-code {
max-width: clamp(90px, 8vw, 132px);
}
body[data-view-mode="dual"] .cell-equal {
max-width: clamp(110px, 11vw, 180px);
}
body[data-view-mode="dual"] .cell-desc {
max-width: clamp(150px, 15vw, 280px);
}
body[data-view-mode="dual"] .col-ledger_vendor,
body[data-view-mode="dual"] .col-voucher_vendor {
width: 11%;
}
body[data-view-mode="dual"] .col-ledger_desc,
body[data-view-mode="dual"] .col-voucher_desc,
body[data-view-mode="dual"] .col-desc1,
body[data-view-mode="dual"] .col-desc2,
body[data-view-mode="dual"] .col-description,
body[data-view-mode="dual"] .col-notes,
body[data-view-mode="dual"] .col-review_memo,
body[data-view-mode="dual"] .col-review_reason {
width: 13%;
}
@media (max-width: 720px) {
.voucher-topbar,
.panel-header,
@@ -701,13 +990,9 @@
align-items: flex-start;
}
.status-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.status-card {
min-width: 0;
.status-card[data-status="voucher_matched"],
.status-card[data-status="erp_voucher_matched"] {
margin-left: 0;
}
.cell-ellipsis {
@@ -763,9 +1048,6 @@
</select>
<button type="submit" class="button-secondary">적용</button>
</form>
<form method="post" action="/wehago-compare/refresh?start_year={{ wehago_compare.selected_start_year or '' }}&end_year={{ wehago_compare.selected_end_year or '' }}">
<button type="submit">WEHAGO 새로 읽기</button>
</form>
<button type="button" class="button-secondary" id="undoLastActionBtn">직전 작업 취소</button>
<span class="panel-meta" id="lastActionMeta">
{% if wehago_compare.last_action %}
@@ -825,11 +1107,11 @@
</div>
<input type="text" name="wehago_amount" placeholder="WEHAGO 금액">
<input type="text" name="erp_amount" placeholder="Hanmac 금액">
<div class="suggest-field">
<div class="suggest-field suggest-field-wide">
<input type="text" name="wehago_vendor" placeholder="WEHAGO 거래처" autocomplete="off">
<div class="suggest-dropdown" data-wehago-vendor-suggest="{{ section.key }}"></div>
</div>
<div class="suggest-field">
<div class="suggest-field suggest-field-wide">
<input type="text" name="erp_vendor" placeholder="Hanmac 거래처" autocomplete="off">
<div class="suggest-dropdown" data-erp-vendor-suggest="{{ section.key }}"></div>
</div>
@@ -960,10 +1242,88 @@
return classes.join(' ');
};
const getColumnClass = (field) => `col-${String(field || '').replace(/[^a-zA-Z0-9_]/g, '_')}`;
const renderVoucherGroups = (container, payload, append = false, statusKey = '') => {
if (!container) return;
const groups = payload.groups || [];
if (!groups.length && !append) {
container.innerHTML = '<div class="table-placeholder">조건에 맞는 항목이 없습니다.</div>';
return;
}
const lineColumns = [
['fiscal_year', '연도'],
['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 적요'],
];
const formatValue = (field, raw) => {
if (field === 'fiscal_year') return String(raw ?? '');
if (typeof raw === 'number') {
return Number.isInteger(raw)
? raw.toLocaleString()
: raw.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
return String(raw ?? '');
};
const lineHead = lineColumns.map(([field, label]) => `<th class="${getColumnClass(field)}">${escapeHtml(label)}</th>`).join('');
const lineBody = groups.map((group, groupIndex) => {
const rows = Array.isArray(group.rows) ? group.rows : [];
return rows.map((row, rowIndex) => {
const rowClasses = [];
if (rowIndex === 0) {
rowClasses.push('voucher-table-group-start');
}
if (row.matched_case === 'boundary_excluded' || row.boundary_excluded === '1') {
rowClasses.push('boundary-excluded-row');
} else if (row.matched_case === 'bank_payable' || row.review_reason === 'BANK_PAYABLE_MATCH') {
rowClasses.push('bank-payable-case');
}
const cells = lineColumns.map(([field]) => {
const display = formatValue(field, row[field]);
return `<td class="${getColumnClass(field)}" title="${escapeHtml(display)}"><span class="${getCellClass(field, ['voucher_no'])}">${escapeHtml(display)}</span></td>`;
}).join('');
return `<tr class="${rowClasses.join(' ')}">${cells}</tr>`;
}).join('');
}).join('');
const tableHtml = `
<table>
<thead><tr>${lineHead}</tr></thead>
<tbody>${lineBody}</tbody>
</table>
`;
if (!append || !container.querySelector('.voucher-group-lines')) {
container.innerHTML = `<div class="voucher-group-lines">${tableHtml}</div>`;
} else {
const tbody = container.querySelector('.voucher-group-lines tbody');
const temp = document.createElement('tbody');
temp.innerHTML = lineBody;
if (tbody) {
tbody.insertAdjacentHTML('beforeend', temp.innerHTML);
} else {
container.innerHTML = `<div class="voucher-group-lines">${tableHtml}</div>`;
}
}
};
const renderTable = (container, payload, monoFields = [], append = false, statusKey = '') => {
if (!container) {
return;
}
if (statusKey === 'voucher_matched' || statusKey === 'erp_voucher_matched' || statusKey === 'voucher_unmatched' || statusKey === 'voucher_recheck') {
renderVoucherGroups(container, payload, append, statusKey);
return;
}
const columns = payload.columns || [];
const rows = payload.rows || [];
if (!rows.length && !append) {
@@ -974,7 +1334,7 @@
const includePairCheckbox = statusKey === 'ledger_only' || statusKey === 'voucher_only';
const head = [
(includeReviewCheckbox || includePairCheckbox) ? '<th></th>' : '',
...columns.map(([, label]) => `<th>${escapeHtml(label)}</th>`),
...columns.map(([field, label]) => `<th class="${getColumnClass(field)}">${escapeHtml(label)}</th>`),
].join('');
const body = rows.map((row) => {
const reviewKey = row.review_key || '';
@@ -1006,15 +1366,23 @@
display = String(raw);
}
const className = getCellClass(field, monoFields);
return `<td title="${escapeHtml(display)}"><span class="${className}">${escapeHtml(display)}</span></td>`;
return `<td class="${getColumnClass(field)}" title="${escapeHtml(display)}"><span class="${className}">${escapeHtml(display)}</span></td>`;
}).join('');
const checkboxCell = includeReviewCheckbox
? `<td><input type="checkbox" data-review-key="${escapeHtml(reviewKey)}" ${recheckSelections.get(reviewKey) ? 'checked' : ''}></td>`
: includePairCheckbox
? `<td><input type="checkbox" data-pair-key="${escapeHtml(pairKey)}" data-pair-status="${escapeHtml(statusKey)}" ${pairSelections.get(pairKey) ? 'checked' : ''}></td>`
: '';
const rowClasses = [];
if (row.matched_case === 'bank_payable' || row.review_reason === 'BANK_PAYABLE_MATCH') {
rowClasses.push('bank-payable-case');
}
if (row.matched_case === 'boundary_excluded' || row.boundary_excluded === '1') {
rowClasses.push('boundary-excluded-row');
}
const rowClass = rowClasses.length ? ` class="${rowClasses.join(' ')}"` : '';
return `<tr${rowClass}>${checkboxCell}${cells}</tr>`;
}).join('');
const checkboxCell = includeReviewCheckbox
? `<td><input type="checkbox" data-review-key="${escapeHtml(reviewKey)}" ${recheckSelections.get(reviewKey) ? 'checked' : ''}></td>`
: includePairCheckbox
? `<td><input type="checkbox" data-pair-key="${escapeHtml(pairKey)}" data-pair-status="${escapeHtml(statusKey)}" ${pairSelections.get(pairKey) ? 'checked' : ''}></td>`
: '';
return `<tr>${checkboxCell}${cells}</tr>`;
}).join('');
if (!append || !container.querySelector('table')) {
container.innerHTML = `<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
} else {
@@ -1043,7 +1411,7 @@
}
};
const setMeta = (key, totalCount, shownCount, notice = '') => {
const setMeta = (key, totalCount, shownCount, notice = '', stats = {}) => {
const target = document.querySelector(`[data-result-meta="${key}"]`);
if (target) {
const countText = `조회 결과 ${Number(totalCount || 0).toLocaleString()}`;
@@ -1051,7 +1419,15 @@
? ` / 현재 ${Number(shownCount || 0).toLocaleString()}`
: '';
const noticeText = notice ? ` / ${notice}` : '';
target.textContent = `${countText}${shownText}${noticeText}`;
const bankPayableCount = Number(stats.bank_payable_case_count || 0);
const bankPayableText = key === 'matched' && bankPayableCount
? ` <button type="button" class="case-stat" data-case-filter="bank_payable">보통예금/미지급금 case : ${bankPayableCount.toLocaleString()}</button>`
: '';
const boundaryCount = Number(stats.boundary_excluded_count || 0);
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}`;
}
};
@@ -1064,6 +1440,43 @@
return payload;
};
const applySummaryPayload = (payload) => {
const sections = Array.isArray(payload?.metric_sections) ? payload.metric_sections : [];
sections.forEach((section) => {
const card = document.querySelector(`.status-card[data-status="${escapeSelectorValue(section.key || '')}"]`);
const valueNode = card?.querySelector('.status-value');
if (valueNode) {
valueNode.textContent = Number(section.count || 0).toLocaleString();
}
});
const lastAction = payload?.last_action;
if (lastActionMeta) {
if (lastAction && lastAction.id) {
lastActionMeta.textContent = `최근 작업: ${lastAction.action_type} / ${Number(lastAction.count || 0).toLocaleString()}`;
} else {
lastActionMeta.textContent = '최근 작업: 없음';
}
}
if (undoLastActionBtn) {
undoLastActionBtn.disabled = !(lastAction && lastAction.id);
}
};
const loadDashboardSummary = async (attempt = 0) => {
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/summary?${params.toString()}`);
applySummaryPayload(payload);
if (payload?.pending && attempt < 6) {
window.setTimeout(() => loadDashboardSummary(attempt + 1), 1200);
}
} catch (_error) {
// Keep the page usable even if the summary loads later or fails once.
}
};
const buildQuery = (form, extra = {}) => {
const params = new URLSearchParams();
if (startYearValue) params.set('start_year', startYearValue);
@@ -1113,6 +1526,11 @@
selectedPairKey: '',
};
const syncRecommendationPanelState = () => {
if (!page || !recommendPanel) return;
page.classList.toggle('has-recommend-panel-open', recommendPanel.classList.contains('active'));
};
const refreshLastActionMeta = async () => {
if (!lastActionMeta || !undoLastActionBtn) return;
try {
@@ -1162,7 +1580,7 @@
const initStatusSuggest = (form, type) => {
if (!form) return;
const status = form.dataset.status || '';
if (!['matched', 'amount_mismatch', 'ledger_only', 'voucher_only'].includes(status)) return;
if (!['matched', 'amount_mismatch', 'ledger_only', 'voucher_only', 'voucher_matched', 'erp_voucher_matched', 'voucher_unmatched', 'voucher_recheck'].includes(status)) return;
const inputName = type === 'account'
? 'account_keyword'
: type === 'wehago_account'
@@ -1359,6 +1777,8 @@
vendor_keyword: '거래처',
amount_keyword: '금액',
desc_keyword: '적요',
review_reason: '검증근거',
boundary_excluded: '연초/연말 대체·이월',
};
const getFormFieldValues = (form) => {
@@ -1373,6 +1793,14 @@
return values;
};
const syncFormWithActiveFilters = (statusKey, form) => {
if (!form) return;
const active = cumulativeFiltersByStatus.get(statusKey) || {};
form.querySelectorAll('input[name]').forEach((input) => {
input.value = active[input.name] || '';
});
};
const updateCumulativeFilter = (statusKey, fieldName, value, checked = true) => {
const current = { ...(cumulativeFiltersByStatus.get(statusKey) || {}) };
if (checked && String(value || '').trim()) {
@@ -1457,6 +1885,7 @@
<th>계정유사도</th>
<th>거래처유사도</th>
<th>신뢰도</th>
<th>검토</th>
<th>연도</th>
<th>전표번호</th>
<th>일자</th>
@@ -1489,6 +1918,7 @@
<td title="${escapeHtml((Number(pair.account_similarity || 0) * 100).toFixed(1) + '%')}"><span class="cell-ellipsis cell-tight">${escapeHtml((Number(pair.account_similarity || 0) * 100).toFixed(1) + '%')}</span></td>
<td title="${escapeHtml((Number(pair.vendor_similarity || 0) * 100).toFixed(1) + '%')}"><span class="cell-ellipsis cell-tight">${escapeHtml((Number(pair.vendor_similarity || 0) * 100).toFixed(1) + '%')}</span></td>
<td title="${escapeHtml(pair.confidence_level)}"><span class="cell-ellipsis cell-tight">${escapeHtml(pair.confidence_level)}</span></td>
<td title="${escapeHtml(pair.substitution_hint || pair.reason || '')}"><span class="cell-ellipsis cell-desc">${escapeHtml(pair.substitution_hint || pair.reason || '')}</span></td>
<td title="${escapeHtml(ledger.fiscal_year || voucher.fiscal_year || '')}"><span class="cell-ellipsis cell-tight">${escapeHtml(ledger.fiscal_year || voucher.fiscal_year || '')}</span></td>
<td title="${escapeHtml(ledger.voucher_no || '')}"><span class="cell-ellipsis cell-code">${escapeHtml(ledger.voucher_no || '')}</span></td>
<td title="${escapeHtml(ledger.ledger_date || '')}"><span class="cell-ellipsis cell-code">${escapeHtml(ledger.ledger_date || '')}</span></td>
@@ -1566,12 +1996,14 @@
if (!recommendPanel) return;
setRecommendMode(mode);
recommendPanel.classList.add('active');
syncRecommendationPanelState();
recommendPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
const closeRecommendationPanel = () => {
if (!recommendPanel) return;
recommendPanel.classList.remove('active');
syncRecommendationPanelState();
};
const getSelectedSourceForIndividual = (sourceStatus) => {
@@ -1699,7 +2131,7 @@
};
const buildStatusQuery = (form, statusKey, extra = {}) => {
const selected = form ? (cumulativeFiltersByStatus.get(statusKey) || {}) : {};
const selected = cumulativeFiltersByStatus.get(statusKey) || {};
const mapped = new URLSearchParams();
if (startYearValue) mapped.set('start_year', startYearValue);
if (endYearValue) mapped.set('end_year', endYearValue);
@@ -1712,9 +2144,11 @@
const account = String(selected.account_keyword || '').trim();
const vendor = String(selected.vendor_keyword || '').trim();
const amount = String(selected.amount_keyword || '').trim();
const boundaryExcluded = String(selected.boundary_excluded || '').trim();
if (account) mapped.set('wehago_account', account);
if (vendor) mapped.set('wehago_vendor', vendor);
if (amount) mapped.set('wehago_amount', amount);
if (boundaryExcluded) mapped.set('boundary_excluded', boundaryExcluded);
} else if (statusKey === 'voucher_only') {
const account = String(selected.account_keyword || '').trim();
const vendor = String(selected.vendor_keyword || '').trim();
@@ -1722,6 +2156,22 @@
if (account) mapped.set('erp_account', account);
if (vendor) mapped.set('erp_vendor', vendor);
if (amount) mapped.set('erp_amount', amount);
} else if (statusKey === 'voucher_matched' || statusKey === 'erp_voucher_matched' || statusKey === 'voucher_unmatched' || statusKey === 'voucher_recheck') {
const account = String(selected.account_keyword || '').trim();
const vendor = String(selected.vendor_keyword || '').trim();
const amount = String(selected.amount_keyword || '').trim();
if (account) {
mapped.set('wehago_account', account);
mapped.set('erp_account', account);
}
if (vendor) {
mapped.set('wehago_vendor', vendor);
mapped.set('erp_vendor', vendor);
}
if (amount) {
mapped.set('wehago_amount', amount);
mapped.set('erp_amount', amount);
}
} else {
for (const [key, value] of Object.entries(selected)) {
if (String(value).trim()) {
@@ -1744,8 +2194,7 @@
if (current.loading) return;
if (!append && form) {
const selected = getFormFieldValues(form);
const merged = { ...(cumulativeFiltersByStatus.get(statusKey) || {}), ...selected };
cumulativeFiltersByStatus.set(statusKey, merged);
cumulativeFiltersByStatus.set(statusKey, selected);
renderActiveFilterCards(statusKey);
}
const nextOffset = append ? (current.nextOffset || 0) : 0;
@@ -1767,7 +2216,7 @@
form,
shownCount,
});
setMeta(statusKey, payload.total_count, shownCount, payload.notice);
setMeta(statusKey, payload.total_count, shownCount, payload.notice, payload);
setLoadMoreState(statusKey, payload.has_more, false);
} catch (error) {
if (!append) {
@@ -1796,6 +2245,7 @@
panel.classList.add('active');
card.setAttribute('aria-expanded', 'true');
const form = panel.querySelector('form[data-filter-kind="status"]');
syncFormWithActiveFilters(statusKey, form);
loadStatusRows(statusKey, form, false);
}
});
@@ -1818,6 +2268,23 @@
});
document.addEventListener('click', (event) => {
const caseButton = event.target.closest('[data-case-filter="bank_payable"], [data-case-filter="boundary_excluded"]');
if (caseButton) {
const caseFilter = caseButton.dataset.caseFilter || '';
const statusKey = caseFilter === 'boundary_excluded' ? 'ledger_only' : 'matched';
const current = { ...(cumulativeFiltersByStatus.get(statusKey) || {}) };
if (caseFilter === 'boundary_excluded') {
current.boundary_excluded = '1';
} else {
current.review_reason = 'BANK_PAYABLE_MATCH';
}
cumulativeFiltersByStatus.set(statusKey, current);
renderActiveFilterCards(statusKey);
const form = document.querySelector(`form[data-filter-kind="status"][data-status="${statusKey}"]`);
syncFormWithActiveFilters(statusKey, form);
loadStatusRows(statusKey, null, false);
return;
}
const removeButton = event.target.closest('[data-remove-active-filter]');
if (!removeButton) return;
const statusKey = removeButton.dataset.removeActiveFilter || '';
@@ -2105,6 +2572,7 @@
const startYear = document.getElementById('start_year');
const endYear = document.getElementById('end_year');
syncRecommendationPanelState();
const syncYearOptions = () => {
if (!startYear || !endYear) {
return;
@@ -2126,6 +2594,9 @@
endYear.addEventListener('change', syncYearOptions);
syncYearOptions();
}
window.setTimeout(() => {
loadDashboardSummary();
}, 400);
refreshLastActionMeta();
})();
</script>
+4735 -369
View File
File diff suppressed because it is too large Load Diff