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); };