사이드바 브랜딩을 PharmQ Super Admin (PSA)로 업데이트

- base.html 상단 네비게이션 브랜딩 변경
- 팜큐 약국 관리 시스템 → PharmQ Super Admin (PSA)
- UI 일관성 향상을 위한 브랜딩 통합

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-11 13:45:29 +09:00
parent 8dbf35d955
commit c37cf023c1
207 changed files with 38417 additions and 2 deletions

View File

@@ -146,7 +146,7 @@
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('dashboard') }}">
<i class="fas fa-hospital"></i> 팜큐 약국 관리 시스템
<i class="fas fa-hospital"></i> PharmQ Super Admin (PSA)
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
@@ -251,7 +251,7 @@
<!-- 푸터 -->
<footer class="footer">
<div class="container">
<span>&copy; 2025 팜큐(FARMQ). Powered by Flask + Headscale</span>
<span>&copy; 2025 PharmQ. Powered by Medivault</span>
</div>
</footer>

View File

@@ -0,0 +1,420 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proxmox VM 관리 - 팜큐 관리자</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body {
background-color: #f8f9fa;
}
.vm-card {
transition: transform 0.2s;
border-left: 4px solid #dee2e6;
}
.vm-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.vm-card.running {
border-left-color: #28a745;
}
.vm-card.stopped {
border-left-color: #dc3545;
}
.status-badge {
font-size: 0.875rem;
}
.vm-actions .btn {
margin: 2px;
}
.resource-info {
font-size: 0.875rem;
color: #6c757d;
}
.loading-spinner {
display: none;
}
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-hospital-alt"></i> 팜큐 관리자
</a>
<div class="navbar-nav">
<a class="nav-link" href="/">대시보드</a>
<a class="nav-link" href="/machines">머신 목록</a>
<a class="nav-link active" href="/vms">VM 관리</a>
</div>
</div>
</nav>
<div class="container mt-4">
<!-- 헤더 -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-desktop"></i> Proxmox VM 관리</h2>
<div>
<button id="refresh-btn" class="btn btn-outline-primary">
<i class="fas fa-sync-alt"></i> 새로고침
</button>
<span class="text-muted ms-3">
<i class="fas fa-server"></i> {{ host }}
</span>
</div>
</div>
</div>
</div>
<!-- 통계 카드 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-white bg-primary">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">{{ total_vms }}</h4>
<p class="card-text">총 VM 수</p>
</div>
<div class="align-self-center">
<i class="fas fa-desktop fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">{{ running_vms }}</h4>
<p class="card-text">실행 중</p>
</div>
<div class="align-self-center">
<i class="fas fa-play-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-secondary">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">{{ stopped_vms }}</h4>
<p class="card-text">정지됨</p>
</div>
<div class="align-self-center">
<i class="fas fa-stop-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-info">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">{{ vnc_ready_vms }}</h4>
<p class="card-text">VNC 가능</p>
</div>
<div class="align-self-center">
<i class="fas fa-eye fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- VM 목록 -->
<div class="row">
{% for vm in vms %}
<div class="col-lg-6 col-xl-4 mb-4">
<div class="card vm-card {{ 'running' if vm.status == 'running' else 'stopped' }}">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<strong>{{ vm.name or 'VM-' + vm.vmid|string }}</strong>
</h6>
{% if vm.status == 'running' %}
<span class="badge bg-success status-badge">
<i class="fas fa-play"></i> 실행 중
</span>
{% else %}
<span class="badge bg-secondary status-badge">
<i class="fas fa-stop"></i> 정지됨
</span>
{% endif %}
</div>
<div class="card-body">
<div class="resource-info mb-3">
<div class="row">
<div class="col-6">
<i class="fas fa-microchip"></i>
{{ vm.maxcpu or 'N/A' }}코어
</div>
<div class="col-6">
<i class="fas fa-memory"></i>
{{ (vm.maxmem/1024/1024/1024)|round(1) if vm.maxmem else 'N/A' }}GB
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<i class="fas fa-server"></i>
{{ vm.node }}
</div>
<div class="col-6">
<i class="fas fa-tag"></i>
VM {{ vm.vmid }}
</div>
</div>
</div>
<div class="vm-actions">
{% if vm.status == 'running' %}
<button class="btn btn-primary btn-sm"
onclick="openVNC('{{ vm.node }}', {{ vm.vmid }}, '{{ vm.name or 'VM-' + vm.vmid|string }}')">
<i class="fas fa-desktop"></i> VNC 접속
</button>
<button class="btn btn-warning btn-sm"
onclick="stopVM('{{ vm.node }}', {{ vm.vmid }})">
<i class="fas fa-stop"></i> 정지
</button>
{% else %}
<button class="btn btn-success btn-sm"
onclick="startVM('{{ vm.node }}', {{ vm.vmid }})">
<i class="fas fa-play"></i> 시작
</button>
<button class="btn btn-secondary btn-sm" disabled>
<i class="fas fa-desktop"></i> VNC 불가
</button>
{% endif %}
<button class="btn btn-info btn-sm"
onclick="showVMDetails('{{ vm.node }}', {{ vm.vmid }})">
<i class="fas fa-info-circle"></i> 상세정보
</button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% if not vms %}
<div class="row">
<div class="col-12">
<div class="alert alert-warning text-center">
<i class="fas fa-exclamation-triangle"></i>
<strong>VM을 찾을 수 없습니다.</strong><br>
Proxmox 서버 연결을 확인해주세요.
</div>
</div>
</div>
{% endif %}
</div>
<!-- 로딩 스피너 -->
<div class="loading-spinner position-fixed top-50 start-50 translate-middle">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">처리 중...</span>
</div>
</div>
<!-- Toast 컨테이너 -->
<div class="toast-container" id="toast-container"></div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// VNC 연결 열기
async function openVNC(node, vmid, vmName) {
try {
showSpinner();
const response = await fetch('/api/vm/vnc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
node: node,
vmid: vmid,
vm_name: vmName
})
});
const data = await response.json();
if (response.ok) {
// 새 창에서 VNC 콘솔 열기
const vncWindow = window.open(
`/vnc/${data.session_id}`,
`vnc_${vmid}`,
'width=1024,height=768,scrollbars=no,resizable=yes,toolbar=no,location=no'
);
if (!vncWindow) {
showToast('팝업 차단됨', '팝업 차단을 해제하고 다시 시도해주세요.', 'warning');
}
} else {
showToast('VNC 연결 실패', data.error || '알 수 없는 오류가 발생했습니다.', 'danger');
}
} catch (error) {
showToast('연결 오류', error.message, 'danger');
} finally {
hideSpinner();
}
}
// VM 시작
async function startVM(node, vmid) {
if (!confirm(`VM ${vmid}을(를) 시작하시겠습니까?`)) return;
try {
showSpinner();
const response = await fetch('/api/vm/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
node: node,
vmid: vmid
})
});
const data = await response.json();
if (response.ok) {
showToast('VM 시작', `VM ${vmid}이(가) 시작되었습니다.`, 'success');
setTimeout(() => location.reload(), 2000);
} else {
showToast('시작 실패', data.error || '알 수 없는 오류가 발생했습니다.', 'danger');
}
} catch (error) {
showToast('연결 오류', error.message, 'danger');
} finally {
hideSpinner();
}
}
// VM 정지
async function stopVM(node, vmid) {
if (!confirm(`VM ${vmid}을(를) 정지하시겠습니까?`)) return;
try {
showSpinner();
const response = await fetch('/api/vm/stop', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
node: node,
vmid: vmid
})
});
const data = await response.json();
if (response.ok) {
showToast('VM 정지', `VM ${vmid}이(가) 정지되었습니다.`, 'success');
setTimeout(() => location.reload(), 2000);
} else {
showToast('정지 실패', data.error || '알 수 없는 오류가 발생했습니다.', 'danger');
}
} catch (error) {
showToast('연결 오류', error.message, 'danger');
} finally {
hideSpinner();
}
}
// VM 상세정보
function showVMDetails(node, vmid) {
// 상세정보 모달이나 페이지로 이동
alert(`VM ${vmid} 상세정보 (노드: ${node})`);
}
// 새로고침
document.getElementById('refresh-btn').onclick = function() {
location.reload();
};
// 스피너 표시/숨김
function showSpinner() {
document.querySelector('.loading-spinner').style.display = 'block';
}
function hideSpinner() {
document.querySelector('.loading-spinner').style.display = 'none';
}
// Toast 메시지 표시
function showToast(title, message, type = 'info') {
const toastContainer = document.getElementById('toast-container');
const toastId = 'toast-' + Date.now();
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('id', toastId);
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<strong>${title}</strong><br>
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto"
data-bs-dismiss="toast"></button>
</div>
`;
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
// 토스트가 숨겨진 후 DOM에서 제거
toast.addEventListener('hidden.bs.toast', function() {
toast.remove();
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ vm_name }} - VNC 콘솔</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
margin: 0;
padding: 0;
background-color: #000;
overflow: hidden;
}
.vnc-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.vnc-toolbar {
background: #2c3e50;
padding: 10px;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
}
.vnc-screen {
flex: 1;
position: relative;
background: #000;
}
.vnc-status {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
text-align: center;
z-index: 100;
}
.btn-vnc {
margin: 0 5px;
padding: 5px 10px;
font-size: 12px;
}
#vnc-canvas {
margin: 0;
padding: 0;
}
.connection-info {
font-size: 12px;
opacity: 0.8;
}
</style>
</head>
<body>
<div class="vnc-container">
<!-- VNC 툴바 -->
<div class="vnc-toolbar">
<div>
<strong>🖥️ {{ vm_name }}</strong>
<span class="connection-info">(VM {{ vmid }} @ {{ node }})</span>
</div>
<div class="vnc-controls">
<button id="fullscreen-btn" class="btn btn-sm btn-primary btn-vnc">
<i class="fas fa-expand"></i> 전체화면
</button>
<button id="clipboard-btn" class="btn btn-sm btn-info btn-vnc">
<i class="fas fa-clipboard"></i> 클립보드
</button>
<button id="ctrl-alt-del-btn" class="btn btn-sm btn-warning btn-vnc">
<i class="fas fa-keyboard"></i> Ctrl+Alt+Del
</button>
<button id="disconnect-btn" class="btn btn-sm btn-danger btn-vnc">
<i class="fas fa-times"></i> 연결종료
</button>
</div>
</div>
<!-- VNC 화면 -->
<div class="vnc-screen">
<div id="vnc-status" class="vnc-status">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">연결 중...</span>
</div>
<div class="mt-3">
<h5>VNC 연결 중...</h5>
<p>{{ vm_name }}에 연결하고 있습니다.</p>
<small>WebSocket URL: {{ websocket_url[:50] }}...</small>
</div>
</div>
<canvas id="vnc-canvas"></canvas>
</div>
</div>
<!-- CDN에서 noVNC 로드 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/noVNC/1.3.0/core/rfb.min.js"></script>
<!-- Bootstrap Icons -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script>
// VNC 연결 설정
const websocketUrl = '{{ websocket_url }}';
const vmName = '{{ vm_name }}';
let rfb;
let isConnected = false;
// DOM 요소
const statusDiv = document.getElementById('vnc-status');
const canvas = document.getElementById('vnc-canvas');
// VNC 연결 시작
function connectVNC() {
try {
console.log('VNC 연결 시도:', websocketUrl);
// RFB 객체 생성 (간소화된 설정)
rfb = new RFB(canvas, websocketUrl);
// 이벤트 리스너 등록
rfb.addEventListener('connect', onConnected);
rfb.addEventListener('disconnect', onDisconnected);
rfb.addEventListener('credentialsrequired', onCredentialsRequired);
// 화면 크기 자동 조정
rfb.scaleViewport = true;
rfb.resizeSession = false;
} catch (error) {
console.error('VNC 연결 오류:', error);
showStatus('❌ 연결 실패', 'VNC 연결 중 오류가 발생했습니다: ' + error.message, 'danger');
}
}
// 연결 성공
function onConnected() {
console.log('VNC 연결 성공');
isConnected = true;
statusDiv.style.display = 'none';
canvas.style.display = 'block';
}
// 연결 해제
function onDisconnected(e) {
console.log('VNC 연결 해제:', e.detail);
isConnected = false;
if (e.detail.clean) {
showStatus('🔌 연결 종료', '연결이 정상적으로 종료되었습니다.', 'info');
} else {
showStatus('❌ 연결 끊김', '연결이 예기치 않게 끊어졌습니다: ' + (e.detail.reason || 'Unknown'), 'warning');
}
}
// 인증 정보 필요
function onCredentialsRequired() {
console.log('VNC 인증 정보 필요');
showStatus('🔐 인증 필요', 'VNC 서버에서 인증이 필요합니다.', 'warning');
}
// 상태 표시
function showStatus(title, message, type = 'info') {
statusDiv.innerHTML = `
<div class="alert alert-${type}" role="alert">
<h5>${title}</h5>
<p>${message}</p>
<button class="btn btn-secondary" onclick="location.reload()">다시 시도</button>
</div>
`;
statusDiv.style.display = 'block';
}
// 전체화면
document.getElementById('fullscreen-btn').onclick = function() {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.body.requestFullscreen();
}
};
// 클립보드
document.getElementById('clipboard-btn').onclick = function() {
if (rfb && isConnected) {
const text = prompt('클립보드에 보낼 텍스트를 입력하세요:');
if (text) {
rfb.clipboardPasteFrom(text);
}
}
};
// Ctrl+Alt+Del 전송
document.getElementById('ctrl-alt-del-btn').onclick = function() {
if (rfb && isConnected) {
rfb.sendCtrlAltDel();
}
};
// 연결 종료
document.getElementById('disconnect-btn').onclick = function() {
if (rfb) {
rfb.disconnect();
}
window.close();
};
// 페이지 로드 후 연결 시작
window.addEventListener('load', function() {
// noVNC 라이브러리 로드 확인
if (typeof RFB === 'undefined') {
showStatus('❌ 라이브러리 오류', 'noVNC 라이브러리를 로드할 수 없습니다.', 'danger');
return;
}
// 잠시 후 연결 시작
setTimeout(connectVNC, 1000);
});
// 페이지 종료 시 연결 해제
window.addEventListener('beforeunload', function() {
if (rfb) {
rfb.disconnect();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,248 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ vm_name }} - VNC 접속</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.vnc-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
padding: 3rem;
max-width: 600px;
width: 100%;
text-align: center;
}
.vm-icon {
font-size: 4rem;
color: #667eea;
margin-bottom: 1rem;
}
.vm-info {
background: #f8f9fa;
border-radius: 10px;
padding: 1.5rem;
margin: 2rem 0;
}
.method-card {
border: 2px solid #e9ecef;
border-radius: 10px;
padding: 1.5rem;
margin: 1rem 0;
transition: all 0.3s ease;
}
.method-card:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.method-card.recommended {
border-color: #28a745;
background: linear-gradient(145deg, #f8fff8, #ffffff);
}
.btn-vnc {
padding: 12px 30px;
font-size: 1.1rem;
border-radius: 25px;
transition: all 0.3s ease;
}
.btn-vnc:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.countdown {
font-size: 2rem;
font-weight: bold;
color: #667eea;
}
.security-note {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
padding: 1rem;
margin-top: 2rem;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="vnc-card">
<div class="vm-icon">
<i class="fas fa-desktop"></i>
</div>
<h1 class="mb-3">{{ vm_name }}</h1>
<p class="lead text-muted">VNC 원격 접속</p>
<div class="vm-info">
<div class="row">
<div class="col-6">
<strong><i class="fas fa-server"></i> 노드</strong><br>
<span class="text-muted">{{ node }}</span>
</div>
<div class="col-6">
<strong><i class="fas fa-tag"></i> VM ID</strong><br>
<span class="text-muted">{{ vmid }}</span>
</div>
</div>
</div>
<!-- 방법 1: 직접 Proxmox 접속 (권장) -->
<div class="method-card recommended">
<div class="d-flex align-items-center mb-3">
<i class="fas fa-star text-success me-2"></i>
<h4 class="mb-0 text-success">권장 방법</h4>
</div>
<h5>Proxmox 웹 콘솔로 접속</h5>
<p class="text-muted">
Proxmox의 기본 noVNC 콘솔로 직접 연결합니다.<br>
가장 안정적이고 모든 기능을 사용할 수 있습니다.
</p>
<a href="{{ proxmox_url }}" target="_blank" class="btn btn-success btn-vnc">
<i class="fas fa-external-link-alt"></i> Proxmox 웹 콘솔 열기
</a>
<div class="mt-3">
<small class="text-muted">
<i class="fas fa-info-circle"></i>
새 탭에서 {{ host }} 웹 인터페이스가 열립니다
</small>
</div>
</div>
<!-- 방법 2: 수동 접속 -->
<div class="method-card">
<h5>수동 접속 방법</h5>
<p class="text-muted">
브라우저에서 직접 Proxmox 주소로 이동하여 접속합니다.
</p>
<div class="input-group mb-3">
<input type="text" class="form-control" value="{{ proxmox_url }}" readonly id="proxmox-url">
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard()">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="row">
<div class="col-md-6">
<button class="btn btn-primary btn-vnc w-100" onclick="openProxmox()">
<i class="fas fa-globe"></i> 브라우저에서 열기
</button>
</div>
<div class="col-md-6">
<button class="btn btn-info btn-vnc w-100" onclick="copyToClipboard()">
<i class="fas fa-copy"></i> URL 복사
</button>
</div>
</div>
</div>
<!-- 보안 알림 -->
<div class="security-note">
<i class="fas fa-shield-alt"></i>
<strong>보안 알림:</strong>
SSL 인증서 경고가 표시되면 "고급" → "{{ host }}로 이동(안전하지 않음)"을 클릭하세요.
팜큐 내부 네트워크에서만 접근 가능합니다.
</div>
<!-- 뒤로가기 -->
<div class="mt-4">
<a href="/vms" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> VM 목록으로 돌아가기
</a>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Proxmox URL
const proxmoxUrl = '{{ proxmox_url }}';
// Proxmox 웹 콘솔 열기
function openProxmox() {
window.open(proxmoxUrl, '_blank');
}
// URL 클립보드 복사
function copyToClipboard() {
const urlInput = document.getElementById('proxmox-url');
urlInput.select();
urlInput.setSelectionRange(0, 99999); // 모바일 대응
try {
document.execCommand('copy');
showToast('URL이 클립보드에 복사되었습니다!', 'success');
} catch (err) {
// 클립보드 API 사용 (최신 브라우저)
if (navigator.clipboard) {
navigator.clipboard.writeText(proxmoxUrl).then(() => {
showToast('URL이 클립보드에 복사되었습니다!', 'success');
}).catch(() => {
showToast('복사에 실패했습니다. 수동으로 복사해주세요.', 'warning');
});
} else {
showToast('복사에 실패했습니다. 수동으로 복사해주세요.', 'warning');
}
}
}
// 토스트 메시지 표시
function showToast(message, type = 'info') {
const toastContainer = document.createElement('div');
toastContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
`;
const toast = document.createElement('div');
toast.className = `alert alert-${type} alert-dismissible fade show`;
toast.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
toastContainer.appendChild(toast);
document.body.appendChild(toastContainer);
// 3초 후 자동 제거
setTimeout(() => {
toastContainer.remove();
}, 3000);
}
// 페이지 로드 시 자동으로 Proxmox 페이지 열기 (옵션)
// window.addEventListener('load', function() {
// setTimeout(openProxmox, 2000); // 2초 후 자동 열기
// });
</script>
</body>
</html>