208 lines
5.0 KiB
HTML
208 lines
5.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="theme-color" content="#1a1d2e">
|
|
<title>오프라인 — GUARDiA ITSM</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--bg: #1a1d2e;
|
|
--card: #242740;
|
|
--border: #3a3d5c;
|
|
--primary: #4f8ef7;
|
|
--text: #e2e8f0;
|
|
--muted: #94a3b8;
|
|
--warn: #f59e0b;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 1rem;
|
|
padding: 2.5rem 2rem;
|
|
max-width: 420px;
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.icon {
|
|
font-size: 3.5rem;
|
|
line-height: 1;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.4rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.75rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
p {
|
|
color: var(--muted);
|
|
font-size: 0.95rem;
|
|
line-height: 1.6;
|
|
margin-bottom: 1.75rem;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
border: none;
|
|
transition: opacity 0.15s;
|
|
text-decoration: none;
|
|
}
|
|
.btn:hover { opacity: 0.85; }
|
|
|
|
.btn-primary {
|
|
background: var(--primary);
|
|
color: #fff;
|
|
}
|
|
.btn-outline {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--muted);
|
|
}
|
|
|
|
.status-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
margin-top: 1.75rem;
|
|
padding-top: 1.25rem;
|
|
border-top: 1px solid var(--border);
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--warn);
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.3; }
|
|
}
|
|
|
|
#retry-msg {
|
|
font-size: 0.82rem;
|
|
color: var(--muted);
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
@media (min-width: 480px) {
|
|
.actions { flex-direction: row; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<div class="icon">🔌</div>
|
|
<h1>네트워크 연결 없음</h1>
|
|
<p>
|
|
GUARDiA ITSM 서버에 연결할 수 없습니다.<br>
|
|
네트워크 상태를 확인하고 다시 시도해 주세요.
|
|
</p>
|
|
|
|
<div class="actions">
|
|
<button class="btn btn-primary" onclick="retryConnection()">
|
|
↻ 다시 시도
|
|
</button>
|
|
<button class="btn btn-outline" onclick="goBack()">
|
|
← 뒤로 가기
|
|
</button>
|
|
</div>
|
|
|
|
<div id="retry-msg"></div>
|
|
|
|
<div class="status-bar">
|
|
<div class="dot" id="status-dot"></div>
|
|
<span id="status-text">오프라인</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 온라인 상태 변화 감지
|
|
function updateOnlineStatus() {
|
|
const dot = document.getElementById('status-dot');
|
|
const text = document.getElementById('status-text');
|
|
if (navigator.onLine) {
|
|
dot.style.background = '#22c55e';
|
|
dot.style.animation = 'none';
|
|
text.textContent = '연결 복구됨 — 페이지를 새로고침합니다...';
|
|
setTimeout(() => location.reload(), 1200);
|
|
} else {
|
|
dot.style.background = 'var(--warn)';
|
|
dot.style.animation = 'pulse 1.5s infinite';
|
|
text.textContent = '오프라인';
|
|
}
|
|
}
|
|
|
|
window.addEventListener('online', updateOnlineStatus);
|
|
window.addEventListener('offline', updateOnlineStatus);
|
|
updateOnlineStatus();
|
|
|
|
function retryConnection() {
|
|
const msg = document.getElementById('retry-msg');
|
|
msg.textContent = '연결 시도 중...';
|
|
fetch('/api/metrics/health', { cache: 'no-store' })
|
|
.then(r => {
|
|
if (r.ok) {
|
|
msg.textContent = '연결 성공! 이동 중...';
|
|
setTimeout(() => location.href = '/', 800);
|
|
} else {
|
|
msg.textContent = '서버 응답 오류. 잠시 후 다시 시도해 주세요.';
|
|
}
|
|
})
|
|
.catch(() => {
|
|
msg.textContent = '연결 실패. 네트워크를 확인해 주세요.';
|
|
});
|
|
}
|
|
|
|
function goBack() {
|
|
if (history.length > 1) history.back();
|
|
else location.href = '/';
|
|
}
|
|
|
|
// 자동 재시도 (30초마다)
|
|
let retryTimer = setInterval(() => {
|
|
if (navigator.onLine) {
|
|
clearInterval(retryTimer);
|
|
retryConnection();
|
|
}
|
|
}, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|