Update wehago matching logic and exclude reports
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -251,6 +251,7 @@
|
||||
<div class="section-title" style="margin-bottom: 14px;">
|
||||
<h2>수익/비용 현황</h2>
|
||||
<div class="summary-actions">
|
||||
<a class="button-link button-secondary" href="/annual-summary/gap-analysis">차이분석</a>
|
||||
<button type="button" class="button-secondary" id="annualRebuildCacheBtn">캐시 재계산</button>
|
||||
<span class="page-job-status" id="annualJobStatus" data-state="">작업 상태 확인 전</span>
|
||||
</div>
|
||||
|
||||
+687
-40
@@ -67,6 +67,81 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hanmac-connection-section {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 14px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.hanmac-connection-section:first-child {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.hanmac-connection-section-title {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.hanmac-connection-section-title strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hanmac-connection-section-title span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.hanmac-connection-section.is-erp {
|
||||
margin: 0 -10px;
|
||||
padding: 14px 10px;
|
||||
border: 1px solid #cfd9e8;
|
||||
border-radius: 14px;
|
||||
background: #f6f9fd;
|
||||
}
|
||||
|
||||
.hanmac-connection-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-bottom: 2px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hanmac-connection-tab {
|
||||
min-width: 104px;
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: var(--ink);
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hanmac-connection-tab.is-active {
|
||||
border-color: #111111;
|
||||
background: #111111;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hanmac-connection-section[hidden],
|
||||
.hanmac-connection-actions[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hanmac-erp-server-note {
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: #eaf1fb;
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hanmac-grade-panel table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -165,6 +240,71 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hanmac-wehago-audit {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.hanmac-wehago-audit-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hanmac-wehago-audit-title strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hanmac-wehago-audit-title a,
|
||||
.hanmac-wehago-audit a {
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hanmac-wehago-audit-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.hanmac-wehago-audit-item {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid #edf1f5;
|
||||
}
|
||||
|
||||
.hanmac-wehago-audit-item:first-child {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.hanmac-wehago-audit-item span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.hanmac-wehago-year-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hanmac-wehago-year-links a {
|
||||
min-width: 48px;
|
||||
padding: 5px 7px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hanmac-filter-bar {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -1155,12 +1295,18 @@
|
||||
|
||||
.hanmac-modal-card {
|
||||
width: min(760px, 100%);
|
||||
max-height: calc(100vh - 48px);
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 20px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.99);
|
||||
box-shadow: 0 20px 50px rgba(17, 17, 17, 0.18);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#hanmacConfigModal .hanmac-modal-card {
|
||||
width: min(860px, 100%);
|
||||
}
|
||||
|
||||
.hanmac-modal-head {
|
||||
@@ -1213,10 +1359,34 @@
|
||||
}
|
||||
|
||||
.hanmac-modal-actions {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto minmax(0, 1fr);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.hanmac-modal-actions .hanmac-button,
|
||||
.hanmac-connection-actions {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hanmac-connection-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.hanmac-modal-actions .hanmac-inline-status {
|
||||
flex: 1 0 360px;
|
||||
min-width: 360px;
|
||||
max-width: none;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
@@ -1252,8 +1422,7 @@
|
||||
@media (max-width: 720px) {
|
||||
.hanmac-form-grid,
|
||||
.hanmac-aggregate-summary,
|
||||
.hanmac-preview-summary,
|
||||
.hanmac-modal-actions {
|
||||
.hanmac-preview-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -1313,6 +1482,29 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hanmac-wehago-audit">
|
||||
<div class="hanmac-wehago-audit-title">
|
||||
<strong>WEHAGO 검증자료</strong>
|
||||
<a href="{{ hanmac_wehago_audit_sources.smarta_home_url }}" target="_blank" rel="noreferrer">SmartA</a>
|
||||
</div>
|
||||
<div class="hanmac-wehago-audit-list">
|
||||
{% for menu in hanmac_wehago_audit_sources.menus %}
|
||||
<div class="hanmac-wehago-audit-item">
|
||||
<a href="{{ hanmac_wehago_audit_sources.smarta_home_url }}" target="_blank" rel="noreferrer">{{ menu.name }}{% if menu.program %} · {{ menu.program }}{% endif %}</a>
|
||||
<span>{{ menu.basis }} · 메뉴 검색어: {{ menu.keyword }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="hanmac-wehago-year-links" aria-label="계정별원장 연도별 바로가기">
|
||||
{% for item in hanmac_wehago_audit_sources.ledger_urls %}
|
||||
<a href="{{ item.url }}" target="_blank" rel="noreferrer">{{ item.year }}<br>{{ item.gisu }}기</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="hanmac-panel-meta">
|
||||
검토 계정: {{ hanmac_wehago_audit_sources.account_codes|join(", ") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hanmac-filter-bar">
|
||||
<input id="hanmacTableSearch" type="search" placeholder="테이블 검색">
|
||||
<div class="hanmac-filter-tabs">
|
||||
@@ -1459,46 +1651,107 @@
|
||||
<div class="section-title" style="margin-bottom: 0;">
|
||||
<h2>서버 접속 정보</h2>
|
||||
</div>
|
||||
<div class="hanmac-connection-tabs" role="tablist" aria-label="서버 접속 정보 구분">
|
||||
<button type="button" class="hanmac-connection-tab is-active" data-connection-tab="work" role="tab" aria-selected="true">근무DB</button>
|
||||
<button type="button" class="hanmac-connection-tab" data-connection-tab="erp" role="tab" aria-selected="false">관리DB</button>
|
||||
</div>
|
||||
<button type="button" class="hanmac-close-button" id="hanmacCloseConfigButton" aria-label="닫기">×</button>
|
||||
</div>
|
||||
|
||||
<form id="hanmacConnectionForm">
|
||||
<div class="hanmac-form-grid">
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-host">서버 IP</label>
|
||||
<input id="hanmac-host" name="host" type="text" value="172.16.42.111" autocomplete="off">
|
||||
<section class="hanmac-connection-section" data-connection-panel="work">
|
||||
<div class="hanmac-connection-section-title">
|
||||
<strong>근무 DB 접속</strong>
|
||||
<span>hanmac DB_external 조회와 집계에 사용하는 MySQL 계정입니다.</span>
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-port">포트</label>
|
||||
<input id="hanmac-port" name="port" type="text" value="3306" autocomplete="off">
|
||||
<div class="hanmac-form-grid">
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-host">서버 IP</label>
|
||||
<input id="hanmac-host" name="host" type="text" value="172.16.42.111" autocomplete="off">
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-port">포트</label>
|
||||
<input id="hanmac-port" name="port" type="text" value="3306" autocomplete="off">
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-user">아이디</label>
|
||||
<input id="hanmac-user" name="user" type="text" value="root" autocomplete="off">
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-password">비밀번호</label>
|
||||
<input id="hanmac-password" name="password" type="password" value="" placeholder="여기에 비밀번호 입력">
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-database">기본 연결 DB</label>
|
||||
<select id="hanmac-database" name="database">
|
||||
<option value="hanmac_manhour">hanmac_manhour</option>
|
||||
<option value="baron_manhour">baron_manhour</option>
|
||||
<option value="hanmac">hanmac</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-engine">DB 종류</label>
|
||||
<input id="hanmac-engine" type="text" value="MySQL" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-user">아이디</label>
|
||||
<input id="hanmac-user" name="user" type="text" value="root" autocomplete="off">
|
||||
</section>
|
||||
<section class="hanmac-connection-section is-erp" data-connection-panel="erp" hidden>
|
||||
<div class="hanmac-connection-section-title">
|
||||
<strong>관리DB / 관리 ERP 접속</strong>
|
||||
<span>입력한 아이디와 비밀번호는 현재 브라우저 탭이 열려 있는 동안만 저장됩니다.</span>
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-password">비밀번호</label>
|
||||
<input id="hanmac-password" name="password" type="password" value="" placeholder="여기에 비밀번호 입력">
|
||||
<div class="hanmac-erp-server-note">
|
||||
웹 주소: http://erp.hanmaceng.co.kr/planning_mng/<br>
|
||||
MySQL 직접 경로: erp.hanmaceng.co.kr:3306 · 서버: MySQL 5.1.41-community-log
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-database">기본 연결 DB</label>
|
||||
<select id="hanmac-database" name="database">
|
||||
<option value="hanmac_manhour">hanmac_manhour</option>
|
||||
<option value="baron_manhour">baron_manhour</option>
|
||||
<option value="hanmac">hanmac</option>
|
||||
</select>
|
||||
<div class="hanmac-form-grid">
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-erp-host">관리DB 서버</label>
|
||||
<input id="hanmac-erp-host" type="text" value="erp.hanmaceng.co.kr" readonly>
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-erp-port">MySQL 포트</label>
|
||||
<input id="hanmac-erp-port" type="text" value="3306" readonly>
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-erp-engine">DB 서버</label>
|
||||
<input id="hanmac-erp-engine" type="text" value="MySQL 5.1.41-community-log" readonly>
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-erp-user">관리 ERP 아이디</label>
|
||||
<input id="hanmac-erp-user" name="erp_user" type="text" value="" autocomplete="username" placeholder="관리 ERP 아이디 입력">
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-erp-password">관리 ERP 비밀번호</label>
|
||||
<input id="hanmac-erp-password" name="erp_password" type="password" value="" autocomplete="current-password" placeholder="관리 ERP 비밀번호 입력">
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-erp-db-user">관리DB MySQL 아이디</label>
|
||||
<input id="hanmac-erp-db-user" name="erp_db_user" type="text" value="" autocomplete="off" placeholder="DB 계정이 ERP 계정과 다르면 입력">
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-erp-db-password">관리DB MySQL 비밀번호</label>
|
||||
<input id="hanmac-erp-db-password" name="erp_db_password" type="password" value="" autocomplete="off" placeholder="MySQL 직접 접속 비밀번호 입력">
|
||||
</div>
|
||||
</div>
|
||||
<div class="hanmac-field">
|
||||
<label for="hanmac-engine">DB 종류</label>
|
||||
<input id="hanmac-engine" type="text" value="MySQL" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<div class="hanmac-modal-actions">
|
||||
<button type="button" class="hanmac-button secondary" id="hanmacTestConnectionButton">연결 확인</button>
|
||||
<button type="button" class="hanmac-button" id="hanmacLoadTablesButton">테이블 목록 보기</button>
|
||||
<button type="button" class="hanmac-button secondary" id="hanmacLoadGradeCodesButton">직급 코드 조회</button>
|
||||
<div class="hanmac-connection-actions" data-connection-actions="work">
|
||||
<button type="button" class="hanmac-button secondary" id="hanmacTestConnectionButton">연결 확인</button>
|
||||
<button type="button" class="hanmac-button" id="hanmacLoadTablesButton">테이블 목록 보기</button>
|
||||
<button type="button" class="hanmac-button secondary" id="hanmacLoadGradeCodesButton">직급 코드 조회</button>
|
||||
</div>
|
||||
<div class="hanmac-connection-actions" data-connection-actions="erp" hidden>
|
||||
<button type="button" class="hanmac-button secondary" id="hanmacTestErpButton">관리 ERP·MySQL 확인</button>
|
||||
<button type="button" class="hanmac-button" id="hanmacDiscoverSatisBudgetButton">Satis 예산 후보 탐색</button>
|
||||
<button type="button" class="hanmac-button" id="hanmacSyncSatisBudgetButton">Satis 예산 금액 저장</button>
|
||||
<button type="button" class="hanmac-button" id="hanmacNormalizeSatisBudgetButton">Satis 예산 정규화</button>
|
||||
<button type="button" class="hanmac-button" id="hanmacProjectSatisBudgetButton">승인 최신 차수 반영</button>
|
||||
<button type="button" class="hanmac-button" id="hanmacRunSatisBudgetFullSyncButton">Satis 예산 전체 실행</button>
|
||||
<button type="button" class="hanmac-button" id="hanmacCollectSatisBudgetWebButton">Satis 웹로그인 예산 수집</button>
|
||||
</div>
|
||||
<div class="hanmac-inline-status" id="hanmacConnectionStatus"></div>
|
||||
</div>
|
||||
<div class="hanmac-grade-panel" id="hanmacGradeCodesPanel"></div>
|
||||
@@ -1622,9 +1875,19 @@
|
||||
const closeConfigButton = document.getElementById("hanmacCloseConfigButton");
|
||||
const statusBox = document.getElementById("hanmacConnectionStatus");
|
||||
const testButton = document.getElementById("hanmacTestConnectionButton");
|
||||
const testErpButton = document.getElementById("hanmacTestErpButton");
|
||||
const discoverSatisBudgetButton = document.getElementById("hanmacDiscoverSatisBudgetButton");
|
||||
const syncSatisBudgetButton = document.getElementById("hanmacSyncSatisBudgetButton");
|
||||
const normalizeSatisBudgetButton = document.getElementById("hanmacNormalizeSatisBudgetButton");
|
||||
const projectSatisBudgetButton = document.getElementById("hanmacProjectSatisBudgetButton");
|
||||
const runSatisBudgetFullSyncButton = document.getElementById("hanmacRunSatisBudgetFullSyncButton");
|
||||
const collectSatisBudgetWebButton = document.getElementById("hanmacCollectSatisBudgetWebButton");
|
||||
const loadTablesButton = document.getElementById("hanmacLoadTablesButton");
|
||||
const loadGradeCodesButton = document.getElementById("hanmacLoadGradeCodesButton");
|
||||
const gradeCodesPanel = document.getElementById("hanmacGradeCodesPanel");
|
||||
const connectionTabs = Array.from(document.querySelectorAll("[data-connection-tab]"));
|
||||
const connectionPanels = Array.from(document.querySelectorAll("[data-connection-panel]"));
|
||||
const connectionActions = Array.from(document.querySelectorAll("[data-connection-actions]"));
|
||||
const tableListMeta = document.getElementById("hanmacTableListMeta");
|
||||
const tableList = document.getElementById("hanmacTableList");
|
||||
const tableSearch = document.getElementById("hanmacTableSearch");
|
||||
@@ -1734,6 +1997,7 @@
|
||||
let aggregateLoadPromise = null;
|
||||
let aggregateRequestSeq = 0;
|
||||
const credentialStorageKey = "hanmac-db-external-credentials-v1";
|
||||
const erpCredentialStorageKey = "hanmac-management-erp-session-credentials-v1";
|
||||
const aggregateStateStorageKey = "hanmac-db-external-aggregate-state-v1";
|
||||
const jointMembersFallbackUrl = "/static/hanmac-joint-members-cache.json";
|
||||
|
||||
@@ -1780,6 +2044,23 @@
|
||||
DeptCode: "부서코드",
|
||||
};
|
||||
|
||||
const selectConnectionTab = (tabName) => {
|
||||
const selectedTab = tabName === "erp" ? "erp" : "work";
|
||||
connectionTabs.forEach((button) => {
|
||||
const active = button.dataset.connectionTab === selectedTab;
|
||||
button.classList.toggle("is-active", active);
|
||||
button.setAttribute("aria-selected", active ? "true" : "false");
|
||||
});
|
||||
connectionPanels.forEach((panel) => {
|
||||
panel.hidden = panel.dataset.connectionPanel !== selectedTab;
|
||||
});
|
||||
connectionActions.forEach((actions) => {
|
||||
actions.hidden = actions.dataset.connectionActions !== selectedTab;
|
||||
});
|
||||
gradeCodesPanel.classList.toggle("is-open", selectedTab === "work" && gradeCodesPanel.innerHTML.trim() !== "");
|
||||
setStatus("");
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
modal.hidden = false;
|
||||
};
|
||||
@@ -1848,6 +2129,39 @@
|
||||
};
|
||||
};
|
||||
|
||||
const getErpPayload = () => {
|
||||
const formData = new FormData(form);
|
||||
return {
|
||||
erp_user: String(formData.get("erp_user") || "").trim(),
|
||||
erp_password: String(formData.get("erp_password") || ""),
|
||||
erp_db_user: String(formData.get("erp_db_user") || "").trim(),
|
||||
erp_db_password: String(formData.get("erp_db_password") || ""),
|
||||
};
|
||||
};
|
||||
|
||||
const saveErpCredentials = () => {
|
||||
try {
|
||||
window.sessionStorage.setItem(erpCredentialStorageKey, JSON.stringify(getErpPayload()));
|
||||
} catch (_error) {
|
||||
// Keep the form usable when session storage is unavailable.
|
||||
}
|
||||
};
|
||||
|
||||
const restoreErpCredentials = () => {
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(erpCredentialStorageKey);
|
||||
if (!raw) return false;
|
||||
const saved = JSON.parse(raw);
|
||||
["erp_user", "erp_password", "erp_db_user", "erp_db_password"].forEach((key) => {
|
||||
const field = form.elements.namedItem(key);
|
||||
if (field && key in saved) field.value = String(saved[key] || "");
|
||||
});
|
||||
return Boolean(saved.erp_user || saved.erp_password || saved.erp_db_user || saved.erp_db_password);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchJson = async (url, options = {}) => {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
@@ -2289,17 +2603,21 @@
|
||||
"project_count",
|
||||
]);
|
||||
const aggregateDetailColumns = new Set([
|
||||
"population_review",
|
||||
"regular_hours",
|
||||
"overtime_hours",
|
||||
"total_hours",
|
||||
"legal_leave_days",
|
||||
"missing_regular_days",
|
||||
"project_count",
|
||||
]);
|
||||
const aggregateDetailLabels = {
|
||||
population_review: "소속검토",
|
||||
regular_hours: "정규근로",
|
||||
overtime_hours: "연장근로",
|
||||
total_hours: "총근로",
|
||||
legal_leave_days: "법정휴가",
|
||||
missing_regular_days: "정규근로 부족",
|
||||
project_count: "프로젝트수",
|
||||
};
|
||||
|
||||
@@ -2468,7 +2786,11 @@
|
||||
|
||||
const getEquivalentProjectTitle = (row) => {
|
||||
const equivalentCodes = Array.isArray(row?.equivalent_project_codes) ? row.equivalent_project_codes.filter(Boolean) : [];
|
||||
return equivalentCodes.length ? equivalentCodes.join(", ") : "";
|
||||
const sourceCodes = Array.isArray(row?.source_project_codes) ? row.source_project_codes.filter(Boolean) : [];
|
||||
const parts = [];
|
||||
if (sourceCodes.length) parts.push(`원천코드: ${sourceCodes.join(", ")}`);
|
||||
if (equivalentCodes.length) parts.push(`연결코드: ${equivalentCodes.join(", ")}`);
|
||||
return parts.join(" / ");
|
||||
};
|
||||
|
||||
const renderProjectCodeText = (row, fallbackKey = "project_code") => {
|
||||
@@ -2486,9 +2808,27 @@
|
||||
const renderAggregateMetricCell = (row, column, rowIndex, view) => {
|
||||
if (view === "member" && column.key === "remarks") {
|
||||
const duplicateDays = Number(row.multi_entry_days || 0);
|
||||
const detailButton = duplicateDays > 0
|
||||
? `<button type="button" class="hanmac-aggregate-detail-link" data-aggregate-duplicate-index="${rowIndex}">중복 ${duplicateDays.toLocaleString("ko-KR")}일</button>`
|
||||
const overlapCounts = row.overlap_type_counts && typeof row.overlap_type_counts === "object"
|
||||
? Object.entries(row.overlap_type_counts).filter(([, count]) => Number(count || 0) > 0)
|
||||
: [];
|
||||
const overlapLabel = overlapCounts.length
|
||||
? overlapCounts.map(([label, count]) => `${label} ${Number(count).toLocaleString("ko-KR")}일`).join(" · ")
|
||||
: `검토 필요 ${duplicateDays.toLocaleString("ko-KR")}일`;
|
||||
const duplicateButton = duplicateDays > 0
|
||||
? `<button type="button" class="hanmac-aggregate-detail-link" data-aggregate-duplicate-index="${rowIndex}">${escapeHtml(overlapLabel)}</button>`
|
||||
: "";
|
||||
const missingDays = Number(row.missing_regular_days || 0);
|
||||
const missingHours = Number(row.missing_regular_hours || 0);
|
||||
const missingButton = missingHours > 0
|
||||
? `<button type="button" class="hanmac-aggregate-detail-link" data-aggregate-detail-index="${rowIndex}" data-aggregate-detail-key="missing_regular_days">정규근로 ${formatNumberText(missingDays)}일 부족 (${formatNumberText(missingHours)}시간)</button>`
|
||||
: "";
|
||||
return `<td>${[missingButton, duplicateButton].filter(Boolean).join(" · ")}</td>`;
|
||||
}
|
||||
if (view === "member" && column.key === "population_review") {
|
||||
const label = String(row.population_review || "");
|
||||
const detailButton = label && canOpenAggregateDetail(row, column.key)
|
||||
? `<button type="button" class="hanmac-aggregate-detail-link" data-aggregate-detail-index="${rowIndex}" data-aggregate-detail-key="population_review">${escapeHtml(label)}</button>`
|
||||
: escapeHtml(label);
|
||||
return `<td>${detailButton}</td>`;
|
||||
}
|
||||
const isNumeric = aggregateNumericColumns.has(column.key);
|
||||
@@ -2703,15 +3043,16 @@
|
||||
|
||||
const openMultiEntryModal = (row) => {
|
||||
const details = Array.isArray(row?.multi_entry_details) ? row.multi_entry_details : [];
|
||||
multiEntryTitle.textContent = `${row?.member_name || row?.member_no || "사원"} 중복 근무 상세`;
|
||||
multiEntryMeta.textContent = `${row?.member_no || ""} · ${Number(row?.multi_entry_days || details.length || 0).toLocaleString("ko-KR")}일 중복`;
|
||||
multiEntryTitle.textContent = `${row?.member_name || row?.member_no || "사원"} 근무기록 검토 상세`;
|
||||
multiEntryMeta.textContent = `${row?.member_no || ""} · ${Number(row?.multi_entry_days || details.length || 0).toLocaleString("ko-KR")}일 검토 필요`;
|
||||
if (!details.length) {
|
||||
multiEntryBody.innerHTML = `<div class="hanmac-preview-empty">중복 상세가 없습니다.</div>`;
|
||||
multiEntryBody.innerHTML = `<div class="hanmac-preview-empty">검토할 근무기록이 없습니다.</div>`;
|
||||
} else {
|
||||
multiEntryBody.innerHTML = details.map((detail) => `
|
||||
<section class="hanmac-detail-day">
|
||||
<div class="hanmac-detail-day-head">
|
||||
<strong class="hanmac-detail-day-title">${escapeHtml(detail.work_date || "-")}</strong>
|
||||
<span class="hanmac-detail-badge">${escapeHtml(detail.overlap_type || "근무기록 검토")}</span>
|
||||
<span class="hanmac-detail-badge">원본근로 ${formatNumberText(detail.raw_total_hours)}시간</span>
|
||||
<span class="hanmac-detail-badge">집계반영 ${formatNumberText(detail.capped_regular_hours)}시간</span>
|
||||
<span class="hanmac-detail-badge">${Number(detail.row_count || 0).toLocaleString("ko-KR")}행</span>
|
||||
@@ -2735,6 +3076,14 @@
|
||||
<td>${Number(entry.row_count || 0) > 1 ? `원본 ${Number(entry.row_count).toLocaleString("ko-KR")}행` : ""}</td>
|
||||
</tr>
|
||||
`).join("")}
|
||||
${(Array.isArray(detail.activity_evidence) ? detail.activity_evidence : []).map((entry) => `
|
||||
<tr>
|
||||
<td>${renderProjectCodeText(entry)}</td>
|
||||
<td>${escapeHtml(entry.project_name || entry.project_code || "공통/미지정")}</td>
|
||||
<td>-</td>
|
||||
<td>${escapeHtml([entry.source_label || "활동 근거", entry.note || ""].filter(Boolean).join(" · "))}</td>
|
||||
</tr>
|
||||
`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
@@ -2787,7 +3136,7 @@
|
||||
if (currentAggregateView !== "project" && Array.isArray(detail.projects) && detail.projects.length) {
|
||||
return detail.projects.map((project) => ({
|
||||
workDate: detail.work_date,
|
||||
project: `${sourcePrefix(project.source_label)}${project.project_name || project.project_code || "(미지정)"}${project.project_code ? ` (${project.project_code})` : ""}`,
|
||||
project: `${sourcePrefix(project.source_label)}${project.project_name || project.project_code || "(미지정)"}${project.project_code ? ` (${project.project_code})` : ""}${Array.isArray(project.source_project_codes) && project.source_project_codes.length ? ` · 원천 ${project.source_project_codes.join(", ")}` : ""}`,
|
||||
recognizedHours: project.recognized_hours ?? (detail.projects.length === 1 ? detail.regular_hours : 0),
|
||||
actualHours: project.hours,
|
||||
}));
|
||||
@@ -2824,6 +3173,60 @@
|
||||
});
|
||||
|
||||
const renderAggregateDetailRows = (detailKey, details) => {
|
||||
if (detailKey === "population_review") {
|
||||
return `
|
||||
<table class="hanmac-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>판정</th>
|
||||
<th>사유</th>
|
||||
<th>Company</th>
|
||||
<th>WorkCompany</th>
|
||||
<th>부서</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${details.map((detail) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(detail.decision || "소속검토필요")}</td>
|
||||
<td>${escapeHtml(detail.reason || "")}</td>
|
||||
<td>${escapeHtml(detail.company || "")}</td>
|
||||
<td>${escapeHtml(detail.work_company || "")}</td>
|
||||
<td>${escapeHtml(detail.dept_name || "")}</td>
|
||||
</tr>
|
||||
`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
if (detailKey === "missing_regular_days") {
|
||||
return `
|
||||
<table class="hanmac-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>일자</th>
|
||||
<th class="is-numeric">근무의무</th>
|
||||
<th class="is-numeric">휴가·시차</th>
|
||||
<th class="is-numeric">인정근로</th>
|
||||
<th class="is-numeric">부족</th>
|
||||
<th>판정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${details.map((detail) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(detail.work_date || "-")}</td>
|
||||
<td class="is-numeric">${formatNumberText(detail.expected_hours)}</td>
|
||||
<td class="is-numeric">${formatNumberText(detail.leave_hours)}</td>
|
||||
<td class="is-numeric">${formatNumberText(detail.recognized_hours)}</td>
|
||||
<td class="is-numeric">${formatNumberText(detail.missing_hours)}</td>
|
||||
<td>${escapeHtml(detail.reason || "")}</td>
|
||||
</tr>
|
||||
`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
if (detailKey === "project_count") {
|
||||
return `
|
||||
<table class="hanmac-detail-table">
|
||||
@@ -2983,8 +3386,9 @@
|
||||
aggregateLeaveDays.textContent = Number(summary.legal_leave_hours || 0).toLocaleString("ko-KR");
|
||||
aggregateProjectCount.textContent = Number(summary.project_count || 0).toLocaleString("ko-KR");
|
||||
const diagnostics = payload.source_diagnostics || {};
|
||||
const logicVersionText = diagnostics.logic_version ? ` · 로직 ${diagnostics.logic_version}` : "";
|
||||
const diagnosticText = diagnostics
|
||||
? ` · 공식연장 ${Number(diagnostics.official_overtime_rows || 0).toLocaleString("ko-KR")}행 · addwork ${Number(diagnostics.addwork_parsed_rows || 0).toLocaleString("ko-KR")}/${Number(diagnostics.addwork_source_rows || 0).toLocaleString("ko-KR")}행 · 합사 ${Number(diagnostics.joint_assignment_records || 0).toLocaleString("ko-KR")}건(코드 ${Number(diagnostics.joint_assignment_code_matched_rows || 0).toLocaleString("ko-KR")}, 문구 ${Number(diagnostics.joint_assignment_text_matched_rows || 0).toLocaleString("ko-KR")})/${Number(diagnostics.joint_assignment_regular_rows || 0).toLocaleString("ko-KR")}일 · 합사연장 ${Number(diagnostics.joint_assignment_overtime_rows || 0).toLocaleString("ko-KR")}일 · 사번통합 ${Number(diagnostics.canonical_member_no_rows || 0).toLocaleString("ko-KR")}행 · 임계 제외 ${Number((diagnostics.addwork_weekday_threshold_filtered_rows || 0) + (diagnostics.addwork_holiday_threshold_filtered_rows || 0)).toLocaleString("ko-KR")}행 · 휴가 ${Number(diagnostics.leave_matched_rows || 0).toLocaleString("ko-KR")}/${Number(diagnostics.tardy_candidate_rows || 0).toLocaleString("ko-KR")}행 · 탄력 제외 ${Number(diagnostics.leave_flexible_work_excluded_rows || 0).toLocaleString("ko-KR")}행 · 부서 연결 ${Number(diagnostics.dept_mapped_rows || 0).toLocaleString("ko-KR")}명 · 바론 제외 ${Number(diagnostics.center_excluded_member_count || 0).toLocaleString("ko-KR")}명 (동일사번 ${Number(diagnostics.center_same_member_hidden_count || 0).toLocaleString("ko-KR")}명)`
|
||||
? `${logicVersionText} · 공식연장 ${Number(diagnostics.official_overtime_rows || 0).toLocaleString("ko-KR")}행 · addwork ${Number(diagnostics.addwork_parsed_rows || 0).toLocaleString("ko-KR")}/${Number(diagnostics.addwork_source_rows || 0).toLocaleString("ko-KR")}행 · 합사 ${Number(diagnostics.joint_assignment_records || 0).toLocaleString("ko-KR")}건(코드 ${Number(diagnostics.joint_assignment_code_matched_rows || 0).toLocaleString("ko-KR")}, 문구 ${Number(diagnostics.joint_assignment_text_matched_rows || 0).toLocaleString("ko-KR")}건)/${Number(diagnostics.joint_assignment_regular_rows || 0).toLocaleString("ko-KR")}일 · 합사연장 ${Number(diagnostics.joint_assignment_overtime_rows || 0).toLocaleString("ko-KR")}일 · 사번통합 ${Number(diagnostics.canonical_member_no_rows || 0).toLocaleString("ko-KR")}행 · 임계 제외 ${Number((diagnostics.addwork_weekday_threshold_filtered_rows || 0) + (diagnostics.addwork_holiday_threshold_filtered_rows || 0)).toLocaleString("ko-KR")}행 · 휴가 ${Number(diagnostics.leave_matched_rows || 0).toLocaleString("ko-KR")}/${Number(diagnostics.tardy_candidate_rows || 0).toLocaleString("ko-KR")}행 · 탄력 제외 ${Number(diagnostics.leave_flexible_work_excluded_rows || 0).toLocaleString("ko-KR")}행 · 부서 연결 ${Number(diagnostics.dept_mapped_rows || 0).toLocaleString("ko-KR")}명 · 계열사스키마 ${Number(diagnostics.affiliate_schema_count || 0).toLocaleString("ko-KR")}개 · 계열사/소속 제외 ${Number((diagnostics.population_owner_excluded_member_count || 0) + (diagnostics.affiliate_actual_work_excluded_count || diagnostics.center_excluded_member_count || 0)).toLocaleString("ko-KR")}명 · 소속코드 검토 ${Number(diagnostics.company_review_member_count || 0).toLocaleString("ko-KR")}명 · 근무기록 없음 검토 ${Number(diagnostics.no_hanmac_work_review_member_count || 0).toLocaleString("ko-KR")}명 · 시스템 제외 ${Number(diagnostics.system_member_filtered_rows || 0).toLocaleString("ko-KR")}건`
|
||||
: "";
|
||||
aggregateMeta.textContent = `${payload.start_date || ""} ~ ${payload.end_date || ""}${diagnosticText}`;
|
||||
if (currentAggregateSort.key && !columns.some((column) => column.key === currentAggregateSort.key)) {
|
||||
@@ -3287,6 +3691,12 @@
|
||||
|
||||
openConfigButton.addEventListener("click", openModal);
|
||||
closeConfigButton.addEventListener("click", closeModal);
|
||||
connectionTabs.forEach((button) => {
|
||||
button.addEventListener("click", () => selectConnectionTab(button.dataset.connectionTab || "work"));
|
||||
});
|
||||
["erp_user", "erp_password", "erp_db_user", "erp_db_password"].forEach((name) => {
|
||||
form.elements.namedItem(name)?.addEventListener("input", saveErpCredentials);
|
||||
});
|
||||
modal.addEventListener("click", (event) => {
|
||||
if (event.target === modal) closeModal();
|
||||
});
|
||||
@@ -3390,6 +3800,241 @@
|
||||
}
|
||||
});
|
||||
|
||||
testErpButton.addEventListener("click", async () => {
|
||||
testErpButton.disabled = true;
|
||||
setStatus("관리 ERP 로그인과 내부 접근 범위를 확인하고 있습니다...");
|
||||
try {
|
||||
saveErpCredentials();
|
||||
const payload = await fetchJson("/hanmac-browser/api/test-management-erp", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(getErpPayload()),
|
||||
});
|
||||
setStatus(
|
||||
`${payload.message} 내부 링크 ${Number(payload.internal_link_count || 0).toLocaleString("ko-KR")}개 확인 · ${payload.direct_db_message || "DB 직접 접속정보 미확인"} · ${payload.transport_warning || ""}`,
|
||||
"success",
|
||||
);
|
||||
} catch (error) {
|
||||
setStatus(error.message || "관리 ERP 접근 확인 중 오류가 발생했습니다.", "error");
|
||||
} finally {
|
||||
testErpButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
discoverSatisBudgetButton.addEventListener("click", async () => {
|
||||
discoverSatisBudgetButton.disabled = true;
|
||||
setStatus("Satis 예산 후보 DB·테이블을 탐색하고 로컬 이력 저장소를 준비하고 있습니다...");
|
||||
try {
|
||||
saveErpCredentials();
|
||||
const payload = await fetchJson("/hanmac-browser/api/discover-satis-budget", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(getErpPayload()),
|
||||
});
|
||||
const dbCount = Number(payload.direct_db_database_count || 0).toLocaleString("ko-KR");
|
||||
const tableCount = Number(payload.candidate_table_count || 0).toLocaleString("ko-KR");
|
||||
const firstTables = Array.isArray(payload.candidate_tables)
|
||||
? payload.candidate_tables.slice(0, 3).map((row) => `${row.database}.${row.table}`).join(", ")
|
||||
: "";
|
||||
const preparedTables = Array.isArray(payload.prepared_local_tables)
|
||||
? payload.prepared_local_tables.length
|
||||
: 0;
|
||||
setStatus(
|
||||
`${payload.message} DB ${dbCount}개 · 후보 테이블 ${tableCount}개 · 로컬 이력 테이블 ${preparedTables}개 준비${firstTables ? ` · 예: ${firstTables}` : ""}`,
|
||||
payload.direct_db_access || payload.web_access ? "success" : "error",
|
||||
);
|
||||
if (payload.transport_warning) {
|
||||
console.warn(payload.transport_warning);
|
||||
}
|
||||
if (payload.candidate_tables?.length) {
|
||||
console.table(payload.candidate_tables.slice(0, 20));
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(error.message || "Satis 예산 후보 탐색 중 오류가 발생했습니다.", "error");
|
||||
} finally {
|
||||
discoverSatisBudgetButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
syncSatisBudgetButton.addEventListener("click", async () => {
|
||||
syncSatisBudgetButton.disabled = true;
|
||||
setStatus("Satis 후보 테이블에서 실제 예산 금액 행을 읽어 원본 저장소에 저장하고 있습니다...");
|
||||
try {
|
||||
saveErpCredentials();
|
||||
const payload = await fetchJson("/hanmac-browser/api/sync-satis-budget-raw", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...getErpPayload(),
|
||||
max_tables: 12,
|
||||
row_limit: 500,
|
||||
}),
|
||||
});
|
||||
const rowCount = Number(payload.inserted_or_updated_rows || 0).toLocaleString("ko-KR");
|
||||
const tableCount = Array.isArray(payload.table_results)
|
||||
? payload.table_results.filter((row) => row.status === "synced").length
|
||||
: 0;
|
||||
const firstTables = Array.isArray(payload.table_results)
|
||||
? payload.table_results
|
||||
.filter((row) => row.status === "synced" && Number(row.row_count || 0) > 0)
|
||||
.slice(0, 3)
|
||||
.map((row) => `${row.database}.${row.table} ${Number(row.row_count || 0).toLocaleString("ko-KR")}건`)
|
||||
.join(", ")
|
||||
: "";
|
||||
setStatus(
|
||||
`${payload.message} 저장 테이블: ${payload.raw_table || "satis_project_budget_raw_rows"} · 처리 테이블 ${tableCount.toLocaleString("ko-KR")}개${firstTables ? ` · 예: ${firstTables}` : ""}`,
|
||||
Number(payload.inserted_or_updated_rows || 0) > 0 ? "success" : "error",
|
||||
);
|
||||
if (payload.table_results?.length) {
|
||||
console.table(payload.table_results);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(error.message || "Satis 예산 금액 저장 중 오류가 발생했습니다.", "error");
|
||||
} finally {
|
||||
syncSatisBudgetButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
normalizeSatisBudgetButton.addEventListener("click", async () => {
|
||||
normalizeSatisBudgetButton.disabled = true;
|
||||
setStatus("저장된 Satis 원본 금액을 차수/상세 예산 테이블로 정규화하고 있습니다...");
|
||||
try {
|
||||
const payload = await fetchJson("/hanmac-browser/api/normalize-satis-budget", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const revisionCount = Number(payload.normalized_revisions || 0).toLocaleString("ko-KR");
|
||||
const taskLineCount = Number(payload.task_lines || 0).toLocaleString("ko-KR");
|
||||
const execLineCount = Number(payload.exec_lines || 0).toLocaleString("ko-KR");
|
||||
const firstSummary = Array.isArray(payload.summary)
|
||||
? payload.summary
|
||||
.slice(0, 3)
|
||||
.map((row) => `${row.budget_type}: ${Number(row.revision_count || 0).toLocaleString("ko-KR")}차수`)
|
||||
.join(", ")
|
||||
: "";
|
||||
setStatus(
|
||||
`${payload.message} 차수 ${revisionCount}건 · 과업수행계획 ${taskLineCount}행 · 실행예산 ${execLineCount}행${firstSummary ? ` · ${firstSummary}` : ""}`,
|
||||
Number(payload.normalized_revisions || 0) > 0 ? "success" : "error",
|
||||
);
|
||||
if (payload.summary?.length) {
|
||||
console.table(payload.summary);
|
||||
}
|
||||
if (payload.source_tables?.length) {
|
||||
console.table(payload.source_tables);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(error.message || "Satis 예산 정규화 중 오류가 발생했습니다.", "error");
|
||||
} finally {
|
||||
normalizeSatisBudgetButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
projectSatisBudgetButton.addEventListener("click", async () => {
|
||||
projectSatisBudgetButton.disabled = true;
|
||||
setStatus("프로젝트별 승인 최신 차수를 기존 예산 입력 테이블에 반영하고 있습니다. 승인 중 최신 차수는 가반영합니다...");
|
||||
try {
|
||||
const payload = await fetchJson("/hanmac-browser/api/project-satis-budget-current", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ include_pending: true }),
|
||||
});
|
||||
const projectCount = Number(payload.project_count || 0).toLocaleString("ko-KR");
|
||||
const taskRows = Number(payload.task_rows_inserted || 0).toLocaleString("ko-KR");
|
||||
const execRows = Number(payload.exec_rows_inserted || 0).toLocaleString("ko-KR");
|
||||
const provisionalCount = Number(payload.provisional_count || 0).toLocaleString("ko-KR");
|
||||
const firstProjected = Array.isArray(payload.projected)
|
||||
? payload.projected
|
||||
.slice(0, 3)
|
||||
.map((row) => `${row.support_dept_code}/${row.budget_type}/${row.projection_mode}`)
|
||||
.join(", ")
|
||||
: "";
|
||||
setStatus(
|
||||
`${payload.message} 과업수행계획 ${taskRows}행 · 실행예산 ${execRows}행 · 가반영 ${provisionalCount}건${firstProjected ? ` · 예: ${firstProjected}` : ""}`,
|
||||
Number(payload.project_count || 0) > 0 ? "success" : "error",
|
||||
);
|
||||
if (payload.projected?.length) {
|
||||
console.table(payload.projected);
|
||||
}
|
||||
if (payload.skipped?.length) {
|
||||
console.table(payload.skipped);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(error.message || "Satis 예산 기존 입력 테이블 반영 중 오류가 발생했습니다.", "error");
|
||||
} finally {
|
||||
projectSatisBudgetButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
runSatisBudgetFullSyncButton.addEventListener("click", async () => {
|
||||
runSatisBudgetFullSyncButton.disabled = true;
|
||||
setStatus("Satis 예산 전체 실행 중입니다. 원본 저장 → 정규화 → 승인 최신 차수 반영을 순차 처리합니다...");
|
||||
try {
|
||||
saveErpCredentials();
|
||||
const payload = await fetchJson("/hanmac-browser/api/run-satis-budget-full-sync", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...getErpPayload(),
|
||||
max_tables: 12,
|
||||
row_limit: 500,
|
||||
}),
|
||||
});
|
||||
const rawRows = Number(payload.inserted_or_updated_rows || 0).toLocaleString("ko-KR");
|
||||
const revisions = Number(payload.normalized_revisions || 0).toLocaleString("ko-KR");
|
||||
const projects = Number(payload.project_count || 0).toLocaleString("ko-KR");
|
||||
const taskRows = Number(payload.task_rows_inserted || 0).toLocaleString("ko-KR");
|
||||
const execRows = Number(payload.exec_rows_inserted || 0).toLocaleString("ko-KR");
|
||||
const provisional = Number(payload.provisional_count || 0).toLocaleString("ko-KR");
|
||||
setStatus(
|
||||
`${payload.message} 원본 ${rawRows}건 · 차수 ${revisions}건 · 프로젝트 ${projects}개 · 과업 ${taskRows}행 · 실행 ${execRows}행 · 가반영 ${provisional}건`,
|
||||
Number(payload.project_count || 0) > 0 ? "success" : "error",
|
||||
);
|
||||
console.log("Satis full sync", payload);
|
||||
} catch (error) {
|
||||
setStatus(error.message || "Satis 예산 전체 실행 중 오류가 발생했습니다.", "error");
|
||||
} finally {
|
||||
runSatisBudgetFullSyncButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
collectSatisBudgetWebButton.addEventListener("click", async () => {
|
||||
collectSatisBudgetWebButton.disabled = true;
|
||||
setStatus("Satis 웹로그인 세션으로 예산 관련 메뉴/컨트롤러 응답을 수집하고 있습니다...");
|
||||
try {
|
||||
saveErpCredentials();
|
||||
const payload = await fetchJson("/hanmac-browser/api/collect-satis-budget-web", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...getErpPayload(),
|
||||
max_pages: 40,
|
||||
}),
|
||||
});
|
||||
const capturedCount = Number(payload.captured_count || 0).toLocaleString("ko-KR");
|
||||
const matchedCount = Number(payload.matched_count || 0).toLocaleString("ko-KR");
|
||||
const errorCount = Number(payload.error_count || 0).toLocaleString("ko-KR");
|
||||
const firstExamples = Array.isArray(payload.matched_examples)
|
||||
? payload.matched_examples
|
||||
.slice(0, 2)
|
||||
.map((row) => `${row.title || row.url || "응답"} (${Number(row.amount_candidate_count || 0).toLocaleString("ko-KR")}개 금액후보)`)
|
||||
.join(", ")
|
||||
: "";
|
||||
setStatus(
|
||||
`${payload.message} 매칭 ${matchedCount}건 · 오류 ${errorCount}건${firstExamples ? ` · 예: ${firstExamples}` : ""}`,
|
||||
Number(payload.captured_count || 0) > 0 ? "success" : "error",
|
||||
);
|
||||
console.log("Satis web collection", payload);
|
||||
if (payload.matched_examples?.length) {
|
||||
console.table(payload.matched_examples);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(error.message || "Satis 웹로그인 예산 수집 중 오류가 발생했습니다.", "error");
|
||||
} finally {
|
||||
collectSatisBudgetWebButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
loadTablesButton.addEventListener("click", loadTables);
|
||||
loadGradeCodesButton.addEventListener("click", async () => {
|
||||
loadGradeCodesButton.disabled = true;
|
||||
@@ -3496,6 +4141,8 @@
|
||||
const today = new Date();
|
||||
const startOfYear = new Date(today.getFullYear(), 0, 1);
|
||||
restoreCredentials();
|
||||
restoreErpCredentials();
|
||||
selectConnectionTab("work");
|
||||
aggregateStartDate.value = startOfYear.toISOString().slice(0, 10);
|
||||
aggregateEndDate.value = today.toISOString().slice(0, 10);
|
||||
const restoredAggregateState = restoreAggregateState();
|
||||
|
||||
+253
-25
@@ -466,6 +466,17 @@
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.status-unit {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
color: #64748b;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status-card[data-count-pending="true"] .status-value {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
@@ -653,18 +664,20 @@
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid rgba(217, 221, 227, 0.9);
|
||||
background: transparent;
|
||||
max-height: 460px;
|
||||
background: #fff;
|
||||
max-height: none;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.detail-panel .table-wrap {
|
||||
max-height: 360px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: clamp(440px, 68dvh, 980px);
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.table-footer {
|
||||
@@ -703,8 +716,10 @@
|
||||
.table-wrap th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
z-index: 10;
|
||||
background: #f8fafc;
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 1px 0 rgba(148, 163, 184, 0.55), 0 3px 8px rgba(15, 23, 42, 0.1);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
@@ -784,14 +799,46 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.voucher-review-notice td {
|
||||
background: #fff7ed;
|
||||
color: #9a3412;
|
||||
font-size: 12px;
|
||||
border-bottom-color: rgba(251, 146, 60, 0.28);
|
||||
}
|
||||
|
||||
.voucher-review-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.voucher-review-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: #fed7aa;
|
||||
color: #7c2d12;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.voucher-review-note {
|
||||
color: #9a3412;
|
||||
}
|
||||
|
||||
.voucher-group-lines {
|
||||
overflow: visible;
|
||||
max-height: none;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.voucher-group-lines table {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-width: 1540px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
@@ -830,8 +877,10 @@
|
||||
.voucher-group-lines th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
z-index: 10;
|
||||
background: #f8fafc;
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 1px 0 rgba(148, 163, 184, 0.55), 0 3px 8px rgba(15, 23, 42, 0.1);
|
||||
color: inherit;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.01em;
|
||||
@@ -1513,11 +1562,14 @@
|
||||
}
|
||||
|
||||
body[data-view-mode="dual"] #pairRecommendPanel.active .table-wrap {
|
||||
max-height: calc(100vh - 230px);
|
||||
height: calc(100dvh - 230px);
|
||||
min-height: 360px;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
body[data-view-mode="dual"] .detail-panel .table-wrap {
|
||||
max-height: clamp(300px, 44vh, 520px);
|
||||
height: clamp(400px, 62dvh, 840px);
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
body[data-view-mode="dual"] .cell-code {
|
||||
@@ -1549,6 +1601,10 @@
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.detail-panel .table-wrap {
|
||||
height: clamp(360px, 62dvh, 720px);
|
||||
}
|
||||
|
||||
.voucher-topbar,
|
||||
.panel-header,
|
||||
.data-panel summary {
|
||||
@@ -1638,6 +1694,15 @@
|
||||
<div><h2>기간 {{ wehago_compare.selected_start_year or '-' }} ~ {{ wehago_compare.selected_end_year or '-' }}</h2></div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% set final_summary = wehago_compare.wehago_final_status_summary or {} %}
|
||||
{% set final_diag = final_summary.diagnostics or {} %}
|
||||
<div class="panel-meta" id="wehagoFinalBasisMeta">
|
||||
WEHAGO 전표 단위 기준:
|
||||
{{ "{:,}".format(final_diag.wehago_status_total or final_summary.classified_total or 0) }}건 /
|
||||
원천 {{ "{:,}".format(final_diag.wehago_voucher_total or final_summary.raw_total or 0) }}건 /
|
||||
차이 {{ "{:,}".format(final_diag.wehago_status_difference or final_summary.difference or 0) }}건 /
|
||||
{{ final_summary.source or 'projection' }}
|
||||
</div>
|
||||
<div class="status-grid">
|
||||
{% for section in wehago_compare.metric_sections %}
|
||||
{% if section.key in ['voucher_matched', 'voucher_unmatched', 'voucher_recheck', 'voucher_excepted', 'hanmac_unconnected', 'erp_voucher_matched', 'erp_voucher_unmatched'] %}
|
||||
@@ -1646,6 +1711,9 @@
|
||||
class="status-card"
|
||||
data-target="detail-{{ section.key }}"
|
||||
data-status="{{ section.key }}"
|
||||
data-count="{{ section.count or 0 }}"
|
||||
data-count-unit="{{ section.count_unit|default('전표그룹') }}"
|
||||
data-projection-signature="{{ section.projection_signature|default('') }}"
|
||||
data-count-pending="{% if wehago_compare.pending and section.count == 0 %}true{% else %}false{% endif %}"
|
||||
aria-expanded="false"
|
||||
>
|
||||
@@ -1653,6 +1721,7 @@
|
||||
<span class="status-value">
|
||||
{% if wehago_compare.pending and section.count == 0 %}갱신 중{% else %}{{ "{:,}".format(section.count) }}{% endif %}
|
||||
</span>
|
||||
<span class="status-unit">{{ section.count_unit|default('전표그룹') }}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -1698,7 +1767,7 @@
|
||||
<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">
|
||||
<input type="text" name="voucher_no" placeholder="일자+전표번호" autocomplete="off">
|
||||
<div class="suggest-dropdown" data-voucher-suggest="{{ section.key }}"></div>
|
||||
</div>
|
||||
<div class="suggest-field">
|
||||
@@ -1723,7 +1792,7 @@
|
||||
{% 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">
|
||||
<input type="text" name="voucher_no" placeholder="일자+전표번호" autocomplete="off">
|
||||
<div class="suggest-dropdown" data-voucher-suggest="{{ section.key }}"></div>
|
||||
</div>
|
||||
<div class="suggest-field">
|
||||
@@ -2064,15 +2133,65 @@
|
||||
];
|
||||
};
|
||||
|
||||
const firstPresentValue = (...values) => {
|
||||
for (const value of values) {
|
||||
if (value === null || value === undefined) continue;
|
||||
if (typeof value === 'string' && value.trim() === '') continue;
|
||||
return value;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const normalizeVoucherDisplayRow = (row = {}, summary = {}) => {
|
||||
const source = row || {};
|
||||
const group = summary || {};
|
||||
return {
|
||||
...source,
|
||||
status_label: firstPresentValue(source.status_label, group.status_label),
|
||||
fiscal_year: firstPresentValue(source.fiscal_year, group.fiscal_year),
|
||||
ledger_date: firstPresentValue(source.ledger_date, group.ledger_date),
|
||||
proof_date: firstPresentValue(source.proof_date, group.proof_date),
|
||||
voucher_no: firstPresentValue(source.voucher_no, group.voucher_no),
|
||||
// 가전표번호는 확정전표번호로 보완하지 않는다. 번호 오염을 막기 위해 draft_no 계열만 허용한다.
|
||||
draft_no: firstPresentValue(source.draft_no, source.erp_draft_no, source.hanmac_draft_no, group.draft_no),
|
||||
ledger_account_name: firstPresentValue(source.ledger_account_name, source.wehago_account_name, group.ledger_accounts),
|
||||
ledger_vendor: firstPresentValue(source.ledger_vendor, source.wehago_vendor, group.ledger_vendors),
|
||||
ledger_debit: firstPresentValue(source.ledger_debit, source.wehago_debit, group.ledger_debit),
|
||||
ledger_credit: firstPresentValue(source.ledger_credit, source.wehago_credit, group.ledger_credit),
|
||||
ledger_desc: firstPresentValue(source.ledger_desc, source.wehago_desc, group.ledger_desc),
|
||||
voucher_account_name: firstPresentValue(
|
||||
source.voucher_account_name,
|
||||
source.erp_account_name,
|
||||
source.hanmac_account_name,
|
||||
group.voucher_account_name,
|
||||
group.voucher_accounts
|
||||
),
|
||||
voucher_vendor: firstPresentValue(
|
||||
source.voucher_vendor,
|
||||
source.erp_vendor,
|
||||
source.hanmac_vendor,
|
||||
group.voucher_vendor,
|
||||
group.voucher_vendors
|
||||
),
|
||||
voucher_debit: firstPresentValue(source.voucher_debit, source.erp_debit, source.hanmac_debit, group.voucher_debit),
|
||||
voucher_credit: firstPresentValue(source.voucher_credit, source.erp_credit, source.hanmac_credit, group.voucher_credit),
|
||||
voucher_desc: firstPresentValue(source.voucher_desc, source.erp_desc, source.hanmac_desc, group.voucher_desc),
|
||||
};
|
||||
};
|
||||
|
||||
const renderVoucherDetailTable = (group) => {
|
||||
if (!voucherDetailTableWrap) return;
|
||||
const rows = Array.isArray(group?.rows) ? group.rows : [];
|
||||
const summary = group?.summary || {};
|
||||
const rawRows = Array.isArray(group?.rows) ? group.rows : [];
|
||||
const rows = rawRows.length
|
||||
? rawRows.map((row) => normalizeVoucherDisplayRow(row, summary))
|
||||
: [normalizeVoucherDisplayRow(summary, summary)];
|
||||
const isHanmacUnconnected = String(group?.summary?.status_label || '').toLowerCase() === 'hanmac unconnected';
|
||||
const lineColumns = [
|
||||
['status_label', '구분'],
|
||||
...getVoucherLineColumns(isHanmacUnconnected ? 'hanmac_unconnected' : ''),
|
||||
];
|
||||
if (!rows.length) {
|
||||
if (!rawRows.length && !Object.keys(summary).length) {
|
||||
voucherDetailTableWrap.innerHTML = '<div class="table-placeholder">표시할 상세 행이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
@@ -2383,6 +2502,57 @@
|
||||
}
|
||||
return String(raw ?? '');
|
||||
};
|
||||
const getVoucherReviewBadges = (group) => {
|
||||
const summary = group?.summary || {};
|
||||
const reasonText = [
|
||||
summary.review_reason || '',
|
||||
...(Array.isArray(group?.rows) ? group.rows.map((row) => row?.review_reason || '') : []),
|
||||
].join(' / ');
|
||||
const badges = [];
|
||||
const add = (key, label, note = '') => {
|
||||
if (!reasonText.includes(key)) return;
|
||||
if (badges.some((item) => item.label === label)) return;
|
||||
badges.push({ label, note });
|
||||
};
|
||||
const hasStrongInternalCandidate = [
|
||||
'INTERNAL_STATUS=matched_direct_candidate',
|
||||
'INTERNAL_STATUS=matched_with_settlement_bridge_candidate',
|
||||
'INTERNAL_STATUS=matched_bundle_candidate',
|
||||
].some((key) => reasonText.includes(key));
|
||||
add('PARTIAL_WEAK_MONTH_MISMATCH', '월 불일치·약한 근거', '부가세/비용 직접행 없이 일부 정산행만 맞아 재검토가 필요합니다.');
|
||||
add('ERP_PARTIAL_VOUCHER_ROWS_ONLY', 'ERP 일부행 후보', '같은 ERP 전표의 일부 행만 연결된 후보입니다.');
|
||||
add('TAX_INVOICE_CORE_ROWS_NOT_MATCHED', '핵심 계정 누락', '부가세·비용·채권채무 구조가 완전하지 않습니다.');
|
||||
add('PROJECT_TOKEN_CONFLICT', '현장명 불일치', 'WEHAGO와 ERP의 현장/사업명 단서가 다릅니다.');
|
||||
add('MONTH_CONFLICT_RECHECK', '월 불일치', '전표월 또는 증빙월 확인이 필요합니다.');
|
||||
if (!hasStrongInternalCandidate) {
|
||||
add('WEAK_MATCH_RECHECK', '약한 매칭', '금액 외 단서가 부족합니다.');
|
||||
}
|
||||
add('DIRECT_MATCH_CANDIDATE', '직접 대응 후보', '금액·계정·거래 문맥이 함께 일치하는 ERP 원본행입니다.');
|
||||
add('SETTLEMENT_BRIDGE_CANDIDATE', '정산 브리지', '직접 업무행이 아니라 결제·정산 상대행으로 연결된 후보입니다.');
|
||||
add('SHARED_ALLOCATION_CANDIDATE', '공유 합산행', '여러 WEHAGO 전표의 배분 합계가 이 ERP 원본행을 구성합니다.');
|
||||
add('SHARED_BUNDLE_PAYMENT_CANDIDATE', 'N:1 결제 묶음', '여러 WEHAGO 전표가 하나의 ERP 결제행을 공유하는 묶음 후보입니다.');
|
||||
add('ERP_FULL_DRAFT_VOUCHER_CONTEXT', 'ERP 전체 전표', '후보 가전표의 원본행 전체를 표시합니다.');
|
||||
return badges;
|
||||
};
|
||||
const renderVoucherReviewNoticeRow = (group, colSpan) => {
|
||||
if (statusKey !== 'voucher_recheck') return '';
|
||||
const badges = getVoucherReviewBadges(group);
|
||||
if (!badges.length) return '';
|
||||
const badgeHtml = badges
|
||||
.map((item) => `<span class="voucher-review-badge" title="${escapeHtml(item.note || item.label)}">${escapeHtml(item.label)}</span>`)
|
||||
.join('');
|
||||
const note = badges.map((item) => item.note).find(Boolean) || '상세 버튼에서 연결 행을 확인해주세요.';
|
||||
return `
|
||||
<tr class="voucher-review-notice">
|
||||
<td colspan="${Number(colSpan || 1)}">
|
||||
<div class="voucher-review-badges">
|
||||
${badgeHtml}
|
||||
<span class="voucher-review-note">${escapeHtml(note)}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
};
|
||||
if (!groups.length) {
|
||||
const fallbackRows = Array.isArray(payload.rows) ? payload.rows : [];
|
||||
if (!fallbackRows.length && !append) {
|
||||
@@ -2393,7 +2563,8 @@
|
||||
includeReviewCheckbox ? '<th class="selection-col">매칭</th><th class="selection-col">분리</th>' : '',
|
||||
...lineColumns.map(([field, label]) => `<th class="${getColumnClass(field)}">${escapeHtml(label)}</th>`),
|
||||
].join('');
|
||||
const fallbackBody = fallbackRows.map((row, summaryIndex) => {
|
||||
const fallbackBody = fallbackRows.map((rawRow, summaryIndex) => {
|
||||
const row = normalizeVoucherDisplayRow(rawRow, rawRow);
|
||||
const groupKey = `${row.fiscal_year || ''}|${row.voucher_no || ''}|${row.draft_no || ''}|${row.ledger_date || ''}|${row.proof_date || ''}|${summaryIndex}`;
|
||||
const cells = lineColumns.map(([field]) => {
|
||||
const display = formatValue(field, row[field]);
|
||||
@@ -2431,9 +2602,13 @@
|
||||
...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 : [];
|
||||
const summary = group?.summary || {};
|
||||
const rawRows = Array.isArray(group.rows) ? group.rows : [];
|
||||
const rows = rawRows.length
|
||||
? rawRows.map((row) => normalizeVoucherDisplayRow(row, summary))
|
||||
: [normalizeVoucherDisplayRow(summary, summary)];
|
||||
const groupKey = buildVoucherRecheckGroupKey(group, groupIndex);
|
||||
return rows.map((row, rowIndex) => {
|
||||
const groupRowsHtml = rows.map((row, rowIndex) => {
|
||||
const rowClasses = [];
|
||||
if (rowIndex === 0) {
|
||||
rowClasses.push('voucher-table-group-start');
|
||||
@@ -2455,6 +2630,7 @@
|
||||
}
|
||||
return `<tr class="${rowClasses.join(' ')}">${checkboxCell}${cells}</tr>`;
|
||||
}).join('');
|
||||
return `${renderVoucherReviewNoticeRow(group, lineColumns.length + (includeReviewCheckbox ? 2 : 0))}${groupRowsHtml}`;
|
||||
}).join('');
|
||||
const tableHtml = `
|
||||
<table>
|
||||
@@ -2575,9 +2751,10 @@
|
||||
const setMeta = (key, totalCount, shownCount, notice = '', stats = {}) => {
|
||||
const target = document.querySelector(`[data-result-meta="${key}"]`);
|
||||
if (target) {
|
||||
const countText = `조회 결과 ${Number(totalCount || 0).toLocaleString()}건`;
|
||||
const unit = String(stats.count_unit || document.querySelector(`.status-card[data-status="${key}"]`)?.dataset.countUnit || '건');
|
||||
const countText = `조회 결과 ${Number(totalCount || 0).toLocaleString()}${unit}`;
|
||||
const shownText = shownCount
|
||||
? ` / 현재 ${Number(shownCount || 0).toLocaleString()}건`
|
||||
? ` / 현재 ${Number(shownCount || 0).toLocaleString()}${unit}`
|
||||
: '';
|
||||
const readyYears = Array.isArray(stats.ready_years) ? stats.ready_years : [];
|
||||
const pendingYears = Array.isArray(stats.pending_years) ? stats.pending_years : [];
|
||||
@@ -2616,7 +2793,21 @@
|
||||
},
|
||||
signal: options.signal || controller?.signal,
|
||||
});
|
||||
const payload = await response.json();
|
||||
const responseText = await response.text();
|
||||
let payload = {};
|
||||
if (responseText) {
|
||||
try {
|
||||
payload = JSON.parse(responseText);
|
||||
} catch (_error) {
|
||||
const fallbackMessage = responseText.length > 240
|
||||
? `${responseText.slice(0, 240)}...`
|
||||
: responseText;
|
||||
if (!response.ok) {
|
||||
throw new Error(fallbackMessage || `HTTP ${response.status}`);
|
||||
}
|
||||
throw new Error('서버 응답을 JSON으로 해석할 수 없습니다.');
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || '조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -2755,6 +2946,16 @@
|
||||
};
|
||||
|
||||
const applySummaryPayload = (payload) => {
|
||||
const finalBasisMeta = document.getElementById('wehagoFinalBasisMeta');
|
||||
if (finalBasisMeta) {
|
||||
const summary = payload?.wehago_final_status_summary || {};
|
||||
const diagnostics = summary.diagnostics || {};
|
||||
const statusTotal = Number(diagnostics.wehago_status_total ?? summary.classified_total ?? 0);
|
||||
const rawTotal = Number(diagnostics.wehago_voucher_total ?? summary.raw_total ?? 0);
|
||||
const difference = Number(diagnostics.wehago_status_difference ?? summary.difference ?? 0);
|
||||
const source = String(summary.source || 'projection');
|
||||
finalBasisMeta.textContent = `WEHAGO 전표 단위 기준: ${statusTotal.toLocaleString()}건 / 원천 ${rawTotal.toLocaleString()}건 / 차이 ${difference.toLocaleString()}건 / ${source}`;
|
||||
}
|
||||
const sections = Array.isArray(payload?.metric_sections) ? payload.metric_sections : [];
|
||||
sections.forEach((section) => {
|
||||
const card = document.querySelector(`.status-card[data-status="${escapeSelectorValue(section.key || '')}"]`);
|
||||
@@ -2763,6 +2964,13 @@
|
||||
const isPendingCount = Boolean(payload?.pending) && countValue === 0;
|
||||
if (card) {
|
||||
card.dataset.countPending = isPendingCount ? 'true' : 'false';
|
||||
card.dataset.count = String(countValue || 0);
|
||||
card.dataset.countUnit = String(section.count_unit || card.dataset.countUnit || '전표');
|
||||
card.dataset.projectionSignature = String(section.projection_signature || '');
|
||||
const unitNode = card.querySelector('.status-unit');
|
||||
if (unitNode) {
|
||||
unitNode.textContent = card.dataset.countUnit;
|
||||
}
|
||||
}
|
||||
if (valueNode) {
|
||||
valueNode.textContent = isPendingCount ? '갱신 중' : countValue.toLocaleString();
|
||||
@@ -2779,7 +2987,8 @@
|
||||
if (undoLastActionBtn) {
|
||||
undoLastActionBtn.disabled = !(lastAction && lastAction.id);
|
||||
}
|
||||
const pending = Boolean(payload?.pending);
|
||||
const jobActive = ['queued', 'running'].includes(String(payload?.latest_job?.status || payload?.system_job?.status || ''));
|
||||
const pending = Boolean(payload?.pending) && jobActive;
|
||||
summaryRefreshState.pending = pending;
|
||||
if (summaryRefreshState.timerId) {
|
||||
clearTimeout(summaryRefreshState.timerId);
|
||||
@@ -3843,6 +4052,11 @@
|
||||
clearTimeout(detailRetryTimers.get(statusKey));
|
||||
detailRetryTimers.delete(statusKey);
|
||||
}
|
||||
if (!append) {
|
||||
detailRequestControllers.forEach((controller, key) => {
|
||||
if (key !== statusKey) controller.abort();
|
||||
});
|
||||
}
|
||||
const current = detailState.get(statusKey) || { offset: 0, totalCount: 0, loading: false };
|
||||
if (current.loading) {
|
||||
const activeController = detailRequestControllers.get(statusKey);
|
||||
@@ -3863,7 +4077,12 @@
|
||||
setMeta(statusKey, current.totalCount || 0, 0, '조회 중입니다...');
|
||||
}
|
||||
const requestController = new AbortController();
|
||||
const requestTimeoutId = window.setTimeout(() => requestController.abort(), 20000);
|
||||
let requestAbortReason = '';
|
||||
const detailRequestTimeoutMs = 30000;
|
||||
const requestTimeoutId = window.setTimeout(() => {
|
||||
requestAbortReason = 'timeout';
|
||||
requestController.abort();
|
||||
}, detailRequestTimeoutMs);
|
||||
detailRequestControllers.set(statusKey, requestController);
|
||||
try {
|
||||
const pageLimit = usesCursorPaging ? 24 : 60;
|
||||
@@ -3875,6 +4094,12 @@
|
||||
{ signal: requestController.signal, timeoutMs: 20000 },
|
||||
);
|
||||
if (detailRequestControllers.get(statusKey) !== requestController) return;
|
||||
const card = document.querySelector(`.status-card[data-status="${statusKey}"]`);
|
||||
const cardSignature = String(card?.dataset.projectionSignature || '').trim();
|
||||
const payloadSignature = String(payload?.projection_signature || '').trim();
|
||||
if (!append && cardSignature && payloadSignature && cardSignature !== payloadSignature) {
|
||||
card.dataset.projectionSignature = payloadSignature;
|
||||
}
|
||||
renderTable(wrap, payload, ['voucher_no'], append, statusKey);
|
||||
const shownCount = append ? nextOffset + payload.shown_count : payload.shown_count;
|
||||
detailState.set(statusKey, {
|
||||
@@ -3900,8 +4125,11 @@
|
||||
if (error.name === 'AbortError') {
|
||||
if (detailRequestControllers.get(statusKey) === requestController) {
|
||||
if (!append) {
|
||||
wrap.innerHTML = '<div class="table-placeholder">조회 시간이 길어져 중단했습니다. 조건을 줄여 다시 조회해주세요.</div>';
|
||||
setMeta(statusKey, current.totalCount || 0, 0, '조회 시간이 길어져 중단했습니다.');
|
||||
const message = requestAbortReason === 'timeout'
|
||||
? '상세 조회 캐시를 준비하는 데 시간이 걸리고 있습니다. 조회 캐시 재생성 또는 잠시 후 다시 확인해주세요.'
|
||||
: '이전 조회가 취소되었습니다.';
|
||||
wrap.innerHTML = `<div class="table-placeholder">${escapeHtml(message)}</div>`;
|
||||
setMeta(statusKey, current.totalCount || 0, 0, message);
|
||||
}
|
||||
detailState.set(statusKey, { ...current, loading: false, form });
|
||||
setLoadMoreState(statusKey, current.hasMore, false);
|
||||
|
||||
Reference in New Issue
Block a user