feat(erp): ERP 대시보드 + 관리자 권한 시스템 도입
- 로그인: dbxcorp.co.kr 도메인 화이트리스트만 사용. ALLOWED_EMAILS 게이트 제거 - king@dbxcorp.co.kr 슈퍼관리자 자동 부여 (강등/삭제 불가) - 신규 로그인 사용자는 JSON 저장소에 자동 등록 (기본 모듈 권한 0) - 관리자 페이지(/admin): 역할/모듈별 토글로 직원 권한 수정 - 메인 대시보드를 ERP 시스템(monochromatic shadcn)으로 재설계 · 모듈: CORM, Order, 개인경비(준비중), 휴가(준비중) · 권한 없는 모듈은 '권한 없음' 배지 + 클릭 불가 - DESIGN.md 토큰 기반 styles → app/static/erp.css - docker-compose: dbx-main-data 볼륨 마운트 (DATA_DIR=/data) 으로 권한 JSON 영구 보관 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>사용자/권한 관리 — DBX ERP</title>
|
||||
<link rel="stylesheet" href="/static/erp.css" />
|
||||
</head>
|
||||
<body class="erp-body">
|
||||
|
||||
<div class="erp-shell">
|
||||
|
||||
<!-- ── 상단 네비게이션 ── -->
|
||||
<nav class="erp-nav">
|
||||
<div class="erp-nav-left">
|
||||
<a href="/" class="erp-brand">
|
||||
<img src="/static/dbx-logo.png" alt="DBX" class="erp-brand-logo" />
|
||||
<span class="erp-brand-divider"></span>
|
||||
<span class="erp-brand-system">ERP 시스템</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="erp-nav-right">
|
||||
<a class="erp-btn erp-btn-ghost" href="/">← 대시보드</a>
|
||||
<div class="erp-user-chip">
|
||||
{% if user.picture %}
|
||||
<img class="erp-user-avatar" src="{{ user.picture }}" alt="{{ user.name }}" />
|
||||
{% else %}
|
||||
<span class="erp-user-avatar-fallback">{{ user.name[0] | upper }}</span>
|
||||
{% endif %}
|
||||
<div class="erp-user-info">
|
||||
<span class="erp-user-name">{{ user.name }}</span>
|
||||
<span class="erp-user-email">{{ user.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="erp-btn erp-btn-outline" href="/logout">로그아웃</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="erp-main" style="max-width: 1100px;">
|
||||
|
||||
<h1 class="erp-greeting">사용자 / 권한 관리</h1>
|
||||
<p class="erp-greeting-sub">
|
||||
회사 계정으로 로그인한 임직원의 모듈 접근 권한을 관리합니다.
|
||||
토글을 변경한 뒤 <strong>저장</strong> 버튼을 누르세요.
|
||||
</p>
|
||||
|
||||
<div class="erp-admin-toolbar">
|
||||
<input type="search"
|
||||
id="user-search"
|
||||
class="erp-input"
|
||||
placeholder="이메일 또는 이름으로 검색…" />
|
||||
<span class="erp-section-meta" id="user-count">총 {{ users | length }}명</span>
|
||||
</div>
|
||||
|
||||
<div class="erp-table-wrap">
|
||||
<table class="erp-table" id="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 28%;">사용자</th>
|
||||
<th style="width: 11%;">역할</th>
|
||||
{% for key in module_keys %}
|
||||
<th style="text-align:center; width: 9%;">
|
||||
{% if key == 'corm' %}CORM
|
||||
{% elif key == 'order' %}Order
|
||||
{% elif key == 'expense' %}개인경비
|
||||
{% elif key == 'vacation' %}휴가
|
||||
{% else %}{{ key }}{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th style="width: 13%;">최근 로그인</th>
|
||||
<th style="width: 12%; text-align: right;">동작</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr data-email="{{ u.email }}" data-dirty="false"
|
||||
data-super="{{ 'true' if u.is_super_admin else 'false' }}">
|
||||
<td>
|
||||
<div class="erp-user-cell">
|
||||
{% if u.picture %}
|
||||
<img class="erp-user-avatar" src="{{ u.picture }}" alt="{{ u.name }}" />
|
||||
{% else %}
|
||||
<span class="erp-user-avatar-fallback">{{ (u.name or u.email)[0] | upper }}</span>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="erp-user-cell-name">
|
||||
{{ u.name or u.email }}
|
||||
{% if u.is_super_admin %}
|
||||
<span class="erp-badge erp-badge-inverse" style="margin-left:6px;">슈퍼관리자</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="erp-user-cell-email">{{ u.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="erp-select" data-field="role"
|
||||
{% if u.is_super_admin %}disabled{% endif %}>
|
||||
<option value="user" {% if u.role == 'user' %}selected{% endif %}>user</option>
|
||||
<option value="admin" {% if u.role == 'admin' %}selected{% endif %}>admin</option>
|
||||
</select>
|
||||
</td>
|
||||
{% for key in module_keys %}
|
||||
<td style="text-align:center;">
|
||||
<label class="erp-switch">
|
||||
<input type="checkbox"
|
||||
data-field="module"
|
||||
data-module="{{ key }}"
|
||||
{% if u.modules[key] %}checked{% endif %}
|
||||
{% if u.is_super_admin or u.role == 'admin' %}disabled{% endif %} />
|
||||
<span class="erp-switch-slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td style="font-size:12px; color: var(--color-midtone-gray);">
|
||||
{{ (u.last_login or '')[:16].replace('T', ' ') }}
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<button class="erp-btn erp-btn-primary" data-action="save"
|
||||
{% if u.is_super_admin %}disabled{% endif %}>저장</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="erp-section-meta" style="margin-top: var(--sp-16);">
|
||||
※ <strong>{{ super_admin_email }}</strong> 은(는) 슈퍼 관리자이므로 권한을 변경할 수 없습니다.
|
||||
관리자(admin) 역할로 지정된 사용자에게는 모든 모듈 권한이 자동 부여됩니다.
|
||||
</p>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="erp-toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script>
|
||||
const tbody = document.querySelector("#users-table tbody");
|
||||
const searchInput = document.getElementById("user-search");
|
||||
const userCountEl = document.getElementById("user-count");
|
||||
const toastEl = document.getElementById("toast");
|
||||
|
||||
function markDirty(tr) { tr.setAttribute("data-dirty", "true"); }
|
||||
function clearDirty(tr) { tr.setAttribute("data-dirty", "false"); }
|
||||
|
||||
function showToast(msg, type) {
|
||||
toastEl.textContent = msg;
|
||||
toastEl.dataset.type = type || "info";
|
||||
toastEl.dataset.visible = "true";
|
||||
clearTimeout(showToast._t);
|
||||
showToast._t = setTimeout(() => {
|
||||
toastEl.dataset.visible = "false";
|
||||
}, 2400);
|
||||
}
|
||||
|
||||
// 역할 변경시 모듈 토글 활성/비활성 갱신
|
||||
function syncModuleSwitches(tr) {
|
||||
const isSuper = tr.dataset.super === "true";
|
||||
const role = tr.querySelector("select[data-field='role']").value;
|
||||
const adminRole = role === "admin";
|
||||
tr.querySelectorAll("input[data-field='module']").forEach((cb) => {
|
||||
if (isSuper || adminRole) {
|
||||
cb.checked = true;
|
||||
cb.disabled = true;
|
||||
} else {
|
||||
cb.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tbody.addEventListener("change", (e) => {
|
||||
const tr = e.target.closest("tr");
|
||||
if (!tr) return;
|
||||
if (e.target.dataset.field === "role") {
|
||||
syncModuleSwitches(tr);
|
||||
}
|
||||
markDirty(tr);
|
||||
});
|
||||
|
||||
tbody.addEventListener("click", async (e) => {
|
||||
const btn = e.target.closest("button[data-action='save']");
|
||||
if (!btn) return;
|
||||
const tr = btn.closest("tr");
|
||||
const email = tr.dataset.email;
|
||||
const role = tr.querySelector("select[data-field='role']").value;
|
||||
const modules = {};
|
||||
tr.querySelectorAll("input[data-field='module']").forEach((cb) => {
|
||||
modules[cb.dataset.module] = cb.checked;
|
||||
});
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = "저장 중…";
|
||||
try {
|
||||
const res = await fetch(`/api/users/${encodeURIComponent(email)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({ role, modules }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.detail || `HTTP ${res.status}`);
|
||||
}
|
||||
clearDirty(tr);
|
||||
showToast(`${email} 저장 완료`, "info");
|
||||
} catch (err) {
|
||||
showToast(`저장 실패: ${err.message}`, "error");
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "저장";
|
||||
}
|
||||
});
|
||||
|
||||
// 검색
|
||||
searchInput.addEventListener("input", () => {
|
||||
const q = searchInput.value.toLowerCase().trim();
|
||||
let visible = 0;
|
||||
tbody.querySelectorAll("tr").forEach((tr) => {
|
||||
const text = tr.textContent.toLowerCase();
|
||||
const match = !q || text.includes(q);
|
||||
tr.style.display = match ? "" : "none";
|
||||
if (match) visible++;
|
||||
});
|
||||
userCountEl.textContent = q
|
||||
? `검색 결과 ${visible}명`
|
||||
: `총 ${tbody.querySelectorAll("tr").length}명`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+139
-123
@@ -3,153 +3,169 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>업무 포털 — DBX Corporation</title>
|
||||
<link rel="stylesheet" href="/static/styles.css" />
|
||||
<title>ERP 시스템 — DBX Corporation</title>
|
||||
<link rel="stylesheet" href="/static/erp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<body class="erp-body">
|
||||
|
||||
<!-- ── 헤더 ── -->
|
||||
<header class="header">
|
||||
<div class="header-brand">
|
||||
<img src="/static/dbx-logo.png" alt="DBX Corporation" class="header-logo-img" />
|
||||
<span class="header-portal">업무 포털</span>
|
||||
</div>
|
||||
<div class="erp-shell">
|
||||
|
||||
<div class="header-user">
|
||||
<div class="header-user-info">
|
||||
<span class="header-user-name">{{ user.name }}</span>
|
||||
<span class="header-user-email">{{ user.email }}</span>
|
||||
<!-- ── 상단 네비게이션 ── -->
|
||||
<nav class="erp-nav">
|
||||
<div class="erp-nav-left">
|
||||
<a href="/" class="erp-brand">
|
||||
<img src="/static/dbx-logo.png" alt="DBX" class="erp-brand-logo" />
|
||||
<span class="erp-brand-divider"></span>
|
||||
<span class="erp-brand-system">ERP 시스템</span>
|
||||
</a>
|
||||
</div>
|
||||
{% if user.picture %}
|
||||
<img class="header-avatar" src="{{ user.picture }}" alt="{{ user.name }}" />
|
||||
{% else %}
|
||||
<div class="header-avatar-placeholder">
|
||||
{{ user.name[0] | upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<a class="btn-logout" href="/logout">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
<span>로그아웃</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── 히어로 배너 ── -->
|
||||
<section class="hero">
|
||||
<p class="hero-greeting">안녕하세요, {{ user.name }}님 👋</p>
|
||||
<p class="hero-sub">오늘도 좋은 하루 되세요.</p>
|
||||
<p class="hero-date" id="today-date"></p>
|
||||
</section>
|
||||
<div class="erp-nav-right">
|
||||
{% if is_admin %}
|
||||
<a class="erp-btn erp-btn-ghost" href="/admin">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2 4 6v6c0 5 3.4 9.4 8 10 4.6-.6 8-5 8-10V6l-8-4Z"/>
|
||||
</svg>
|
||||
관리자
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── 메뉴 콘텐츠 ── -->
|
||||
<main class="content">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">업무 바로가기</h2>
|
||||
<span class="section-count">{{ menu_items | length }}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-grid">
|
||||
{% for item in menu_items %}
|
||||
<a class="menu-card" href="{{ item.url }}" target="_blank" rel="noopener noreferrer"
|
||||
{% if item.health_url %}data-health-url="{{ item.health_url }}"{% endif %}>
|
||||
<div class="card-icon">
|
||||
{% if loop.index == 1 %}
|
||||
<!-- 클립보드 / 발주 아이콘 -->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
||||
<line x1="8" y1="13" x2="16" y2="13"/>
|
||||
<line x1="8" y1="17" x2="14" y2="17"/>
|
||||
</svg>
|
||||
{% elif loop.index == 2 %}
|
||||
<!-- 리스트 / 주문 아이콘 -->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="8" y1="6" x2="21" y2="6"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6"/>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||
</svg>
|
||||
{% elif loop.index == 3 %}
|
||||
<!-- 차트 아이콘 -->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="20" x2="18" y2="10"/>
|
||||
<line x1="12" y1="20" x2="12" y2="4"/>
|
||||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||||
<line x1="2" y1="20" x2="22" y2="20"/>
|
||||
</svg>
|
||||
<div class="erp-user-chip">
|
||||
{% if user.picture %}
|
||||
<img class="erp-user-avatar" src="{{ user.picture }}" alt="{{ user.name }}" />
|
||||
{% else %}
|
||||
<!-- 기본 링크 아이콘 -->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="16"/>
|
||||
<line x1="8" y1="12" x2="16" y2="12"/>
|
||||
</svg>
|
||||
<span class="erp-user-avatar-fallback">{{ user.name[0] | upper }}</span>
|
||||
{% endif %}
|
||||
<div class="erp-user-info">
|
||||
<span class="erp-user-name">{{ user.name }}</span>
|
||||
<span class="erp-user-email">{{ user.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<p class="card-title">
|
||||
{{ item.title }}
|
||||
{% if item.health_url %}
|
||||
<span class="status-badge" data-status="checking" aria-live="polite">확인 중</span>
|
||||
<a class="erp-btn erp-btn-outline" href="/logout">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
로그아웃
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- ── 본문 ── -->
|
||||
<main class="erp-main">
|
||||
|
||||
<h1 class="erp-greeting">안녕하세요, {{ user.name }}님</h1>
|
||||
<p class="erp-greeting-sub" id="erp-greeting-sub"></p>
|
||||
|
||||
<div class="erp-section-head">
|
||||
<h2 class="erp-section-title">업무 모듈</h2>
|
||||
<span class="erp-section-meta">{{ menu_items | length }}개 모듈</span>
|
||||
</div>
|
||||
|
||||
<div class="erp-grid">
|
||||
{% for item in menu_items %}
|
||||
{% set clickable = item.allowed and item.status == 'ready' %}
|
||||
{% if clickable %}
|
||||
<a class="erp-card" href="{{ item.url }}"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
data-clickable="true"
|
||||
{% if item.health_url %}data-health-url="{{ item.health_url }}"{% endif %}>
|
||||
{% else %}
|
||||
<div class="erp-card" data-clickable="false">
|
||||
{% endif %}
|
||||
|
||||
<div class="erp-card-head">
|
||||
<div>
|
||||
<div class="erp-card-title">{{ item.title }}</div>
|
||||
<div class="erp-card-subtitle">{{ item.subtitle }}</div>
|
||||
</div>
|
||||
{% if item.status == 'preparing' %}
|
||||
<span class="erp-badge erp-badge-neutral">준비중</span>
|
||||
{% elif not item.allowed %}
|
||||
<span class="erp-badge erp-badge-outline">권한 없음</span>
|
||||
{% elif item.health_url %}
|
||||
<span class="erp-badge erp-badge-neutral erp-badge-dot"
|
||||
data-health-url="{{ item.health_url }}" data-status="checking">확인 중</span>
|
||||
{% else %}
|
||||
<span class="erp-badge erp-badge-inverse">활성</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="card-desc">{{ item.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<span class="card-link">
|
||||
바로가기
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<polyline points="12 5 19 12 12 19"/>
|
||||
</svg>
|
||||
</span>
|
||||
<p class="erp-card-desc">{{ item.description }}</p>
|
||||
|
||||
<div class="erp-card-foot">
|
||||
<span class="erp-badge erp-badge-outline">{{ item.category }}</span>
|
||||
{% if clickable %}
|
||||
<span class="erp-card-link">
|
||||
바로가기
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<polyline points="12 5 19 12 12 19"/>
|
||||
</svg>
|
||||
</span>
|
||||
{% elif item.status == 'preparing' %}
|
||||
<span class="erp-card-link" style="color: var(--color-midtone-gray);">출시 예정</span>
|
||||
{% else %}
|
||||
<span class="erp-card-link" style="color: var(--color-midtone-gray);">접근 불가</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if clickable %}
|
||||
</a>
|
||||
{% else %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 오늘 날짜 표시
|
||||
const d = new Date();
|
||||
const days = ["일요일","월요일","화요일","수요일","목요일","금요일","토요일"];
|
||||
document.getElementById("today-date").textContent =
|
||||
d.getFullYear() + "년 " + (d.getMonth()+1) + "월 " + d.getDate() + "일 " + days[d.getDay()];
|
||||
// 인사말 — 시간대별
|
||||
(function() {
|
||||
const d = new Date();
|
||||
const h = d.getHours();
|
||||
let greet = "오늘도 좋은 하루 되세요.";
|
||||
if (h < 5) greet = "늦은 시간까지 수고 많으세요.";
|
||||
else if (h < 12) greet = "활기찬 아침입니다.";
|
||||
else if (h < 18) greet = "오후 업무 화이팅입니다.";
|
||||
else greet = "오늘도 고생 많으셨습니다.";
|
||||
const days = ["일","월","화","수","목","금","토"];
|
||||
const dateStr = `${d.getFullYear()}.${String(d.getMonth()+1).padStart(2,"0")}.${String(d.getDate()).padStart(2,"0")} (${days[d.getDay()]})`;
|
||||
document.getElementById("erp-greeting-sub").textContent = `${dateStr} · ${greet}`;
|
||||
})();
|
||||
|
||||
// 헬스 배지 — data-health-url 이 있는 카드만 갱신
|
||||
async function refreshHealthBadges() {
|
||||
const cards = document.querySelectorAll(".menu-card[data-health-url]");
|
||||
await Promise.all(Array.from(cards).map(async (card) => {
|
||||
const url = card.getAttribute("data-health-url");
|
||||
const badge = card.querySelector(".status-badge");
|
||||
if (!badge) return;
|
||||
// 헬스 배지 — data-health-url 이 있는 배지만 갱신
|
||||
async function refreshHealth() {
|
||||
const badges = document.querySelectorAll(".erp-badge[data-health-url]");
|
||||
await Promise.all(Array.from(badges).map(async (badge) => {
|
||||
const url = badge.getAttribute("data-health-url");
|
||||
try {
|
||||
const res = await fetch(url, { cache: "no-store", credentials: "omit" });
|
||||
const ok = res.ok;
|
||||
badge.dataset.status = ok ? "online" : "offline";
|
||||
badge.textContent = ok ? "온라인" : "오프라인";
|
||||
if (res.ok) {
|
||||
badge.dataset.status = "online";
|
||||
badge.className = "erp-badge erp-badge-success erp-badge-dot";
|
||||
badge.textContent = "온라인";
|
||||
} else {
|
||||
badge.dataset.status = "offline";
|
||||
badge.className = "erp-badge erp-badge-danger erp-badge-dot";
|
||||
badge.textContent = "오프라인";
|
||||
}
|
||||
} catch {
|
||||
badge.dataset.status = "offline";
|
||||
badge.className = "erp-badge erp-badge-danger erp-badge-dot";
|
||||
badge.textContent = "오프라인";
|
||||
}
|
||||
}));
|
||||
}
|
||||
refreshHealthBadges();
|
||||
setInterval(refreshHealthBadges, 30000);
|
||||
refreshHealth();
|
||||
setInterval(refreshHealth, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user