initial commit
This commit is contained in:
117
Web/wwwroot/index.html
Normal file
117
Web/wwwroot/index.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VNC 서버 목록 관리</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3B82F6',
|
||||
secondary: '#6B7280',
|
||||
success: '#10B981',
|
||||
danger: '#EF4444',
|
||||
warning: '#F59E0B'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 헤더 -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h1 class="text-3xl font-bold text-gray-800 mb-2">VNC 서버 목록 관리</h1>
|
||||
<p class="text-gray-600">VNC 서버를 관리하고 연결할 수 있습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- 상태 표시 -->
|
||||
<div id="status" class="mb-6"></div>
|
||||
|
||||
<!-- 서버 추가 버튼 -->
|
||||
<div class="mb-6">
|
||||
<button id="addServerBtn" class="bg-primary hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition duration-200">
|
||||
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
새 서버 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 서버 목록 -->
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-xl font-semibold text-gray-800">서버 목록</h2>
|
||||
</div>
|
||||
<div id="serverList" class="divide-y divide-gray-200">
|
||||
<!-- 서버 목록이 여기에 동적으로 로드됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 서버 추가/편집 모달 -->
|
||||
<div id="serverModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 id="modalTitle" class="text-lg font-semibold text-gray-800">서버 추가</h3>
|
||||
</div>
|
||||
<form id="serverForm" class="p-6">
|
||||
<div class="mb-4">
|
||||
<label for="serverUser" class="block text-sm font-medium text-gray-700 mb-2">사용자</label>
|
||||
<input type="text" id="serverUser" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="serverIp" class="block text-sm font-medium text-gray-700 mb-2">IP 주소</label>
|
||||
<input type="text" id="serverIp" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="serverCategory" class="block text-sm font-medium text-gray-700 mb-2">카테고리</label>
|
||||
<input type="text" id="serverCategory" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="serverDescription" class="block text-sm font-medium text-gray-700 mb-2">설명</label>
|
||||
<textarea id="serverDescription" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="serverPassword" class="block text-sm font-medium text-gray-700 mb-2">비밀번호</label>
|
||||
<input type="password" id="serverPassword" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="serverArgument" class="block text-sm font-medium text-gray-700 mb-2">VNC 인수</label>
|
||||
<input type="text" id="serverArgument" placeholder="-port=5900" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" id="cancelBtn" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition duration-200">취소</button>
|
||||
<button type="submit" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-blue-700 transition duration-200">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 확인 모달 -->
|
||||
<div id="confirmModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800">확인</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p id="confirmMessage" class="text-gray-700 mb-6"></p>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button id="confirmCancel" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition duration-200">취소</button>
|
||||
<button id="confirmOk" class="px-4 py-2 bg-danger text-white rounded-md hover:bg-red-700 transition duration-200">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
305
Web/wwwroot/js/app.js
Normal file
305
Web/wwwroot/js/app.js
Normal file
@@ -0,0 +1,305 @@
|
||||
class VNCServerApp {
|
||||
constructor() {
|
||||
this.apiBase = '/api/vncserver';
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.loadServerList();
|
||||
this.checkVNCStatus();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 서버 추가 버튼
|
||||
document.getElementById('addServerBtn').addEventListener('click', () => {
|
||||
this.showServerModal();
|
||||
});
|
||||
|
||||
// 모달 취소 버튼
|
||||
document.getElementById('cancelBtn').addEventListener('click', () => {
|
||||
this.hideServerModal();
|
||||
});
|
||||
|
||||
// 서버 폼 제출
|
||||
document.getElementById('serverForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.saveServer();
|
||||
});
|
||||
|
||||
// 확인 모달 이벤트
|
||||
document.getElementById('confirmCancel').addEventListener('click', () => {
|
||||
this.hideConfirmModal();
|
||||
});
|
||||
|
||||
document.getElementById('confirmOk').addEventListener('click', () => {
|
||||
this.executeConfirmAction();
|
||||
});
|
||||
}
|
||||
|
||||
async loadServerList() {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/list`);
|
||||
if (!response.ok) throw new Error('서버 목록을 불러올 수 없습니다.');
|
||||
|
||||
const servers = await response.json();
|
||||
this.renderServerList(servers);
|
||||
} catch (error) {
|
||||
this.showError('서버 목록을 불러오는 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
renderServerList(servers) {
|
||||
const serverList = document.getElementById('serverList');
|
||||
|
||||
if (servers.length === 0) {
|
||||
serverList.innerHTML = `
|
||||
<div class="px-6 py-8 text-center text-gray-500">
|
||||
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<p>등록된 서버가 없습니다.</p>
|
||||
<p class="text-sm">새 서버를 추가해보세요.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
serverList.innerHTML = servers.map(server => `
|
||||
<div class="px-6 py-4 hover:bg-gray-50 transition duration-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
<h3 class="text-lg font-medium text-gray-900">${this.escapeHtml(server.user)}@${this.escapeHtml(server.ip)}</h3>
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
<p><span class="font-medium">IP:</span> ${this.escapeHtml(server.ip)}</p>
|
||||
${server.category ? `<p class="mt-1"><span class="font-medium">카테고리:</span> ${this.escapeHtml(server.category)}</p>` : ''}
|
||||
${server.description ? `<p class="mt-1"><span class="font-medium">설명:</span> ${this.escapeHtml(server.description)}</p>` : ''}
|
||||
${server.argument ? `<p class="mt-1"><span class="font-medium">인수:</span> ${this.escapeHtml(server.argument)}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="app.connectToServer('${this.escapeHtml(server.user)}', '${this.escapeHtml(server.ip)}')" class="bg-primary hover:bg-blue-700 text-white px-3 py-1 rounded text-sm transition duration-200">
|
||||
연결
|
||||
</button>
|
||||
<button onclick="app.editServer('${this.escapeHtml(server.user)}', '${this.escapeHtml(server.ip)}')" class="bg-secondary hover:bg-gray-600 text-white px-3 py-1 rounded text-sm transition duration-200">
|
||||
편집
|
||||
</button>
|
||||
<button onclick="app.deleteServer('${this.escapeHtml(server.user)}', '${this.escapeHtml(server.ip)}', '${this.escapeHtml(server.user)}@${this.escapeHtml(server.ip)}')" class="bg-danger hover:bg-red-700 text-white px-3 py-1 rounded text-sm transition duration-200">
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async checkVNCStatus() {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/vnc-status`);
|
||||
if (!response.ok) throw new Error('VNC 상태를 확인할 수 없습니다.');
|
||||
|
||||
const status = await response.json();
|
||||
this.showVNCStatus(status);
|
||||
} catch (error) {
|
||||
this.showError('VNC 상태 확인 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
showVNCStatus(status) {
|
||||
const statusDiv = document.getElementById('status');
|
||||
if (status.isInstalled) {
|
||||
statusDiv.innerHTML = `
|
||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
VNC Viewer가 설치되어 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
statusDiv.innerHTML = `
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
VNC Viewer가 설치되어 있지 않습니다. 경로: ${status.path}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
showServerModal(server = null) {
|
||||
const modal = document.getElementById('serverModal');
|
||||
const title = document.getElementById('modalTitle');
|
||||
const form = document.getElementById('serverForm');
|
||||
|
||||
if (server) {
|
||||
title.textContent = '서버 편집';
|
||||
document.getElementById('serverUser').value = server.user;
|
||||
document.getElementById('serverIp').value = server.ip;
|
||||
document.getElementById('serverCategory').value = server.category || '';
|
||||
document.getElementById('serverDescription').value = server.description || '';
|
||||
document.getElementById('serverPassword').value = server.password || '';
|
||||
document.getElementById('serverArgument').value = server.argument || '';
|
||||
} else {
|
||||
title.textContent = '서버 추가';
|
||||
form.reset();
|
||||
}
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
hideServerModal() {
|
||||
document.getElementById('serverModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async saveServer() {
|
||||
const serverUser = document.getElementById('serverUser').value;
|
||||
const serverIp = document.getElementById('serverIp').value;
|
||||
const serverData = {
|
||||
user: serverUser,
|
||||
ip: serverIp,
|
||||
category: document.getElementById('serverCategory').value,
|
||||
description: document.getElementById('serverDescription').value,
|
||||
password: document.getElementById('serverPassword').value,
|
||||
argument: document.getElementById('serverArgument').value
|
||||
};
|
||||
|
||||
try {
|
||||
const url = serverUser && serverIp ? `${this.apiBase}/update` : `${this.apiBase}/add`;
|
||||
const method = serverUser && serverIp ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('서버 저장에 실패했습니다.');
|
||||
|
||||
const result = await response.json();
|
||||
this.showSuccess(result.message);
|
||||
this.hideServerModal();
|
||||
this.loadServerList();
|
||||
} catch (error) {
|
||||
this.showError('서버 저장 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async editServer(user, ip) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/get/${encodeURIComponent(user)}/${encodeURIComponent(ip)}`);
|
||||
if (!response.ok) throw new Error('서버 정보를 불러올 수 없습니다.');
|
||||
|
||||
const server = await response.json();
|
||||
this.showServerModal(server);
|
||||
} catch (error) {
|
||||
this.showError('서버 정보를 불러오는 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
deleteServer(user, ip, name) {
|
||||
this.showConfirmModal(
|
||||
`"${name}" 서버를 삭제하시겠습니까?`,
|
||||
() => this.executeDeleteServer(user, ip)
|
||||
);
|
||||
}
|
||||
|
||||
async executeDeleteServer(user, ip) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/delete/${encodeURIComponent(user)}/${encodeURIComponent(ip)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('서버 삭제에 실패했습니다.');
|
||||
|
||||
const result = await response.json();
|
||||
this.showSuccess(result.message);
|
||||
this.loadServerList();
|
||||
} catch (error) {
|
||||
this.showError('서버 삭제 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async connectToServer(user, ip) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/connect/${encodeURIComponent(user)}/${encodeURIComponent(ip)}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('VNC 연결에 실패했습니다.');
|
||||
|
||||
const result = await response.json();
|
||||
this.showSuccess(result.message);
|
||||
} catch (error) {
|
||||
this.showError('VNC 연결 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
showConfirmModal(message, onConfirm) {
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
document.getElementById('confirmModal').classList.remove('hidden');
|
||||
this.confirmAction = onConfirm;
|
||||
}
|
||||
|
||||
hideConfirmModal() {
|
||||
document.getElementById('confirmModal').classList.add('hidden');
|
||||
this.confirmAction = null;
|
||||
}
|
||||
|
||||
executeConfirmAction() {
|
||||
if (this.confirmAction) {
|
||||
this.confirmAction();
|
||||
}
|
||||
this.hideConfirmModal();
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
this.showNotification(message, 'success');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.showNotification(message, 'error');
|
||||
}
|
||||
|
||||
showNotification(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 transition-all duration-300 transform translate-x-full ${
|
||||
type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
||||
}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 애니메이션
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// 자동 제거
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// 앱 초기화
|
||||
const app = new VNCServerApp();
|
||||
Reference in New Issue
Block a user