사이드바 브랜딩을 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:
@@ -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>© 2025 팜큐(FARMQ). Powered by Flask + Headscale</span>
|
||||
<span>© 2025 PharmQ. Powered by Medivault</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
420
farmq-admin/templates/vm_list.html
Normal file
420
farmq-admin/templates/vm_list.html
Normal 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>
|
||||
244
farmq-admin/templates/vnc_console.html
Normal file
244
farmq-admin/templates/vnc_console.html
Normal 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>
|
||||
248
farmq-admin/templates/vnc_redirect.html
Normal file
248
farmq-admin/templates/vnc_redirect.html
Normal 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>
|
||||
Reference in New Issue
Block a user