2604 lines
107 KiB
HTML
2604 lines
107 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}전표비교{% endblock %}
|
||
|
||
{% block head_extra %}
|
||
<style>
|
||
.voucher-page {
|
||
display: grid;
|
||
gap: 14px;
|
||
}
|
||
|
||
.voucher-topbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
padding-bottom: 14px;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.voucher-title-wrap h2 {
|
||
margin: 0;
|
||
}
|
||
|
||
.voucher-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: nowrap;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.voucher-toolbar form,
|
||
.filter-form {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: nowrap;
|
||
margin: 0;
|
||
overflow: visible;
|
||
}
|
||
|
||
.voucher-toolbar label,
|
||
.filter-form label {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.voucher-toolbar select,
|
||
.voucher-toolbar button,
|
||
.filter-form input,
|
||
.filter-form button,
|
||
.upload-picker {
|
||
height: 38px;
|
||
box-sizing: border-box;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.voucher-toolbar select,
|
||
.filter-form input {
|
||
min-width: 104px;
|
||
padding: 0 30px 0 10px;
|
||
font-size: 14px;
|
||
line-height: 38px;
|
||
}
|
||
|
||
.filter-form input {
|
||
min-width: 120px;
|
||
padding-right: 10px;
|
||
}
|
||
|
||
.detail-head .filter-form {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
min-width: 0;
|
||
overflow: visible;
|
||
row-gap: 0;
|
||
}
|
||
|
||
.detail-head .filter-form input {
|
||
min-width: 98px;
|
||
}
|
||
|
||
.detail-panel[data-status-panel="amount_mismatch"] .detail-head {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr);
|
||
align-items: start;
|
||
row-gap: 6px;
|
||
overflow: visible;
|
||
}
|
||
|
||
.detail-panel[data-status-panel="amount_mismatch"] .detail-head > strong {
|
||
grid-column: 1 / -1;
|
||
grid-row: 1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.detail-panel[data-status-panel="amount_mismatch"] .detail-head > .filter-form {
|
||
grid-column: 1 / -1;
|
||
grid-row: 2;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
min-width: 0;
|
||
overflow: visible;
|
||
padding-bottom: 2px;
|
||
}
|
||
|
||
.detail-panel[data-status-panel="amount_mismatch"] .detail-head > .result-meta {
|
||
grid-column: 1 / -1;
|
||
grid-row: 3;
|
||
}
|
||
|
||
.active-filter-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.active-filter-card {
|
||
min-width: 0;
|
||
border: 1px solid #d3dee8;
|
||
border-radius: 10px;
|
||
padding: 6px 8px;
|
||
background: #f8fafc;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 6px;
|
||
align-items: center;
|
||
}
|
||
|
||
.active-filter-card .value {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
font-size: 12px;
|
||
color: #0f172a;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.active-filter-card .remove {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 999px;
|
||
border: 0;
|
||
background: rgba(15, 23, 42, 0.08);
|
||
color: #111827;
|
||
box-shadow: none;
|
||
transform: none;
|
||
padding: 0;
|
||
cursor: pointer;
|
||
line-height: 1;
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.active-filter-card .remove:hover {
|
||
background: rgba(15, 23, 42, 0.14);
|
||
}
|
||
|
||
.panel-shell {
|
||
border: 0;
|
||
border-top: 1px solid var(--line);
|
||
border-radius: 0;
|
||
background: transparent;
|
||
overflow: hidden;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.voucher-page > .panel-shell:first-of-type {
|
||
border-top: 0;
|
||
}
|
||
|
||
.voucher-page > .panel-shell:first-of-type > .panel-header {
|
||
padding-top: 0;
|
||
}
|
||
|
||
.panel-header {
|
||
padding: 14px 0 10px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.panel-header h2,
|
||
.summary-copy h2 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.panel-meta,
|
||
.empty-note,
|
||
.result-meta {
|
||
color: var(--muted);
|
||
line-height: 1.5;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 0 0 16px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.status-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||
gap: 4px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.status-card {
|
||
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,
|
||
.status-card[aria-expanded="true"] {
|
||
background: #111827;
|
||
border-color: #111827;
|
||
color: #fff;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
.status-label {
|
||
display: block;
|
||
width: 100%;
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
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,
|
||
.status-card[aria-expanded="true"] .status-label {
|
||
color: rgba(255, 255, 255, 0.82);
|
||
}
|
||
|
||
.status-value {
|
||
display: block;
|
||
width: 100%;
|
||
font-size: 18px;
|
||
font-weight: 800;
|
||
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,
|
||
.status-card[aria-expanded="true"] .status-value {
|
||
color: #fff;
|
||
}
|
||
|
||
.detail-stack {
|
||
display: grid;
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.detail-panel {
|
||
display: none;
|
||
border: 0;
|
||
border-top: 1px solid var(--line);
|
||
border-radius: 0;
|
||
background: transparent;
|
||
overflow: visible;
|
||
min-width: 0;
|
||
}
|
||
|
||
.detail-panel.active {
|
||
display: block;
|
||
}
|
||
|
||
.detail-head {
|
||
padding: 14px 0 10px;
|
||
border-bottom: 1px solid rgba(217, 221, 227, 0.9);
|
||
background: transparent;
|
||
display: grid;
|
||
gap: 10px;
|
||
position: relative;
|
||
z-index: 6;
|
||
overflow: visible;
|
||
}
|
||
|
||
.data-panel summary {
|
||
list-style: none;
|
||
cursor: pointer;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 14px 0;
|
||
}
|
||
|
||
.data-panel summary::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
|
||
.summary-badges {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.summary-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
border-radius: 999px;
|
||
padding: 5px 9px;
|
||
background: #f3f4f6;
|
||
color: #111827;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.summary-toggle {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.data-panel[open] .summary-toggle::after {
|
||
content: "접기";
|
||
}
|
||
|
||
.data-panel:not([open]) .summary-toggle::after {
|
||
content: "펼치기";
|
||
}
|
||
|
||
.table-wrap {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
border-top: 1px solid rgba(217, 221, 227, 0.9);
|
||
background: transparent;
|
||
max-height: 460px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.detail-panel .table-wrap {
|
||
max-height: 360px;
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.table-footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 10px 0 14px;
|
||
border-top: 1px solid rgba(217, 221, 227, 0.9);
|
||
background: transparent;
|
||
}
|
||
|
||
.load-more-button {
|
||
min-width: 120px;
|
||
}
|
||
|
||
.table-wrap table {
|
||
width: 100%;
|
||
min-width: 0;
|
||
border-collapse: collapse;
|
||
table-layout: fixed;
|
||
}
|
||
|
||
.table-wrap th,
|
||
.table-wrap td {
|
||
border-bottom: 1px solid rgba(148, 163, 184, 0.18);
|
||
padding: 7px 8px;
|
||
vertical-align: middle;
|
||
text-align: left;
|
||
font-size: 13px;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.table-wrap td {
|
||
background: #fff;
|
||
}
|
||
|
||
.table-wrap th {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
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 {
|
||
width: 100%;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
display: block;
|
||
}
|
||
|
||
.cell-tight {
|
||
max-width: clamp(56px, 5vw, 86px);
|
||
}
|
||
|
||
.cell-code {
|
||
max-width: clamp(72px, 7vw, 108px);
|
||
}
|
||
|
||
.cell-equal {
|
||
max-width: clamp(88px, 9vw, 148px);
|
||
}
|
||
|
||
.cell-desc {
|
||
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 {
|
||
font-family: "Consolas", "Courier New", monospace;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.upload-form {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: nowrap;
|
||
overflow-x: auto;
|
||
padding-bottom: 12px;
|
||
}
|
||
|
||
.upload-picker {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 0 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--line);
|
||
background: #fff;
|
||
cursor: pointer;
|
||
font-weight: 700;
|
||
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
||
}
|
||
|
||
.upload-picker:hover {
|
||
border-color: #9ca3af;
|
||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
||
}
|
||
|
||
.upload-picker::before {
|
||
content: "";
|
||
width: 15px;
|
||
height: 11px;
|
||
border: 2px solid currentColor;
|
||
border-top-left-radius: 3px;
|
||
border-top-right-radius: 3px;
|
||
border-bottom-left-radius: 2px;
|
||
border-bottom-right-radius: 2px;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
display: inline-block;
|
||
transform: translateY(1px);
|
||
clip-path: polygon(0 28%, 22% 28%, 30% 0, 100% 0, 100% 100%, 0 100%);
|
||
}
|
||
|
||
.hidden-input {
|
||
display: none;
|
||
}
|
||
|
||
.table-placeholder {
|
||
padding: 16px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.recommend-panel {
|
||
display: none;
|
||
}
|
||
|
||
.recommend-panel.active {
|
||
display: block;
|
||
}
|
||
|
||
.recommend-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
white-space: normal;
|
||
word-break: keep-all;
|
||
overflow-wrap: break-word;
|
||
}
|
||
|
||
.recommend-sort {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
white-space: normal;
|
||
word-break: keep-all;
|
||
overflow-wrap: break-word;
|
||
}
|
||
|
||
.recommend-sort button.active {
|
||
background: #111827;
|
||
border-color: #111827;
|
||
color: #fff;
|
||
}
|
||
|
||
.recommend-summary {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.recommend-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
border-radius: 999px;
|
||
padding: 4px 10px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
background: #f3f4f6;
|
||
color: #111827;
|
||
}
|
||
|
||
.recommend-badge.good {
|
||
background: #ecfdf3;
|
||
color: #166534;
|
||
}
|
||
|
||
.recommend-badge.warn {
|
||
background: #fff7ed;
|
||
color: #9a3412;
|
||
}
|
||
|
||
.recommend-panel .panel-header h2 {
|
||
margin: 0;
|
||
white-space: normal;
|
||
word-break: keep-all;
|
||
overflow-wrap: break-word;
|
||
}
|
||
|
||
.suggest-field {
|
||
position: relative;
|
||
width: 132px;
|
||
min-width: 132px;
|
||
}
|
||
|
||
.suggest-field > input[type="text"] {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.detail-panel[data-status-panel="matched"] .filter-form input[name="wehago_amount"],
|
||
.detail-panel[data-status-panel="matched"] .filter-form input[name="erp_amount"],
|
||
.detail-panel[data-status-panel="amount_mismatch"] .filter-form input[name="wehago_amount"],
|
||
.detail-panel[data-status-panel="amount_mismatch"] .filter-form input[name="erp_amount"] {
|
||
width: 106px;
|
||
min-width: 106px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.detail-panel[data-status-panel="ledger_only"] .filter-form .suggest-field,
|
||
.detail-panel[data-status-panel="voucher_only"] .filter-form .suggest-field,
|
||
.detail-panel[data-status-panel="ledger_only"] .filter-form input[name="amount_keyword"],
|
||
.detail-panel[data-status-panel="voucher_only"] .filter-form input[name="amount_keyword"] {
|
||
width: 140px;
|
||
min-width: 140px;
|
||
}
|
||
|
||
.suggest-dropdown {
|
||
position: absolute;
|
||
top: calc(100% + 4px);
|
||
left: 0;
|
||
right: auto;
|
||
width: 100%;
|
||
min-width: 100%;
|
||
max-width: 100%;
|
||
box-sizing: border-box;
|
||
background: #fff;
|
||
border: 1px solid var(--line);
|
||
border-radius: 10px;
|
||
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.14);
|
||
max-height: 260px;
|
||
overflow: auto;
|
||
z-index: 50;
|
||
display: none;
|
||
}
|
||
|
||
.suggest-dropdown.open {
|
||
display: block;
|
||
}
|
||
|
||
.suggest-item {
|
||
width: 100%;
|
||
text-align: left;
|
||
padding: 3px 4px;
|
||
margin: 0;
|
||
border: 0;
|
||
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
|
||
background: #fff;
|
||
color: #111827;
|
||
box-shadow: none;
|
||
border-radius: 0;
|
||
min-height: 28px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
cursor: pointer;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.suggest-item:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.suggest-item:hover {
|
||
background: #f3f4f6;
|
||
transform: none;
|
||
}
|
||
|
||
.suggest-item input[type="checkbox"] {
|
||
width: 12px !important;
|
||
height: 12px !important;
|
||
min-width: 12px !important;
|
||
min-height: 12px !important;
|
||
max-width: 12px !important;
|
||
max-height: 12px !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
border: 0 !important;
|
||
box-shadow: none !important;
|
||
line-height: 1 !important;
|
||
flex: 0 0 12px !important;
|
||
accent-color: #111827;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.suggest-item .text {
|
||
min-width: 0;
|
||
flex: 1 1 auto;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.suggest-empty {
|
||
padding: 10px 12px;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
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-recommend-panel-open > .voucher-topbar {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
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-recommend-panel-open > #pairRecommendPanel.active {
|
||
grid-column: 2;
|
||
grid-row: 2;
|
||
display: grid;
|
||
grid-template-rows: auto auto minmax(0, 1fr);
|
||
position: sticky;
|
||
top: 10px;
|
||
max-height: calc(100vh - 90px);
|
||
min-width: 0;
|
||
}
|
||
|
||
body[data-view-mode="dual"] #pairRecommendPanel.active .table-wrap {
|
||
max-height: calc(100vh - 230px);
|
||
}
|
||
|
||
body[data-view-mode="dual"] .detail-panel .table-wrap {
|
||
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,
|
||
.data-panel summary {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.status-card[data-status="voucher_matched"],
|
||
.status-card[data-status="erp_voucher_matched"] {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.cell-ellipsis {
|
||
max-width: 180px;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<section
|
||
class="voucher-page"
|
||
data-start-year="{{ wehago_compare.selected_start_year or '' }}"
|
||
data-end-year="{{ wehago_compare.selected_end_year or '' }}"
|
||
>
|
||
<datalist id="wehagoAccountOptions">
|
||
{% for item in wehago_compare.wehago_account_options %}
|
||
<option value="{{ item }}"></option>
|
||
{% endfor %}
|
||
</datalist>
|
||
<datalist id="erpAccountOptions">
|
||
{% for item in wehago_compare.erp_account_options %}
|
||
<option value="{{ item }}"></option>
|
||
{% endfor %}
|
||
</datalist>
|
||
<div class="voucher-topbar">
|
||
<div class="voucher-title-wrap">
|
||
<div class="section-title" style="margin-bottom: 0;">
|
||
<h2>{{ wehago_compare.page_title }}</h2>
|
||
</div>
|
||
</div>
|
||
<div class="voucher-toolbar">
|
||
<form method="get" action="/wehago-compare">
|
||
<label for="start_year">기간</label>
|
||
<select id="start_year" name="start_year">
|
||
{% for year in wehago_compare.available_years %}
|
||
<option
|
||
value="{{ year }}"
|
||
{% if wehago_compare.selected_start_year == year %}selected{% endif %}
|
||
{% if wehago_compare.selected_end_year and year > wehago_compare.selected_end_year %}disabled{% endif %}
|
||
>{{ year }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<span>~</span>
|
||
<select id="end_year" name="end_year">
|
||
{% for year in wehago_compare.available_years %}
|
||
<option
|
||
value="{{ year }}"
|
||
{% if wehago_compare.selected_end_year == year %}selected{% endif %}
|
||
{% if wehago_compare.selected_start_year and year < wehago_compare.selected_start_year %}disabled{% endif %}
|
||
>{{ year }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<button type="submit" class="button-secondary">적용</button>
|
||
</form>
|
||
<button type="button" class="button-secondary" id="undoLastActionBtn">직전 작업 취소</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) }}건
|
||
{% else %}
|
||
최근 작업: 없음
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<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>
|
||
<div class="panel-body">
|
||
<div class="status-grid">
|
||
{% for section in wehago_compare.metric_sections %}
|
||
<button
|
||
type="button"
|
||
class="status-card"
|
||
data-target="detail-{{ section.key }}"
|
||
data-status="{{ section.key }}"
|
||
aria-expanded="false"
|
||
>
|
||
<span class="status-label">{{ section.label }}</span>
|
||
<span class="status-value">{{ "{:,}".format(section.count) }}</span>
|
||
</button>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div class="detail-stack">
|
||
{% for section in wehago_compare.metric_sections %}
|
||
<section class="detail-panel" id="detail-{{ section.key }}" data-status-panel="{{ section.key }}">
|
||
<div class="detail-head">
|
||
<strong>{{ section.label }}</strong>
|
||
{% if section.key in ['matched', 'amount_mismatch'] %}
|
||
<form class="filter-form" data-filter-kind="status" data-status="{{ section.key }}">
|
||
<div class="suggest-field">
|
||
<input type="text" name="wehago_account" placeholder="WEHAGO 계정코드" autocomplete="off">
|
||
<div class="suggest-dropdown" data-wehago-account-suggest="{{ section.key }}"></div>
|
||
</div>
|
||
<div class="suggest-field">
|
||
<input type="text" name="erp_account" placeholder="Hanmac 계정코드" autocomplete="off">
|
||
<div class="suggest-dropdown" data-erp-account-suggest="{{ section.key }}"></div>
|
||
</div>
|
||
<div class="suggest-field">
|
||
<input type="text" name="voucher_no" placeholder="전표번호" autocomplete="off">
|
||
<div class="suggest-dropdown" data-voucher-suggest="{{ section.key }}"></div>
|
||
</div>
|
||
<div class="suggest-field">
|
||
<input type="text" name="draft_no" placeholder="가전표번호" autocomplete="off">
|
||
<div class="suggest-dropdown" data-draft-suggest="{{ section.key }}"></div>
|
||
</div>
|
||
<input type="text" name="wehago_amount" placeholder="WEHAGO 금액">
|
||
<input type="text" name="erp_amount" placeholder="Hanmac 금액">
|
||
<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 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>
|
||
<button type="submit">조회</button>
|
||
{% if section.key == 'amount_mismatch' %}
|
||
<button type="button" class="button-secondary" data-review-save>검토확인</button>
|
||
{% endif %}
|
||
</form>
|
||
{% else %}
|
||
<form class="filter-form" data-filter-kind="status" data-status="{{ section.key }}">
|
||
<div class="suggest-field">
|
||
<input type="text" name="voucher_no" placeholder="전표번호" autocomplete="off">
|
||
<div class="suggest-dropdown" data-voucher-suggest="{{ section.key }}"></div>
|
||
</div>
|
||
<div class="suggest-field">
|
||
<input type="text" name="account_keyword" placeholder="계정" autocomplete="off">
|
||
<div class="suggest-dropdown" data-account-suggest="{{ section.key }}"></div>
|
||
</div>
|
||
<div class="suggest-field">
|
||
<input type="text" name="vendor_keyword" placeholder="거래처" autocomplete="off">
|
||
<div class="suggest-dropdown" data-vendor-suggest="{{ section.key }}"></div>
|
||
</div>
|
||
<input type="text" name="amount_keyword" placeholder="금액">
|
||
<input type="text" name="desc_keyword" placeholder="적요">
|
||
<button type="submit">조회</button>
|
||
{% if section.key in ['ledger_only', 'voucher_only'] %}
|
||
<button type="button" class="button-secondary" data-pair-recommend>정확도 추천</button>
|
||
<button type="button" class="button-secondary" data-pair-save>개별 추천</button>
|
||
{% endif %}
|
||
</form>
|
||
{% endif %}
|
||
<div class="active-filter-cards" data-active-filters="{{ section.key }}"></div>
|
||
<div class="result-meta" data-result-meta="{{ section.key }}"></div>
|
||
</div>
|
||
<div class="table-wrap" data-table-wrap="{{ section.key }}">
|
||
<div class="table-placeholder">상태를 클릭하거나 조회를 실행하면 결과를 불러옵니다.</div>
|
||
</div>
|
||
<div class="table-footer" data-table-footer="{{ section.key }}" hidden>
|
||
<button type="button" class="button-secondary load-more-button" data-load-more="{{ section.key }}">더 보기</button>
|
||
</div>
|
||
</section>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel-shell recommend-panel" id="pairRecommendPanel">
|
||
<div class="panel-header">
|
||
<div>
|
||
<h2 id="recommendPanelTitle">정확도 추천 워크벤치</h2>
|
||
<div class="panel-meta" id="recommendModeMeta"></div>
|
||
</div>
|
||
<div class="recommend-actions">
|
||
<div class="recommend-sort" id="recommendSortTools">
|
||
<button type="button" class="button-secondary active" data-recommend-sort="score">점수순</button>
|
||
<button type="button" class="button-secondary" data-recommend-sort="amount_gap">금액차이순</button>
|
||
<button type="button" class="button-secondary" data-recommend-sort="account_similarity">계정유사도순</button>
|
||
<button type="button" class="button-secondary" data-recommend-sort="vendor_similarity">거래처유사도순</button>
|
||
</div>
|
||
<label id="recommendHighOnlyWrap" style="display:inline-flex; align-items:center; gap:6px; font-size:13px; color:var(--muted);">
|
||
<input type="checkbox" id="recommendHighOnly">
|
||
고신뢰만 보기
|
||
</label>
|
||
<button type="button" class="button-secondary" id="recommendApplySelected">선택 저장</button>
|
||
<button type="button" class="button-secondary" id="recommendApplyIndividual" hidden>선택 매칭 저장</button>
|
||
<button type="button" class="button-secondary" id="recommendClose">닫기</button>
|
||
</div>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="recommend-summary" id="recommendSummary"></div>
|
||
</div>
|
||
<div class="table-wrap" id="recommendTableWrap">
|
||
<div class="table-placeholder">정확도 추천 버튼을 누르면 결과가 표시됩니다.</div>
|
||
</div>
|
||
</section>
|
||
|
||
</section>
|
||
|
||
<script>
|
||
(() => {
|
||
const page = document.querySelector('.voucher-page');
|
||
const startYearValue = page?.dataset.startYear || '';
|
||
const endYearValue = page?.dataset.endYear || '';
|
||
|
||
const escapeHtml = (value) => String(value ?? '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
|
||
const escapeSelectorValue = (value) => String(value ?? '').replace(/["\\]/g, '\\$&');
|
||
|
||
const detailState = new Map();
|
||
const recheckSelections = new Map();
|
||
const recheckRowStore = new Map();
|
||
const pairSelections = new Map();
|
||
const pairLedgerStore = new Map();
|
||
const pairVoucherStore = new Map();
|
||
const cumulativeFiltersByStatus = new Map();
|
||
let latestPairRecommendation = null;
|
||
|
||
const getCellClass = (field, monoFields = []) => {
|
||
const classes = ['cell-ellipsis'];
|
||
if (monoFields.includes(field)) {
|
||
classes.push('mono');
|
||
}
|
||
if (['fiscal_year'].includes(field)) {
|
||
classes.push('cell-tight');
|
||
} else if (['voucher_no', 'draft_no', 'ledger_date', 'proof_date', 'confirmed_no', 'account_code'].includes(field)) {
|
||
classes.push('cell-code');
|
||
} else if (
|
||
[
|
||
'ledger_account_name',
|
||
'voucher_account_name',
|
||
'ledger_vendor',
|
||
'voucher_vendor',
|
||
'ledger_debit',
|
||
'ledger_credit',
|
||
'voucher_debit',
|
||
'voucher_credit',
|
||
].includes(field)
|
||
) {
|
||
classes.push('cell-equal');
|
||
} else if (['ledger_desc', 'voucher_desc', 'desc1', 'desc2', 'description', 'review_reason', 'review_memo', 'notes'].includes(field)) {
|
||
classes.push('cell-desc');
|
||
}
|
||
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) {
|
||
container.innerHTML = '<div class="table-placeholder">조건에 맞는 항목이 없습니다.</div>';
|
||
return;
|
||
}
|
||
const includeReviewCheckbox = statusKey === 'amount_mismatch';
|
||
const includePairCheckbox = statusKey === 'ledger_only' || statusKey === 'voucher_only';
|
||
const head = [
|
||
(includeReviewCheckbox || includePairCheckbox) ? '<th></th>' : '',
|
||
...columns.map(([field, label]) => `<th class="${getColumnClass(field)}">${escapeHtml(label)}</th>`),
|
||
].join('');
|
||
const body = rows.map((row) => {
|
||
const reviewKey = row.review_key || '';
|
||
if (includeReviewCheckbox && reviewKey) {
|
||
recheckRowStore.set(reviewKey, row);
|
||
}
|
||
const pairKey = statusKey === 'ledger_only'
|
||
? (row.ledger_row_key || '')
|
||
: statusKey === 'voucher_only'
|
||
? (row.voucher_row_key || '')
|
||
: '';
|
||
if (includePairCheckbox && pairKey) {
|
||
if (statusKey === 'ledger_only') {
|
||
pairLedgerStore.set(pairKey, row);
|
||
} else {
|
||
pairVoucherStore.set(pairKey, row);
|
||
}
|
||
}
|
||
const cells = columns.map(([field]) => {
|
||
const raw = row[field] ?? '';
|
||
let display = '';
|
||
if (field === 'fiscal_year') {
|
||
display = String(raw ?? '');
|
||
} else if (typeof raw === 'number') {
|
||
display = Number.isInteger(raw)
|
||
? raw.toLocaleString()
|
||
: raw.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
||
} else {
|
||
display = String(raw);
|
||
}
|
||
const className = getCellClass(field, monoFields);
|
||
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('');
|
||
if (!append || !container.querySelector('table')) {
|
||
container.innerHTML = `<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
|
||
} else {
|
||
const tbody = container.querySelector('tbody');
|
||
if (tbody && body) {
|
||
tbody.insertAdjacentHTML('beforeend', body);
|
||
}
|
||
}
|
||
if (includeReviewCheckbox) {
|
||
container.querySelectorAll('input[data-review-key]').forEach((checkbox) => {
|
||
checkbox.addEventListener('change', () => {
|
||
const key = checkbox.dataset.reviewKey || '';
|
||
if (!key) return;
|
||
recheckSelections.set(key, checkbox.checked);
|
||
});
|
||
});
|
||
}
|
||
if (includePairCheckbox) {
|
||
container.querySelectorAll('input[data-pair-key]').forEach((checkbox) => {
|
||
checkbox.addEventListener('change', () => {
|
||
const key = checkbox.dataset.pairKey || '';
|
||
if (!key) return;
|
||
pairSelections.set(key, checkbox.checked);
|
||
});
|
||
});
|
||
}
|
||
};
|
||
|
||
const setMeta = (key, totalCount, shownCount, notice = '', stats = {}) => {
|
||
const target = document.querySelector(`[data-result-meta="${key}"]`);
|
||
if (target) {
|
||
const countText = `조회 결과 ${Number(totalCount || 0).toLocaleString()}건`;
|
||
const shownText = shownCount
|
||
? ` / 현재 ${Number(shownCount || 0).toLocaleString()}건`
|
||
: '';
|
||
const noticeText = notice ? ` / ${notice}` : '';
|
||
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}`;
|
||
}
|
||
};
|
||
|
||
const fetchJson = async (url) => {
|
||
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||
const payload = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(payload.error || '조회 중 오류가 발생했습니다.');
|
||
}
|
||
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);
|
||
if (endYearValue) params.set('end_year', endYearValue);
|
||
if (form) {
|
||
const data = new FormData(form);
|
||
for (const [key, value] of data.entries()) {
|
||
if (String(value).trim()) {
|
||
params.set(key, String(value).trim());
|
||
}
|
||
}
|
||
}
|
||
Object.entries(extra).forEach(([key, value]) => {
|
||
if (value) params.set(key, value);
|
||
});
|
||
return params.toString();
|
||
};
|
||
|
||
const getStatusForm = (statusKey) => document.querySelector(`form[data-filter-kind="status"][data-status="${statusKey}"]`);
|
||
const suggestState = new Map();
|
||
const recommendPanel = document.getElementById('pairRecommendPanel');
|
||
const recommendPanelTitle = document.getElementById('recommendPanelTitle');
|
||
const recommendModeMeta = document.getElementById('recommendModeMeta');
|
||
const recommendSummary = document.getElementById('recommendSummary');
|
||
const recommendTableWrap = document.getElementById('recommendTableWrap');
|
||
const recommendHighOnly = document.getElementById('recommendHighOnly');
|
||
const recommendHighOnlyWrap = document.getElementById('recommendHighOnlyWrap');
|
||
const recommendApplySelected = document.getElementById('recommendApplySelected');
|
||
const recommendApplyIndividual = document.getElementById('recommendApplyIndividual');
|
||
const recommendClose = document.getElementById('recommendClose');
|
||
const recommendSortTools = document.getElementById('recommendSortTools');
|
||
const recommendSortButtons = Array.from(document.querySelectorAll('[data-recommend-sort]'));
|
||
const undoLastActionBtn = document.getElementById('undoLastActionBtn');
|
||
const lastActionMeta = document.getElementById('lastActionMeta');
|
||
const recommendSelections = new Map();
|
||
let recommendSortKey = 'score';
|
||
let recommendMode = 'batch';
|
||
const individualState = {
|
||
sourceStatus: '',
|
||
sourceRow: null,
|
||
sourceRowKey: '',
|
||
rows: [],
|
||
totalCount: 0,
|
||
nextOffset: 0,
|
||
hasMore: false,
|
||
loading: false,
|
||
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 {
|
||
const payload = await fetchJson('/wehago-compare/api/last-action');
|
||
const hasAction = Boolean(payload && payload.id);
|
||
undoLastActionBtn.disabled = !hasAction;
|
||
if (!hasAction) {
|
||
lastActionMeta.textContent = '최근 작업: 없음';
|
||
return;
|
||
}
|
||
const actionTypeMap = {
|
||
recheck_save: 'recheck 저장',
|
||
pair_save: '쌍매칭 저장',
|
||
};
|
||
const actionLabel = actionTypeMap[payload.action_type] || payload.action_type || '작업';
|
||
lastActionMeta.textContent = `최근 작업: ${actionLabel} / ${formatNumber(payload.count || 0)}건`;
|
||
} catch (_error) {
|
||
undoLastActionBtn.disabled = true;
|
||
lastActionMeta.textContent = '최근 작업: 조회 실패';
|
||
}
|
||
};
|
||
|
||
const buildPairQuery = (extra = {}) => {
|
||
const ledgerForm = getStatusForm('ledger_only');
|
||
const voucherForm = getStatusForm('voucher_only');
|
||
const params = new URLSearchParams();
|
||
if (startYearValue) params.set('start_year', startYearValue);
|
||
if (endYearValue) params.set('end_year', endYearValue);
|
||
if (ledgerForm) {
|
||
const data = new FormData(ledgerForm);
|
||
params.set('ledger_voucher_no', String(data.get('voucher_no') || '').trim());
|
||
params.set('ledger_review_reason', String(data.get('desc_keyword') || '').trim());
|
||
}
|
||
if (voucherForm) {
|
||
const data = new FormData(voucherForm);
|
||
params.set('voucher_voucher_no', String(data.get('voucher_no') || '').trim());
|
||
params.set('voucher_review_reason', String(data.get('desc_keyword') || '').trim());
|
||
}
|
||
Object.entries(extra).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||
params.set(key, String(value).trim());
|
||
}
|
||
});
|
||
return params;
|
||
};
|
||
|
||
const initStatusSuggest = (form, type) => {
|
||
if (!form) return;
|
||
const status = form.dataset.status || '';
|
||
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'
|
||
? 'wehago_account'
|
||
: type === 'erp_account'
|
||
? 'erp_account'
|
||
: type === 'draft'
|
||
? 'draft_no'
|
||
: type === 'vendor'
|
||
? 'vendor_keyword'
|
||
: type === 'wehago_vendor'
|
||
? 'wehago_vendor'
|
||
: type === 'erp_vendor'
|
||
? 'erp_vendor'
|
||
: 'voucher_no';
|
||
const fieldName = type === 'account'
|
||
? 'account'
|
||
: type === 'wehago_account'
|
||
? 'wehago_account'
|
||
: type === 'erp_account'
|
||
? 'erp_account'
|
||
: type === 'draft'
|
||
? 'draft_no'
|
||
: type === 'vendor'
|
||
? 'vendor'
|
||
: type === 'wehago_vendor'
|
||
? 'wehago_vendor'
|
||
: type === 'erp_vendor'
|
||
? 'erp_vendor'
|
||
: 'voucher_no';
|
||
const dropdownSelector = type === 'account'
|
||
? `[data-account-suggest="${status}"]`
|
||
: type === 'wehago_account'
|
||
? `[data-wehago-account-suggest="${status}"]`
|
||
: type === 'erp_account'
|
||
? `[data-erp-account-suggest="${status}"]`
|
||
: type === 'draft'
|
||
? `[data-draft-suggest="${status}"]`
|
||
: type === 'vendor'
|
||
? `[data-vendor-suggest="${status}"]`
|
||
: type === 'wehago_vendor'
|
||
? `[data-wehago-vendor-suggest="${status}"]`
|
||
: type === 'erp_vendor'
|
||
? `[data-erp-vendor-suggest="${status}"]`
|
||
: `[data-voucher-suggest="${status}"]`;
|
||
const input = form.querySelector(`input[name="${inputName}"]`);
|
||
const dropdown = form.querySelector(dropdownSelector);
|
||
if (!input || !dropdown) return;
|
||
const stateKey = `${status}:${type}`;
|
||
suggestState.set(stateKey, { offset: 0, hasMore: false, loading: false, keyword: '' });
|
||
|
||
const renderSuggest = (rows, append = false) => {
|
||
if (!append) {
|
||
dropdown.innerHTML = '';
|
||
}
|
||
if (!rows.length && !append) {
|
||
dropdown.innerHTML = '<div class="suggest-empty">검색 결과가 없습니다.</div>';
|
||
dropdown.classList.add('open');
|
||
return;
|
||
}
|
||
const active = cumulativeFiltersByStatus.get(status) || {};
|
||
const html = rows.map((row) => {
|
||
const value = type === 'voucher' ? (row.voucher_no || '') : (row.label || '');
|
||
const checked = String(active[inputName] || '') === String(value || '');
|
||
return `
|
||
<label class="suggest-item" title="${escapeHtml(row.label || '')}">
|
||
<input type="checkbox" data-suggest-check="${escapeHtml(inputName)}" data-suggest-value="${escapeHtml(value)}" ${checked ? 'checked' : ''}>
|
||
<span class="text">${escapeHtml(row.label || '')}</span>
|
||
</label>
|
||
`;
|
||
}).join('');
|
||
if (!append) {
|
||
dropdown.innerHTML = html;
|
||
} else {
|
||
dropdown.insertAdjacentHTML('beforeend', html);
|
||
}
|
||
dropdown.classList.add('open');
|
||
dropdown.querySelectorAll('input[type="checkbox"][data-suggest-check]').forEach((checkbox) => {
|
||
if (checkbox.dataset.bound === 'y') return;
|
||
checkbox.dataset.bound = 'y';
|
||
checkbox.addEventListener('change', () => {
|
||
const value = checkbox.dataset.suggestValue || '';
|
||
if (checkbox.checked) {
|
||
dropdown.querySelectorAll(`input[type="checkbox"][data-suggest-check="${escapeSelectorValue(inputName)}"]`).forEach((other) => {
|
||
if (other !== checkbox) other.checked = false;
|
||
});
|
||
input.value = value;
|
||
updateCumulativeFilter(status, inputName, value, true);
|
||
} else {
|
||
if (String(input.value || '').trim() === value) {
|
||
input.value = '';
|
||
}
|
||
updateCumulativeFilter(status, inputName, '', false);
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
const loadSuggest = async (append = false) => {
|
||
const state = suggestState.get(stateKey) || { offset: 0, hasMore: false, loading: false, keyword: '' };
|
||
if (state.loading) return;
|
||
const keyword = String(input.value || '').trim();
|
||
if (!keyword) {
|
||
dropdown.classList.remove('open');
|
||
dropdown.innerHTML = '';
|
||
suggestState.set(stateKey, { ...state, offset: 0, hasMore: false, keyword: '' });
|
||
return;
|
||
}
|
||
const nextOffset = append ? (state.offset || 0) : 0;
|
||
suggestState.set(stateKey, { ...state, loading: true, keyword });
|
||
try {
|
||
const params = new URLSearchParams();
|
||
if (startYearValue) params.set('start_year', startYearValue);
|
||
if (endYearValue) params.set('end_year', endYearValue);
|
||
params.set('status', status);
|
||
params.set('field', fieldName);
|
||
params.set('keyword', keyword);
|
||
params.set('offset', String(nextOffset));
|
||
params.set('limit', '10');
|
||
let payload = await fetchJson(`/wehago-compare/api/status-suggestions?${params.toString()}`);
|
||
let rows = payload.rows || [];
|
||
if (!append && rows.length === 0) {
|
||
const fallbackParams = new URLSearchParams();
|
||
if (startYearValue) fallbackParams.set('start_year', startYearValue);
|
||
if (endYearValue) fallbackParams.set('end_year', endYearValue);
|
||
fallbackParams.set('status', status);
|
||
fallbackParams.set('field', fieldName);
|
||
fallbackParams.set('keyword', '');
|
||
fallbackParams.set('offset', '0');
|
||
fallbackParams.set('limit', '10');
|
||
const fallback = await fetchJson(`/wehago-compare/api/status-suggestions?${fallbackParams.toString()}`);
|
||
rows = fallback.rows || [];
|
||
payload = rows.length ? fallback : payload;
|
||
}
|
||
renderSuggest(rows, append);
|
||
suggestState.set(stateKey, {
|
||
...state,
|
||
loading: false,
|
||
offset: Number(payload.next_offset || 0),
|
||
hasMore: Boolean(payload.has_more),
|
||
keyword,
|
||
});
|
||
} catch (_error) {
|
||
suggestState.set(stateKey, { ...state, loading: false });
|
||
}
|
||
};
|
||
|
||
let debounceTimer = null;
|
||
input.addEventListener('input', () => {
|
||
if (debounceTimer) {
|
||
window.clearTimeout(debounceTimer);
|
||
}
|
||
debounceTimer = window.setTimeout(() => {
|
||
loadSuggest(false);
|
||
}, 180);
|
||
});
|
||
input.addEventListener('focus', () => {
|
||
if (String(input.value || '').trim()) {
|
||
loadSuggest(false);
|
||
}
|
||
});
|
||
dropdown.addEventListener('scroll', () => {
|
||
const state = suggestState.get(stateKey);
|
||
if (!state || !state.hasMore || state.loading) return;
|
||
const remain = dropdown.scrollHeight - dropdown.scrollTop - dropdown.clientHeight;
|
||
if (remain <= 40) {
|
||
loadSuggest(true);
|
||
}
|
||
});
|
||
document.addEventListener('mousedown', (event) => {
|
||
if (!form.contains(event.target)) {
|
||
dropdown.classList.remove('open');
|
||
}
|
||
});
|
||
};
|
||
|
||
const formatNumber = (value) => {
|
||
const numberValue = Number(value || 0);
|
||
return Number.isInteger(numberValue)
|
||
? numberValue.toLocaleString()
|
||
: numberValue.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
||
};
|
||
|
||
const prettyFilterLabels = {
|
||
voucher_no: '전표번호',
|
||
draft_no: '가전표번호',
|
||
wehago_account: 'WEHAGO 계정',
|
||
erp_account: 'Hanmac 계정',
|
||
wehago_amount: 'WEHAGO 금액',
|
||
erp_amount: 'Hanmac 금액',
|
||
wehago_vendor: 'WEHAGO 거래처',
|
||
erp_vendor: 'Hanmac 거래처',
|
||
account_keyword: '계정',
|
||
vendor_keyword: '거래처',
|
||
amount_keyword: '금액',
|
||
desc_keyword: '적요',
|
||
review_reason: '검증근거',
|
||
boundary_excluded: '연초/연말 대체·이월',
|
||
};
|
||
|
||
const getFormFieldValues = (form) => {
|
||
const values = {};
|
||
form.querySelectorAll('input[name]').forEach((input) => {
|
||
const name = input.name;
|
||
if (!name) return;
|
||
const value = String(input.value || '').trim();
|
||
if (!value) return;
|
||
values[name] = value;
|
||
});
|
||
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()) {
|
||
current[fieldName] = String(value).trim();
|
||
} else {
|
||
delete current[fieldName];
|
||
}
|
||
cumulativeFiltersByStatus.set(statusKey, current);
|
||
renderActiveFilterCards(statusKey);
|
||
};
|
||
|
||
const renderActiveFilterCards = (statusKey) => {
|
||
const box = document.querySelector(`[data-active-filters="${statusKey}"]`);
|
||
if (!box) return;
|
||
const active = cumulativeFiltersByStatus.get(statusKey) || {};
|
||
const entries = Object.entries(active).filter(([, value]) => String(value || '').trim() !== '');
|
||
if (!entries.length) {
|
||
box.innerHTML = '';
|
||
return;
|
||
}
|
||
box.innerHTML = entries.map(([name, value]) => `
|
||
<div class="active-filter-card">
|
||
<div class="value" title="${escapeHtml(`${prettyFilterLabels[name] || name}: ${value}`)}">${escapeHtml(`${prettyFilterLabels[name] || name}: ${value}`)}</div>
|
||
<button type="button" class="remove" data-remove-active-filter="${escapeHtml(statusKey)}" data-filter-name="${escapeHtml(name)}" aria-label="조건 제거">×</button>
|
||
</div>
|
||
`).join('');
|
||
};
|
||
|
||
const getFilteredRecommendations = () => {
|
||
if (!latestPairRecommendation || !Array.isArray(latestPairRecommendation.pairs)) {
|
||
return [];
|
||
}
|
||
const onlyHigh = Boolean(recommendHighOnly?.checked);
|
||
const filtered = latestPairRecommendation.pairs.filter((pair) => !onlyHigh || pair.confidence_level === 'high');
|
||
filtered.sort((a, b) => {
|
||
if (recommendSortKey === 'amount_gap') {
|
||
const left = Number(a.amount_gap || 0);
|
||
const right = Number(b.amount_gap || 0);
|
||
return left - right;
|
||
}
|
||
if (recommendSortKey === 'account_similarity') {
|
||
const left = Number(a.account_similarity || 0);
|
||
const right = Number(b.account_similarity || 0);
|
||
return right - left;
|
||
}
|
||
if (recommendSortKey === 'vendor_similarity') {
|
||
const left = Number(a.vendor_similarity || 0);
|
||
const right = Number(b.vendor_similarity || 0);
|
||
return right - left;
|
||
}
|
||
return Number(b.score || 0) - Number(a.score || 0);
|
||
});
|
||
return filtered;
|
||
};
|
||
|
||
const renderRecommendationSummary = (stats = {}) => {
|
||
if (!recommendSummary) {
|
||
return;
|
||
}
|
||
recommendSummary.innerHTML = `
|
||
<span class="recommend-badge">추천 ${formatNumber(stats.recommended || 0)}건</span>
|
||
<span class="recommend-badge good">고신뢰 ${formatNumber(stats.high_confidence || 0)}건</span>
|
||
<span class="recommend-badge">WEHAGO 풀 ${formatNumber(stats.ledger_rows || 0)}건</span>
|
||
<span class="recommend-badge">ERP 풀 ${formatNumber(stats.voucher_rows || 0)}건</span>
|
||
`;
|
||
};
|
||
|
||
const renderRecommendationTable = () => {
|
||
if (!recommendTableWrap) {
|
||
return;
|
||
}
|
||
const pairs = getFilteredRecommendations();
|
||
if (!pairs.length) {
|
||
recommendTableWrap.innerHTML = '<div class="table-placeholder">표시할 추천 결과가 없습니다.</div>';
|
||
return;
|
||
}
|
||
const head = `
|
||
<tr>
|
||
<th><input type="checkbox" id="recommendSelectAll"></th>
|
||
<th>점수</th>
|
||
<th>금액차이</th>
|
||
<th>계정유사도</th>
|
||
<th>거래처유사도</th>
|
||
<th>신뢰도</th>
|
||
<th>검토</th>
|
||
<th>연도</th>
|
||
<th>전표번호</th>
|
||
<th>일자</th>
|
||
<th>가전표번호</th>
|
||
<th>WEHAGO 계정</th>
|
||
<th>ERP 계정</th>
|
||
<th>WEHAGO 거래처</th>
|
||
<th>ERP 거래처</th>
|
||
<th>WEHAGO 금액</th>
|
||
<th>ERP 금액</th>
|
||
<th>WEHAGO 적요</th>
|
||
<th>ERP 적요</th>
|
||
</tr>
|
||
`;
|
||
const body = pairs.map((pair) => {
|
||
const key = pair.pair_key || '';
|
||
const ledger = pair.ledger_row || {};
|
||
const voucher = pair.voucher_row || {};
|
||
const ledgerAmount = Math.max(Number(ledger.ledger_debit || 0), Number(ledger.ledger_credit || 0));
|
||
const voucherAmount = Math.max(Number(voucher.voucher_debit || 0), Number(voucher.voucher_credit || 0));
|
||
if (!recommendSelections.has(key) && pair.auto_eligible) {
|
||
recommendSelections.set(key, true);
|
||
}
|
||
const checked = recommendSelections.get(key) ? 'checked' : '';
|
||
return `
|
||
<tr>
|
||
<td><input type="checkbox" data-recommend-key="${escapeHtml(key)}" ${checked}></td>
|
||
<td title="${escapeHtml(pair.score)}"><span class="cell-ellipsis cell-tight">${escapeHtml(pair.score)}</span></td>
|
||
<td title="${escapeHtml(formatNumber(pair.amount_gap || 0))}"><span class="cell-ellipsis cell-tight">${escapeHtml(formatNumber(pair.amount_gap || 0))}</span></td>
|
||
<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>
|
||
<td title="${escapeHtml(voucher.draft_no || '')}"><span class="cell-ellipsis cell-code">${escapeHtml(voucher.draft_no || '')}</span></td>
|
||
<td title="${escapeHtml(ledger.ledger_account_name || '')}"><span class="cell-ellipsis cell-equal">${escapeHtml(ledger.ledger_account_name || '')}</span></td>
|
||
<td title="${escapeHtml(voucher.voucher_account_name || '')}"><span class="cell-ellipsis cell-equal">${escapeHtml(voucher.voucher_account_name || '')}</span></td>
|
||
<td title="${escapeHtml(ledger.ledger_vendor || '')}"><span class="cell-ellipsis cell-equal">${escapeHtml(ledger.ledger_vendor || '')}</span></td>
|
||
<td title="${escapeHtml(voucher.voucher_vendor || '')}"><span class="cell-ellipsis cell-equal">${escapeHtml(voucher.voucher_vendor || '')}</span></td>
|
||
<td title="${escapeHtml(formatNumber(ledgerAmount))}"><span class="cell-ellipsis cell-equal">${escapeHtml(formatNumber(ledgerAmount))}</span></td>
|
||
<td title="${escapeHtml(formatNumber(voucherAmount))}"><span class="cell-ellipsis cell-equal">${escapeHtml(formatNumber(voucherAmount))}</span></td>
|
||
<td title="${escapeHtml(ledger.ledger_desc || '')}"><span class="cell-ellipsis cell-desc">${escapeHtml(ledger.ledger_desc || '')}</span></td>
|
||
<td title="${escapeHtml(voucher.voucher_desc || '')}"><span class="cell-ellipsis cell-desc">${escapeHtml(voucher.voucher_desc || '')}</span></td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
recommendTableWrap.innerHTML = `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
||
|
||
const selectAll = document.getElementById('recommendSelectAll');
|
||
const rowChecks = Array.from(recommendTableWrap.querySelectorAll('input[data-recommend-key]'));
|
||
const syncHeader = () => {
|
||
if (!selectAll) return;
|
||
const checkedCount = rowChecks.filter((el) => el.checked).length;
|
||
selectAll.checked = rowChecks.length > 0 && checkedCount === rowChecks.length;
|
||
selectAll.indeterminate = checkedCount > 0 && checkedCount < rowChecks.length;
|
||
};
|
||
rowChecks.forEach((checkbox) => {
|
||
checkbox.addEventListener('change', () => {
|
||
const key = checkbox.dataset.recommendKey || '';
|
||
if (!key) return;
|
||
recommendSelections.set(key, checkbox.checked);
|
||
syncHeader();
|
||
});
|
||
});
|
||
if (selectAll) {
|
||
selectAll.addEventListener('change', () => {
|
||
rowChecks.forEach((checkbox) => {
|
||
checkbox.checked = selectAll.checked;
|
||
const key = checkbox.dataset.recommendKey || '';
|
||
if (key) {
|
||
recommendSelections.set(key, selectAll.checked);
|
||
}
|
||
});
|
||
syncHeader();
|
||
});
|
||
}
|
||
syncHeader();
|
||
};
|
||
|
||
const setRecommendMode = (mode) => {
|
||
recommendMode = mode === 'individual' ? 'individual' : 'batch';
|
||
if (recommendPanelTitle) {
|
||
recommendPanelTitle.textContent = recommendMode === 'individual' ? '개별 추천' : '정확도 추천 워크벤치';
|
||
}
|
||
if (recommendModeMeta && recommendMode === 'batch') {
|
||
recommendModeMeta.textContent = '';
|
||
}
|
||
if (recommendSortTools) {
|
||
recommendSortTools.hidden = recommendMode === 'individual';
|
||
}
|
||
if (recommendHighOnlyWrap) {
|
||
recommendHighOnlyWrap.hidden = recommendMode === 'individual';
|
||
}
|
||
if (recommendApplySelected) {
|
||
recommendApplySelected.hidden = recommendMode === 'individual';
|
||
}
|
||
if (recommendApplyIndividual) {
|
||
recommendApplyIndividual.hidden = recommendMode !== 'individual';
|
||
}
|
||
if (recommendSummary) {
|
||
recommendSummary.style.display = recommendMode === 'individual' ? 'none' : 'flex';
|
||
}
|
||
};
|
||
|
||
const openRecommendationPanel = (mode = 'batch') => {
|
||
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) => {
|
||
if (sourceStatus === 'ledger_only') {
|
||
return Array.from(pairSelections.entries())
|
||
.filter(([key, checked]) => checked && pairLedgerStore.has(key))
|
||
.map(([key]) => pairLedgerStore.get(key))
|
||
.filter(Boolean);
|
||
}
|
||
return Array.from(pairSelections.entries())
|
||
.filter(([key, checked]) => checked && pairVoucherStore.has(key))
|
||
.map(([key]) => pairVoucherStore.get(key))
|
||
.filter(Boolean);
|
||
};
|
||
|
||
const renderIndividualRows = (append = false) => {
|
||
if (!recommendTableWrap) return;
|
||
const rows = individualState.rows || [];
|
||
if (!rows.length && !append) {
|
||
recommendTableWrap.innerHTML = '<div class="table-placeholder">추천 항목이 없습니다.</div>';
|
||
return;
|
||
}
|
||
const head = `
|
||
<tr>
|
||
<th></th>
|
||
<th>점수</th>
|
||
<th>금액차이</th>
|
||
<th>계정유사도</th>
|
||
<th>거래처유사도</th>
|
||
<th>연도</th>
|
||
<th>전표번호</th>
|
||
<th>일자</th>
|
||
<th>가전표번호</th>
|
||
<th>WEHAGO 계정</th>
|
||
<th>ERP 계정</th>
|
||
<th>WEHAGO 거래처</th>
|
||
<th>ERP 거래처</th>
|
||
<th>WEHAGO 적요</th>
|
||
<th>ERP 적요</th>
|
||
</tr>
|
||
`;
|
||
const body = rows.map((item) => {
|
||
const target = item.target_row || {};
|
||
const isLedgerSource = individualState.sourceStatus === 'ledger_only';
|
||
const ledger = isLedgerSource ? (individualState.sourceRow || {}) : target;
|
||
const voucher = isLedgerSource ? target : (individualState.sourceRow || {});
|
||
const checked = individualState.selectedPairKey === item.pair_key ? 'checked' : '';
|
||
return `
|
||
<tr>
|
||
<td><input type="radio" name="individualPairRadio" value="${escapeHtml(item.pair_key || '')}" ${checked}></td>
|
||
<td title="${escapeHtml(item.score)}"><span class="cell-ellipsis cell-tight">${escapeHtml(item.score)}</span></td>
|
||
<td title="${escapeHtml(formatNumber(item.amount_gap || 0))}"><span class="cell-ellipsis cell-tight">${escapeHtml(formatNumber(item.amount_gap || 0))}</span></td>
|
||
<td title="${escapeHtml((Number(item.account_similarity || 0) * 100).toFixed(1) + '%')}"><span class="cell-ellipsis cell-tight">${escapeHtml((Number(item.account_similarity || 0) * 100).toFixed(1) + '%')}</span></td>
|
||
<td title="${escapeHtml((Number(item.vendor_similarity || 0) * 100).toFixed(1) + '%')}"><span class="cell-ellipsis cell-tight">${escapeHtml((Number(item.vendor_similarity || 0) * 100).toFixed(1) + '%')}</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>
|
||
<td title="${escapeHtml(voucher.draft_no || '')}"><span class="cell-ellipsis cell-code">${escapeHtml(voucher.draft_no || '')}</span></td>
|
||
<td title="${escapeHtml(ledger.ledger_account_name || '')}"><span class="cell-ellipsis cell-equal">${escapeHtml(ledger.ledger_account_name || '')}</span></td>
|
||
<td title="${escapeHtml(voucher.voucher_account_name || '')}"><span class="cell-ellipsis cell-equal">${escapeHtml(voucher.voucher_account_name || '')}</span></td>
|
||
<td title="${escapeHtml(ledger.ledger_vendor || '')}"><span class="cell-ellipsis cell-equal">${escapeHtml(ledger.ledger_vendor || '')}</span></td>
|
||
<td title="${escapeHtml(voucher.voucher_vendor || '')}"><span class="cell-ellipsis cell-equal">${escapeHtml(voucher.voucher_vendor || '')}</span></td>
|
||
<td title="${escapeHtml(ledger.ledger_desc || '')}"><span class="cell-ellipsis cell-desc">${escapeHtml(ledger.ledger_desc || '')}</span></td>
|
||
<td title="${escapeHtml(voucher.voucher_desc || '')}"><span class="cell-ellipsis cell-desc">${escapeHtml(voucher.voucher_desc || '')}</span></td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
recommendTableWrap.innerHTML = `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
||
recommendTableWrap.querySelectorAll('input[name="individualPairRadio"]').forEach((radio) => {
|
||
radio.addEventListener('change', () => {
|
||
individualState.selectedPairKey = radio.value || '';
|
||
});
|
||
});
|
||
};
|
||
|
||
const loadIndividualRecommendations = async (append = false) => {
|
||
if (!individualState.sourceStatus || !individualState.sourceRowKey || individualState.loading) return;
|
||
individualState.loading = true;
|
||
if (!append && recommendTableWrap) {
|
||
recommendTableWrap.innerHTML = '<div class="table-placeholder">개별 추천 불러오는 중...</div>';
|
||
}
|
||
try {
|
||
const params = new URLSearchParams();
|
||
if (startYearValue) params.set('start_year', startYearValue);
|
||
if (endYearValue) params.set('end_year', endYearValue);
|
||
params.set('source_status', individualState.sourceStatus);
|
||
params.set('source_row_key', individualState.sourceRowKey);
|
||
params.set('offset', append ? String(individualState.nextOffset || 0) : '0');
|
||
params.set('limit', '10');
|
||
const payload = await fetchJson(`/wehago-compare/api/pair-individual-recommendations?${params.toString()}`);
|
||
if (!append) {
|
||
individualState.rows = payload.rows || [];
|
||
individualState.sourceRow = payload.source_row || null;
|
||
individualState.totalCount = Number(payload.total_count || 0);
|
||
} else {
|
||
individualState.rows = [...(individualState.rows || []), ...(payload.rows || [])];
|
||
}
|
||
individualState.nextOffset = Number(payload.next_offset || 0);
|
||
individualState.hasMore = Boolean(payload.has_more);
|
||
if (recommendModeMeta) {
|
||
const source = individualState.sourceRow || {};
|
||
const label = individualState.sourceStatus === 'ledger_only' ? 'Unmatched 기준' : 'ERP Unmatched 기준';
|
||
recommendModeMeta.textContent = `${label} / 전표번호 ${source.voucher_no || '-'} / 계정 ${source.ledger_account_name || source.voucher_account_name || '-'}`;
|
||
}
|
||
if (!individualState.selectedPairKey && individualState.rows.length) {
|
||
individualState.selectedPairKey = individualState.rows[0].pair_key || '';
|
||
}
|
||
renderIndividualRows(append);
|
||
} catch (error) {
|
||
if (recommendTableWrap) {
|
||
recommendTableWrap.innerHTML = `<div class="table-placeholder">${escapeHtml(error.message)}</div>`;
|
||
}
|
||
} finally {
|
||
individualState.loading = false;
|
||
}
|
||
};
|
||
|
||
const setLoadMoreState = (statusKey, hasMore, loading = false) => {
|
||
const footer = document.querySelector(`[data-table-footer="${statusKey}"]`);
|
||
const button = document.querySelector(`[data-load-more="${statusKey}"]`);
|
||
if (!footer || !button) return;
|
||
footer.hidden = !hasMore && !loading;
|
||
button.disabled = loading;
|
||
button.textContent = loading ? '불러오는 중...' : '더 보기';
|
||
};
|
||
|
||
const buildStatusQuery = (form, statusKey, extra = {}) => {
|
||
const selected = cumulativeFiltersByStatus.get(statusKey) || {};
|
||
const mapped = new URLSearchParams();
|
||
if (startYearValue) mapped.set('start_year', startYearValue);
|
||
if (endYearValue) mapped.set('end_year', endYearValue);
|
||
mapped.set('status', statusKey);
|
||
const rawVoucher = String(selected.voucher_no || '').trim();
|
||
const rawDesc = String(selected.desc_keyword || '').trim();
|
||
if (rawVoucher) mapped.set('voucher_no', rawVoucher);
|
||
if (rawDesc) mapped.set('desc_keyword', rawDesc);
|
||
if (statusKey === 'ledger_only') {
|
||
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();
|
||
const amount = String(selected.amount_keyword || '').trim();
|
||
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()) {
|
||
mapped.set(key, String(value).trim());
|
||
}
|
||
}
|
||
}
|
||
Object.entries(extra).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && String(value) !== '') {
|
||
mapped.set(key, String(value));
|
||
}
|
||
});
|
||
return mapped.toString();
|
||
};
|
||
|
||
const loadStatusRows = async (statusKey, form = null, append = false) => {
|
||
const wrap = document.querySelector(`[data-table-wrap="${statusKey}"]`);
|
||
if (!wrap) return;
|
||
const current = detailState.get(statusKey) || { offset: 0, totalCount: 0, loading: false };
|
||
if (current.loading) return;
|
||
if (!append && form) {
|
||
const selected = getFormFieldValues(form);
|
||
cumulativeFiltersByStatus.set(statusKey, selected);
|
||
renderActiveFilterCards(statusKey);
|
||
}
|
||
const nextOffset = append ? (current.nextOffset || 0) : 0;
|
||
detailState.set(statusKey, { ...current, loading: true, form });
|
||
setLoadMoreState(statusKey, true, true);
|
||
if (!append) {
|
||
wrap.innerHTML = '<div class="table-placeholder">조회 중입니다...</div>';
|
||
}
|
||
try {
|
||
const payload = await fetchJson(`/wehago-compare/api/status-rows?${buildStatusQuery(form, statusKey, { offset: nextOffset, limit: 200 })}`);
|
||
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,
|
||
totalCount: payload.total_count,
|
||
hasMore: payload.has_more,
|
||
loading: false,
|
||
form,
|
||
shownCount,
|
||
});
|
||
setMeta(statusKey, payload.total_count, shownCount, payload.notice, payload);
|
||
setLoadMoreState(statusKey, payload.has_more, false);
|
||
} catch (error) {
|
||
if (!append) {
|
||
wrap.innerHTML = `<div class="table-placeholder">${escapeHtml(error.message)}</div>`;
|
||
}
|
||
detailState.set(statusKey, { ...current, loading: false, form });
|
||
setLoadMoreState(statusKey, current.hasMore, false);
|
||
}
|
||
};
|
||
|
||
const cards = Array.from(document.querySelectorAll('.status-card'));
|
||
const panels = Array.from(document.querySelectorAll('.detail-panel'));
|
||
cards.forEach((card) => {
|
||
card.addEventListener('click', () => {
|
||
// 상태 탭 전환 시 추천/개별 추천 패널은 즉시 닫는다.
|
||
closeRecommendationPanel();
|
||
const targetId = card.dataset.target;
|
||
const statusKey = card.dataset.status;
|
||
const panel = targetId ? document.getElementById(targetId) : null;
|
||
const isActive = panel ? panel.classList.contains('active') : false;
|
||
|
||
cards.forEach((item) => item.setAttribute('aria-expanded', 'false'));
|
||
panels.forEach((item) => item.classList.remove('active'));
|
||
|
||
if (panel && !isActive) {
|
||
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);
|
||
}
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('form[data-filter-kind="status"]').forEach((form) => {
|
||
initStatusSuggest(form, 'voucher');
|
||
initStatusSuggest(form, 'draft');
|
||
initStatusSuggest(form, 'account');
|
||
initStatusSuggest(form, 'vendor');
|
||
initStatusSuggest(form, 'wehago_account');
|
||
initStatusSuggest(form, 'erp_account');
|
||
initStatusSuggest(form, 'wehago_vendor');
|
||
initStatusSuggest(form, 'erp_vendor');
|
||
form.addEventListener('submit', (event) => {
|
||
event.preventDefault();
|
||
const statusKey = form.dataset.status;
|
||
loadStatusRows(statusKey, form, false);
|
||
});
|
||
});
|
||
|
||
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 || '';
|
||
const filterName = removeButton.dataset.filterName || '';
|
||
if (!statusKey || !filterName) return;
|
||
const current = { ...(cumulativeFiltersByStatus.get(statusKey) || {}) };
|
||
delete current[filterName];
|
||
cumulativeFiltersByStatus.set(statusKey, current);
|
||
renderActiveFilterCards(statusKey);
|
||
const form = document.querySelector(`form[data-filter-kind="status"][data-status="${statusKey}"]`);
|
||
if (form) {
|
||
const safeName = escapeSelectorValue(filterName);
|
||
const input = form.querySelector(`input[name="${safeName}"]`);
|
||
if (input) input.value = '';
|
||
}
|
||
loadStatusRows(statusKey, form, false);
|
||
});
|
||
|
||
document.querySelectorAll('[data-load-more]').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
const statusKey = button.dataset.loadMore;
|
||
const state = detailState.get(statusKey);
|
||
const form = state?.form || document.querySelector(`form[data-filter-kind="status"][data-status="${statusKey}"]`);
|
||
loadStatusRows(statusKey, form, true);
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-review-save]').forEach((button) => {
|
||
button.addEventListener('click', async () => {
|
||
const selectedRows = Array.from(recheckSelections.entries())
|
||
.filter(([, checked]) => checked)
|
||
.map(([key]) => recheckRowStore.get(key))
|
||
.filter(Boolean);
|
||
if (!selectedRows.length) {
|
||
alert('저장할 검토 항목을 선택해주세요.');
|
||
return;
|
||
}
|
||
button.disabled = true;
|
||
try {
|
||
const response = await fetch('/wehago-compare/api/recheck-review-save', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
},
|
||
body: JSON.stringify({ rows: selectedRows }),
|
||
});
|
||
const payload = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(payload.error || '검토 저장 중 오류가 발생했습니다.');
|
||
}
|
||
window.location.reload();
|
||
} catch (error) {
|
||
alert(error.message);
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-pair-save]').forEach((button) => {
|
||
button.addEventListener('click', async () => {
|
||
const form = button.closest('form[data-status]');
|
||
const sourceStatus = form?.dataset.status || '';
|
||
if (!['ledger_only', 'voucher_only'].includes(sourceStatus)) {
|
||
alert('개별 추천은 Unmatched 또는 ERP Unmatched에서만 사용할 수 있습니다.');
|
||
return;
|
||
}
|
||
const selectedRows = getSelectedSourceForIndividual(sourceStatus);
|
||
if (!selectedRows.length) {
|
||
alert(sourceStatus === 'ledger_only' ? 'Unmatched에서 항목 1건을 체크해주세요.' : 'ERP Unmatched에서 항목 1건을 체크해주세요.');
|
||
return;
|
||
}
|
||
if (selectedRows.length > 1) {
|
||
alert('개별 추천은 한 번에 1건만 선택해서 실행해주세요.');
|
||
return;
|
||
}
|
||
const sourceRow = selectedRows[0];
|
||
individualState.sourceStatus = sourceStatus;
|
||
individualState.sourceRow = sourceRow;
|
||
individualState.sourceRowKey = sourceStatus === 'ledger_only'
|
||
? (sourceRow.ledger_row_key || '')
|
||
: (sourceRow.voucher_row_key || '');
|
||
individualState.rows = [];
|
||
individualState.nextOffset = 0;
|
||
individualState.hasMore = false;
|
||
individualState.selectedPairKey = '';
|
||
button.disabled = true;
|
||
try {
|
||
openRecommendationPanel('individual');
|
||
await loadIndividualRecommendations(false);
|
||
} catch (error) {
|
||
alert(error.message);
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-pair-recommend]').forEach((button) => {
|
||
button.addEventListener('click', async () => {
|
||
button.disabled = true;
|
||
try {
|
||
const params = buildPairQuery({ limit: 300 });
|
||
const payload = await fetchJson(`/wehago-compare/api/pair-recommendations?${params.toString()}`);
|
||
recommendSelections.clear();
|
||
latestPairRecommendation = payload;
|
||
openRecommendationPanel('batch');
|
||
renderRecommendationSummary(payload.stats || {});
|
||
renderRecommendationTable();
|
||
const stats = payload.stats || {};
|
||
const pairs = payload.pairs || [];
|
||
if (!pairs.length) {
|
||
recommendTableWrap.innerHTML = '<div class="table-placeholder">추천 가능한 쌍이 없습니다. 조회 조건을 조금 더 좁혀주세요.</div>';
|
||
}
|
||
if (stats.recommended) {
|
||
setMeta('ledger_only', stats.ledger_rows, undefined, `추천 ${Number(stats.recommended).toLocaleString()}건 준비됨`);
|
||
setMeta('voucher_only', stats.voucher_rows, undefined, `추천 ${Number(stats.recommended).toLocaleString()}건 준비됨`);
|
||
}
|
||
} catch (error) {
|
||
alert(error.message);
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
});
|
||
|
||
if (recommendHighOnly) {
|
||
recommendHighOnly.addEventListener('change', () => {
|
||
renderRecommendationTable();
|
||
});
|
||
}
|
||
|
||
recommendSortButtons.forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
const key = button.dataset.recommendSort || 'score';
|
||
recommendSortKey = key;
|
||
recommendSortButtons.forEach((item) => {
|
||
item.classList.toggle('active', item === button);
|
||
});
|
||
renderRecommendationTable();
|
||
});
|
||
});
|
||
|
||
if (recommendApplySelected) {
|
||
recommendApplySelected.addEventListener('click', async () => {
|
||
const keys = Array.from(recommendSelections.entries())
|
||
.filter(([, checked]) => checked)
|
||
.map(([key]) => key);
|
||
if (!keys.length) {
|
||
alert('저장할 추천 항목을 선택해주세요.');
|
||
return;
|
||
}
|
||
recommendApplySelected.disabled = true;
|
||
try {
|
||
const params = buildPairQuery();
|
||
const response = await fetch('/wehago-compare/api/pair-recommendations/apply', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
},
|
||
body: JSON.stringify({
|
||
start_year: startYearValue || null,
|
||
end_year: endYearValue || null,
|
||
pair_keys: keys,
|
||
auto_only: false,
|
||
ledger_voucher_no: params.get('ledger_voucher_no') || '',
|
||
ledger_review_reason: params.get('ledger_review_reason') || '',
|
||
voucher_voucher_no: params.get('voucher_voucher_no') || '',
|
||
voucher_review_reason: params.get('voucher_review_reason') || '',
|
||
}),
|
||
});
|
||
const payload = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(payload.error || '추천 선택 저장 중 오류가 발생했습니다.');
|
||
}
|
||
alert(`추천 선택 저장 완료: ${Number(payload.saved_count || 0).toLocaleString()}건`);
|
||
window.location.reload();
|
||
} catch (error) {
|
||
alert(error.message);
|
||
} finally {
|
||
recommendApplySelected.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (recommendClose) {
|
||
recommendClose.addEventListener('click', () => {
|
||
closeRecommendationPanel();
|
||
});
|
||
}
|
||
|
||
if (recommendApplyIndividual) {
|
||
recommendApplyIndividual.addEventListener('click', async () => {
|
||
const picked = (individualState.rows || []).find((row) => row.pair_key === individualState.selectedPairKey);
|
||
if (!picked) {
|
||
alert('매칭할 추천 항목 1건을 선택해주세요.');
|
||
return;
|
||
}
|
||
recommendApplyIndividual.disabled = true;
|
||
try {
|
||
const isLedgerSource = individualState.sourceStatus === 'ledger_only';
|
||
const ledgerRows = isLedgerSource ? [individualState.sourceRow] : [picked.target_row];
|
||
const voucherRows = isLedgerSource ? [picked.target_row] : [individualState.sourceRow];
|
||
const response = await fetch('/wehago-compare/api/pair-match-save', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
},
|
||
body: JSON.stringify({ ledger_rows: ledgerRows, voucher_rows: voucherRows }),
|
||
});
|
||
const payload = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(payload.error || '개별 매칭 저장 중 오류가 발생했습니다.');
|
||
}
|
||
alert(`개별 매칭 저장 완료: ${Number(payload.saved_count || 0).toLocaleString()}건`);
|
||
window.location.reload();
|
||
} catch (error) {
|
||
alert(error.message);
|
||
} finally {
|
||
recommendApplyIndividual.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (recommendTableWrap) {
|
||
recommendTableWrap.addEventListener('scroll', () => {
|
||
if (recommendMode !== 'individual') return;
|
||
if (!individualState.hasMore || individualState.loading) return;
|
||
const remain = recommendTableWrap.scrollHeight - recommendTableWrap.scrollTop - recommendTableWrap.clientHeight;
|
||
if (remain <= 100) {
|
||
loadIndividualRecommendations(true);
|
||
}
|
||
});
|
||
}
|
||
|
||
if (undoLastActionBtn) {
|
||
undoLastActionBtn.addEventListener('click', async () => {
|
||
const confirmed = window.confirm('직전 작업을 취소할까요? 최근 저장 작업 1건만 되돌립니다.');
|
||
if (!confirmed) return;
|
||
undoLastActionBtn.disabled = true;
|
||
try {
|
||
const response = await fetch('/wehago-compare/api/undo-last-action', {
|
||
method: 'POST',
|
||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||
});
|
||
const payload = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(payload.error || '되돌리기 중 오류가 발생했습니다.');
|
||
}
|
||
if (!payload.undone) {
|
||
alert(payload.message || '되돌릴 작업이 없습니다.');
|
||
} else {
|
||
alert(`되돌리기 완료: ${formatNumber(payload.affected || 0)}건`);
|
||
window.location.reload();
|
||
}
|
||
} catch (error) {
|
||
alert(error.message);
|
||
} finally {
|
||
undoLastActionBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('[data-table-wrap]').forEach((wrap) => {
|
||
const statusKey = wrap.dataset.tableWrap;
|
||
if (!statusKey) {
|
||
return;
|
||
}
|
||
wrap.addEventListener('scroll', () => {
|
||
const state = detailState.get(statusKey);
|
||
if (!state || state.loading || !state.hasMore) {
|
||
return;
|
||
}
|
||
const threshold = 120;
|
||
const remain = wrap.scrollHeight - wrap.scrollTop - wrap.clientHeight;
|
||
if (remain <= threshold) {
|
||
const form = state.form || document.querySelector(`form[data-filter-kind="status"][data-status="${statusKey}"]`);
|
||
loadStatusRows(statusKey, form, true);
|
||
}
|
||
});
|
||
});
|
||
|
||
const startYear = document.getElementById('start_year');
|
||
const endYear = document.getElementById('end_year');
|
||
syncRecommendationPanelState();
|
||
const syncYearOptions = () => {
|
||
if (!startYear || !endYear) {
|
||
return;
|
||
}
|
||
const startValue = Number(startYear.value || 0);
|
||
const endValue = Number(endYear.value || 0);
|
||
Array.from(startYear.options).forEach((option) => {
|
||
option.disabled = Boolean(endValue) && Number(option.value) > endValue;
|
||
});
|
||
Array.from(endYear.options).forEach((option) => {
|
||
option.disabled = Boolean(startValue) && Number(option.value) < startValue;
|
||
});
|
||
if (startValue && endValue && startValue > endValue) {
|
||
endYear.value = startYear.value;
|
||
}
|
||
};
|
||
if (startYear && endYear) {
|
||
startYear.addEventListener('change', syncYearOptions);
|
||
endYear.addEventListener('change', syncYearOptions);
|
||
syncYearOptions();
|
||
}
|
||
window.setTimeout(() => {
|
||
loadDashboardSummary();
|
||
}, 400);
|
||
refreshLastActionMeta();
|
||
})();
|
||
</script>
|
||
{% endblock %}
|