class VNCServerApp {
constructor() {
this.apiBase = '/api/vncserver';
this.init();
// 개발용: Ctrl+R로 강제 새로고침
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'r') {
e.preventDefault();
window.location.reload(true);
}
});
}
async init() {
this.bindEvents();
await this.loadSettings(); // 설정 먼저 로드
// 사용자 이름이 없으면 설정 모달 자동 열기
const userName = document.getElementById('userName')?.value || '';
if (!userName) {
this.showSettingsModal();
}
this.loadServerList();
this.checkVNCStatus();
}
bindEvents() {
// 서버 추가 버튼
document.getElementById('addServerBtn').addEventListener('click', () => {
this.showServerModal();
});
// 설정 버튼
document.getElementById('settingsBtn').addEventListener('click', () => {
this.showSettingsModal();
});
// 모달 취소 버튼
document.getElementById('cancelBtn').addEventListener('click', () => {
this.hideServerModal();
});
document.getElementById('settingsCancelBtn').addEventListener('click', () => {
this.hideSettingsModal();
});
// 서버 폼 제출
document.getElementById('serverForm').addEventListener('submit', (e) => {
e.preventDefault();
this.saveServer();
});
// 설정 폼 제출
document.getElementById('settingsForm').addEventListener('submit', (e) => {
e.preventDefault();
this.saveSettings();
});
// VNC 경로 찾아보기 버튼
document.getElementById('browseVncBtn').addEventListener('click', () => {
this.browseVNCViewer();
});
// 확인 모달 이벤트
document.getElementById('confirmCancel').addEventListener('click', () => {
this.hideConfirmModal();
});
document.getElementById('confirmOk').addEventListener('click', () => {
this.executeConfirmAction();
});
}
async loadServerList() {
try {
// 설정에서 사용자 이름 가져오기
const userName = document.getElementById('userName')?.value || '';
// 사용자 이름이 없으면 서버 목록을 로드하지 않고 설정 안내 메시지 표시
if (!userName) {
const serverList = document.getElementById('serverList');
serverList.innerHTML = this.renderTemplate('userNameRequiredTemplate', {});
return;
}
let url = `${this.apiBase}/list?userName=${encodeURIComponent(userName)}`;
const response = await fetch(url);
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');
console.log('서버 데이터:', servers); // 디버깅용
if (servers.length === 0) {
serverList.innerHTML = this.renderTemplate('emptyServerListTemplate', {});
return;
}
// 계층적 카테고리 구조 생성
const categoryTree = {};
servers.forEach(server => {
console.log('서버 카테고리:', server.category, '타입:', typeof server.category); // 디버깅용
console.log('서버 Category:', server.Category, '타입:', typeof server.Category); // 디버깅용
// category와 Category 둘 다 확인
const categoryValue = server.category || server.Category || '';
if (categoryValue && categoryValue.trim()) {
console.log('처리할 카테고리 값:', categoryValue); // 디버깅용
// | 기호로 구분된 카테고리들을 분리하여 계층 구조 생성
const categoryPath = categoryValue.split('|').map(cat => cat.trim()).filter(cat => cat.length > 0);
console.log('카테고리 경로:', categoryPath); // 디버깅용
let currentLevel = categoryTree;
categoryPath.forEach((category, index) => {
if (!currentLevel[category]) {
currentLevel[category] = {
servers: [],
subcategories: {}
};
}
// 마지막 레벨에만 서버 추가
if (index === categoryPath.length - 1) {
currentLevel[category].servers.push(server);
}
currentLevel = currentLevel[category].subcategories;
});
} else {
// 카테고리가 없으면 '기타'로 분류
if (!categoryTree['기타']) {
categoryTree['기타'] = {
servers: [],
subcategories: {}
};
}
categoryTree['기타'].servers.push(server);
}
});
console.log('카테고리 트리:', categoryTree); // 디버깅용
let html = '';
const sortedCategories = Object.keys(categoryTree).sort();
sortedCategories.forEach(category => {
html += this.renderCategoryNode(category, categoryTree[category], 0);
});
serverList.innerHTML = html;
}
async checkVNCStatus() {
try {
console.log('VNC 상태 확인 시작...'); // 디버깅용
const response = await fetch(`${this.apiBase}/vnc-status`);
if (!response.ok) throw new Error('VNC 상태를 확인할 수 없습니다.');
const status = await response.json();
console.log('VNC 상태:', status); // 디버깅용
this.showVNCStatus(status);
} catch (error) {
console.error('VNC 상태 확인 오류:', error); // 디버깅용
this.showError('VNC 상태 확인 중 오류가 발생했습니다: ' + error.message);
}
}
showVNCStatus(status) {
const statusDiv = document.getElementById('status');
if (status.IsInstalled) {
// VNC가 설치되어 있으면 상태 메시지를 숨김
statusDiv.innerHTML = '';
} else {
statusDiv.innerHTML = `
VNC Viewer를 찾을 수 없습니다. 경로: ${status.Path}
`;
}
}
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('serverTitle').value = server.title || '';
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');
}
showSettingsModal() {
console.log('설정 모달 열기 시작'); // 디버깅용
this.loadSettings();
document.getElementById('settingsModal').classList.remove('hidden');
console.log('설정 모달 열기 완료'); // 디버깅용
}
hideSettingsModal() {
document.getElementById('settingsModal').classList.add('hidden');
}
async loadSettings() {
try {
console.log('설정 로드 시작...'); // 디버깅용
const response = await fetch(`${this.apiBase}/settings`);
console.log('설정 로드 응답 상태:', response.status); // 디버깅용
if (!response.ok) throw new Error('설정을 불러올 수 없습니다.');
const settings = await response.json();
console.log('로드된 설정:', settings); // 디버깅용
console.log('VNC 경로:', settings.VNCViewerPath); // 디버깅용
console.log('웹 포트:', settings.WebServerPort); // 디버깅용
const userNameElement = document.getElementById('userName');
const vncPathElement = document.getElementById('vncViewerPath');
const vncArgumentElement = document.getElementById('vncArgument');
const webPortElement = document.getElementById('webServerPort');
console.log('사용자 이름 요소:', userNameElement); // 디버깅용
console.log('VNC 경로 요소:', vncPathElement); // 디버깅용
console.log('VNC 인수 요소:', vncArgumentElement); // 디버깅용
console.log('웹 포트 요소:', webPortElement); // 디버깅용
userNameElement.value = settings.UserName || '';
vncPathElement.value = settings.VNCViewerPath || '';
vncArgumentElement.value = settings.Argument || '';
webPortElement.value = settings.WebServerPort || 8080;
console.log('설정 로드 완료'); // 디버깅용
console.log('설정된 VNC 경로 값:', vncPathElement.value); // 디버깅용
console.log('설정된 웹 포트 값:', webPortElement.value); // 디버깅용
} catch (error) {
console.error('설정 로드 오류:', error); // 디버깅용
this.showError('설정을 불러오는 중 오류가 발생했습니다: ' + error.message);
}
}
async saveSettings() {
const userName = document.getElementById('userName').value.trim();
const vncPath = document.getElementById('vncViewerPath').value;
const vncArgument = document.getElementById('vncArgument').value;
const webPort = parseInt(document.getElementById('webServerPort').value);
// 사용자 이름 필수 검증
if (!userName) {
this.showError('사용자 이름을 입력해주세요.');
document.getElementById('userName').focus();
return;
}
const settings = {
UserName: userName,
VNCViewerPath: vncPath,
Argument: vncArgument,
WebServerPort: webPort
};
console.log('저장할 설정:', settings); // 디버깅용
console.log('VNC 경로 값:', vncPath); // 디버깅용
console.log('웹 포트 값:', webPort); // 디버깅용
try {
const response = await fetch(`${this.apiBase}/settings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(settings)
});
console.log('응답 상태:', response.status); // 디버깅용
if (!response.ok) {
const errorText = await response.text();
throw new Error(`설정 저장에 실패했습니다. 상태: ${response.status}, 응답: ${errorText}`);
}
const result = await response.json();
console.log('저장 결과:', result); // 디버깅용
this.showSuccess(result.message);
this.hideSettingsModal();
this.checkVNCStatus(); // VNC 상태 다시 확인
this.loadServerList(); // 서버 목록 다시 로드 (사용자 필터 적용)
} catch (error) {
console.error('설정 저장 오류:', error); // 디버깅용
this.showError('설정 저장 중 오류가 발생했습니다: ' + error.message);
}
}
browseVNCViewer() {
// C#의 OpenFileDialog 호출
if (window.chrome && window.chrome.webview) {
// WebView2 환경에서 C# 메서드 호출
window.chrome.webview.postMessage('OPEN_FILE_DIALOG');
} else {
// 일반 브라우저에서는 수동 입력 안내
this.showError('WebView2 환경에서만 파일 선택이 가능합니다. 경로를 수동으로 입력해주세요.');
}
}
// C#에서 호출하는 함수
receiveFilePath(filePath) {
document.getElementById('vncViewerPath').value = filePath;
this.showSuccess('VNC Viewer 경로가 설정되었습니다.');
}
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,
title: document.getElementById('serverTitle').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;
}
// 간단한 템플릿 엔진
renderTemplate(templateId, data) {
const template = document.getElementById(templateId);
if (!template) {
console.error(`Template not found: ${templateId}`);
return '';
}
let html = template.innerHTML;
// 변수 치환
Object.keys(data).forEach(key => {
const regex = new RegExp(`{{${key}}}`, 'g');
html = html.replace(regex, this.escapeHtml(data[key] || ''));
});
// 조건부 렌더링 ({{#if variable}}...{{/if}})
html = html.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, variable, content) => {
return data[variable] ? content : '';
});
return html;
}
renderCategoryNode(categoryName, categoryData, depth) {
const indent = depth * 20; // 들여쓰기
const hasSubcategories = Object.keys(categoryData.subcategories).length > 0;
const hasServers = categoryData.servers.length > 0;
const totalItems = categoryData.servers.length + Object.keys(categoryData.subcategories).length;
const headerData = {
categoryName: categoryName,
depth: depth,
bgClass: depth === 0 ? 'bg-gray-50' : 'bg-gray-25',
hasContent: hasSubcategories || hasServers,
totalItems: totalItems
};
let html = `
${this.renderTemplate('categoryHeaderTemplate', headerData)}
`;
// 서버 목록 렌더링
if (hasServers) {
categoryData.servers.forEach(server => {
const serverData = {
serverName: `${server.User || server.user}@${server.Title || server.title}`,
userName: server.User || server.user,
serverIP: server.IP || server.ip,
serverCategory: server.Category || server.category,
serverTitle: server.Title || server.title,
serverDescription: server.Description || server.description,
serverArgument: server.Argument || server.argument
};
html += this.renderTemplate('serverItemTemplate', serverData);
});
}
// 하위 카테고리 렌더링
const sortedSubcategories = Object.keys(categoryData.subcategories).sort();
sortedSubcategories.forEach(subcategoryName => {
html += this.renderCategoryNode(subcategoryName, categoryData.subcategories[subcategoryName], depth + 1);
});
html += `
`;
return html;
}
toggleCategory(category, depth = 0) {
const content = document.getElementById(`category-${category}-${depth}`);
const icon = content.previousElementSibling.querySelector('.category-icon');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.style.transform = 'rotate(0deg)';
} else {
content.style.display = 'none';
icon.style.transform = 'rotate(-90deg)';
}
}
}
// 앱 초기화
const app = new VNCServerApp();
// C#에서 호출할 수 있도록 전역 함수로 등록
window.receiveFilePath = function(filePath) {
app.receiveFilePath(filePath);
};