Update intranet tools and voucher comparison

This commit is contained in:
b17301
2026-06-04 09:00:51 +09:00
parent 2ab74bac88
commit b93289bfa8
55 changed files with 32809 additions and 1063 deletions
+70 -17
View File
@@ -520,7 +520,7 @@
.sync-status {
margin-left: auto;
max-width: 240px;
max-width: 320px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(248, 250, 252, 0.92);
@@ -645,6 +645,31 @@
font-weight: 700;
}
.sync-refresh-button {
display: none;
height: calc(var(--status-widget-height) - 10px);
border: 1px solid #cbd5e1;
border-radius: 999px;
background: #ffffff;
color: #111827;
font-size: 11px;
font-weight: 800;
padding: 0 10px;
cursor: pointer;
white-space: nowrap;
}
.sync-status.has-pending {
border-color: #f59e0b;
background: #fff7ed;
}
.sync-status.has-pending .sync-refresh-button {
display: inline-flex;
align-items: center;
justify-content: center;
}
@media (max-width: 1200px) {
.stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -682,14 +707,15 @@
<body>
<div class="page">
<nav class="nav">
<a href="/" class="{% if request.url.path == '/' %}active{% endif %}">대시보드</a>
<a href="/annual-summary" class="{% if request.url.path == '/annual-summary' %}active{% endif %}">연도별 수익/비용</a>
<a href="/process-cost" class="{% if request.url.path == '/process-cost' %}active{% endif %}">프로젝트 원가</a>
<a href="/projects" class="{% if request.url.path == '/projects' %}active{% endif %}">프로젝트 정보</a>
<a href="/wehago-compare" class="{% if request.url.path == '/wehago-compare' %}active{% endif %}">전표비교</a>
<a href="/hanmac-browser" class="{% if request.url.path == '/hanmac-browser' %}active{% endif %}">hanmac DB_external</a>
<a href="/db-browser" class="{% if request.url.path.startswith('/db') %}active{% endif %}">DB 조회</a>
{% for item in nav_items %}
{% set is_active = request.url.path == item.href or (item.active == 'db' and request.url.path.startswith('/db')) or (item.active == 'admin' and request.url.path.startswith('/admin')) %}
<a href="{{ item.href }}" class="{% if is_active %}active{% endif %}">{{ item.label }}</a>
{% endfor %}
<div class="nav-spacer"></div>
{% if current_user %}
<span class="sync-pill">{{ current_user.display_name }}</span>
<a href="/logout">로그아웃</a>
{% endif %}
<button type="button" class="view-mode-switch" id="viewModeSwitch" data-mode="dual" aria-label="화면 구성 전환">
<span class="label" id="viewModeSwitchLabel">듀얼</span>
</button>
@@ -698,19 +724,20 @@
id="syncStatusWidget"
data-data-version="{{ data_version or '' }}"
data-refresh-url="{{ request.url.path }}{% if request.url.query %}?{{ request.url.query }}{% endif %}"
data-refresh-mode="{{ 'disabled' if request.url.path in ['/projects', '/wehago-compare'] else 'auto' }}"
data-refresh-mode="manual"
>
<div class="sync-status-head">
<div class="sync-status-title">
<span class="sync-dot" id="syncStatusDot"></span>
<span id="syncStatusLabel">연결 확인 중</span>
</div>
<button type="button" class="sync-refresh-button" id="syncRefreshButton">새로고침</button>
<span class="sync-pill" id="syncSessionPill">세션 준비 중</span>
</div>
<div class="sync-meta">
<div>마지막 확인: <strong id="syncLastChecked">-</strong></div>
<div>서버 시간: <strong id="syncServerTime">{{ server_time or '-' }}</strong></div>
<div>데이터 버전: <strong id="syncDataVersion">{{ data_version or '-' }}</strong></div>
<div>업무 데이터 버전: <strong id="syncDataVersion">{{ data_version or '-' }}</strong></div>
<div>계약/청구 동기화: <strong>{{ import_sync_summary.contract_project_count or 0 }}</strong> / <strong>{{ import_sync_summary.billing_project_count or 0 }}</strong></div>
</div>
</aside>
@@ -774,10 +801,11 @@
const dot = document.getElementById("syncStatusDot");
const label = document.getElementById("syncStatusLabel");
const sessionPill = document.getElementById("syncSessionPill");
const refreshButton = document.getElementById("syncRefreshButton");
const lastChecked = document.getElementById("syncLastChecked");
const serverTime = document.getElementById("syncServerTime");
const dataVersion = document.getElementById("syncDataVersion");
if (!dot || !label || !sessionPill || !lastChecked || !serverTime || !dataVersion) return;
if (!dot || !label || !sessionPill || !refreshButton || !lastChecked || !serverTime || !dataVersion) return;
let pageVersion = widget.dataset.dataVersion || "";
const refreshUrl = widget.dataset.refreshUrl || window.location.href;
const refreshMode = widget.dataset.refreshMode || "auto";
@@ -856,6 +884,7 @@
pageVersion = nextVersion || "";
pendingVersion = "";
widget.dataset.dataVersion = pageVersion;
widget.classList.remove("has-pending");
dataVersion.textContent = pageVersion || "-";
updateWidgetTitle();
}
@@ -877,7 +906,7 @@
async function refreshPageWhenSafe(nextVersion) {
if (refreshInFlight) return;
if (refreshMode === "disabled") {
if (refreshMode === "disabled" || refreshMode === "manual") {
setPageDataVersion(nextVersion || pageVersion);
setStatus("online", "서버 정상 연결");
return;
@@ -900,6 +929,20 @@
}
}
function markPendingBusinessData(nextVersion) {
pendingVersion = nextVersion || pendingVersion || pageVersion;
widget.classList.add("has-pending");
dataVersion.textContent = pendingVersion || "-";
setStatus("online", "새 업무 데이터 있음");
}
refreshButton.addEventListener("click", () => {
if (refreshInFlight) return;
refreshInFlight = true;
setStatus("online", "새 데이터 반영 중");
window.location.replace(refreshUrl);
});
async function pollHealth() {
if (document.hidden) {
return;
@@ -908,18 +951,28 @@
const response = await fetch(`/health?ts=${Date.now()}`, { cache: "no-store" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json();
const businessDataVersion = payload.business_data_version || payload.data_version || "";
serverTime.textContent = payload.server_time || "-";
dataVersion.textContent = payload.data_version || "-";
dataVersion.textContent = businessDataVersion || "-";
updateWidgetTitle();
if (payload.data_version && payload.data_version !== pageVersion) {
pendingVersion = payload.data_version;
await refreshPageWhenSafe(payload.data_version);
if (businessDataVersion && businessDataVersion !== pageVersion) {
if (refreshMode === "auto") {
pendingVersion = businessDataVersion;
await refreshPageWhenSafe(businessDataVersion);
} else {
markPendingBusinessData(businessDataVersion);
}
return;
}
if (pendingVersion && pendingVersion !== pageVersion) {
await refreshPageWhenSafe(pendingVersion);
if (refreshMode === "auto") {
await refreshPageWhenSafe(pendingVersion);
} else {
markPendingBusinessData(pendingVersion);
}
return;
}
widget.classList.remove("has-pending");
setStatus("online", "서버 정상 연결");
} catch (error) {
setStatus("error", "연결 오류");