902 lines
36 KiB
JavaScript
902 lines
36 KiB
JavaScript
class VNCServerApp {
|
|
constructor() {
|
|
this.apiBase = '/api/vncserver';
|
|
this.allServers = []; // 모든 서버 데이터 저장
|
|
this.filteredServers = []; // 필터링된 서버 데이터
|
|
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();
|
|
});
|
|
|
|
// 아이콘 관련 이벤트
|
|
document.getElementById('selectIconBtn').addEventListener('click', () => {
|
|
document.getElementById('iconFile').click();
|
|
});
|
|
|
|
document.getElementById('iconFile').addEventListener('change', (e) => {
|
|
this.handleIconFileSelect(e);
|
|
});
|
|
|
|
document.getElementById('removeIconBtn').addEventListener('click', () => {
|
|
this.removeIconPreview();
|
|
});
|
|
|
|
// 검색 관련 이벤트
|
|
document.getElementById('searchInput').addEventListener('input', (e) => {
|
|
this.handleSearch(e.target.value);
|
|
});
|
|
|
|
document.getElementById('clearSearchBtn').addEventListener('click', () => {
|
|
document.getElementById('searchInput').value = '';
|
|
this.handleSearch('');
|
|
});
|
|
|
|
document.getElementById('categoryFilter').addEventListener('change', (e) => {
|
|
this.handleCategoryFilter(e.target.value);
|
|
});
|
|
|
|
document.getElementById('clearFiltersBtn').addEventListener('click', () => {
|
|
this.clearAllFilters();
|
|
});
|
|
}
|
|
|
|
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.allServers = servers;
|
|
this.filteredServers = [...servers];
|
|
|
|
// 카테고리 필터 옵션 업데이트
|
|
this.updateCategoryFilter();
|
|
|
|
// 서버 목록 렌더링
|
|
this.renderServerList(this.filteredServers);
|
|
} catch (error) {
|
|
this.showError('서버 목록을 불러오는 중 오류가 발생했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
renderServerList(servers) {
|
|
const serverList = document.getElementById('serverList');
|
|
|
|
console.log('서버 데이터:', servers); // 디버깅용
|
|
|
|
if (servers.length === 0) {
|
|
// 검색 필터가 적용된 상태인지 확인
|
|
const searchTerm = document.getElementById('searchInput').value.trim();
|
|
const selectedCategory = document.getElementById('categoryFilter').value;
|
|
|
|
if (searchTerm || selectedCategory) {
|
|
// 검색 결과가 없는 경우
|
|
serverList.innerHTML = this.renderTemplate('emptySearchResultTemplate', {});
|
|
} else {
|
|
// 서버가 아예 없는 경우
|
|
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 = `
|
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
<div class="flex items-center justify-between">
|
|
<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>
|
|
<button onclick="app.showSettingsModal()" class="text-red-600 hover:text-red-800 text-sm underline">설정</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
handleIconFileSelect(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
// 파일 크기 체크 (1MB)
|
|
if (file.size > 1024 * 1024) {
|
|
this.showError('파일 크기는 1MB를 초과할 수 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 이미지 크기 체크 (클라이언트 측에서 미리 확인)
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
if (img.width > 128 || img.height > 128) {
|
|
this.showSuccess('이미지가 128x128로 자동 리사이즈됩니다.');
|
|
}
|
|
URL.revokeObjectURL(img.src); // 메모리 정리
|
|
};
|
|
img.src = URL.createObjectURL(file);
|
|
|
|
// 파일 타입 체크
|
|
if (!file.type.startsWith('image/')) {
|
|
this.showError('이미지 파일만 선택할 수 있습니다.');
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const iconPreview = document.getElementById('iconPreview');
|
|
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
|
const removeIconBtn = document.getElementById('removeIconBtn');
|
|
|
|
iconPreview.src = e.target.result;
|
|
iconPreview.classList.remove('hidden');
|
|
iconPlaceholder.classList.add('hidden');
|
|
removeIconBtn.classList.remove('hidden');
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
removeIconPreview() {
|
|
const iconPreview = document.getElementById('iconPreview');
|
|
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
|
const removeIconBtn = document.getElementById('removeIconBtn');
|
|
const iconFile = document.getElementById('iconFile');
|
|
|
|
iconPreview.src = '';
|
|
iconPreview.classList.add('hidden');
|
|
iconPlaceholder.classList.remove('hidden');
|
|
removeIconBtn.classList.add('hidden');
|
|
iconFile.value = '';
|
|
}
|
|
|
|
showServerModal(server = null) {
|
|
const modal = document.getElementById('serverModal');
|
|
const modalTitle = document.getElementById('modalTitle');
|
|
const form = document.getElementById('serverForm');
|
|
|
|
// 폼 초기화
|
|
form.reset();
|
|
this.removeIconPreview();
|
|
|
|
if (server) {
|
|
// 편집 모드
|
|
modalTitle.textContent = '서버 편집';
|
|
document.getElementById('serverIp').value = server.IP;
|
|
document.getElementById('serverPassword').value = server.Password || '';
|
|
document.getElementById('serverTitle').value = server.Title || '';
|
|
document.getElementById('serverCategory').value = server.Category || '';
|
|
document.getElementById('serverDescription').value = server.Description || '';
|
|
document.getElementById('serverArgument').value = server.Argument || '';
|
|
|
|
// 아이콘 미리보기 설정
|
|
if (server.Icon) {
|
|
const iconPreview = document.getElementById('iconPreview');
|
|
const iconPlaceholder = document.getElementById('iconPlaceholder');
|
|
const removeIconBtn = document.getElementById('removeIconBtn');
|
|
|
|
iconPreview.src = `${this.apiBase}/icon/${encodeURIComponent(server.User)}/${encodeURIComponent(server.IP)}`;
|
|
iconPreview.classList.remove('hidden');
|
|
iconPlaceholder.classList.add('hidden');
|
|
removeIconBtn.classList.remove('hidden');
|
|
}
|
|
|
|
form.dataset.editMode = 'true';
|
|
form.dataset.originalUser = server.User;
|
|
form.dataset.originalIp = server.IP;
|
|
} else {
|
|
// 추가 모드
|
|
modalTitle.textContent = '서버 추가';
|
|
delete form.dataset.editMode;
|
|
delete form.dataset.originalUser;
|
|
delete form.dataset.originalIp;
|
|
}
|
|
|
|
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() {
|
|
try {
|
|
const form = document.getElementById('serverForm');
|
|
const userName = document.getElementById('userName')?.value || '';
|
|
|
|
if (!userName) {
|
|
this.showError('사용자 이름을 먼저 설정해주세요.');
|
|
return;
|
|
}
|
|
|
|
const serverData = {
|
|
User: userName,
|
|
IP: document.getElementById('serverIp').value,
|
|
Title: document.getElementById('serverTitle').value,
|
|
Category: document.getElementById('serverCategory').value,
|
|
Description: document.getElementById('serverDescription').value,
|
|
Password: document.getElementById('serverPassword').value,
|
|
Argument: document.getElementById('serverArgument').value
|
|
};
|
|
|
|
// 아이콘 파일이 선택된 경우 Base64로 변환
|
|
const iconFile = document.getElementById('iconFile');
|
|
if (iconFile.files.length > 0) {
|
|
const file = iconFile.files[0];
|
|
|
|
// 파일을 Base64로 변환하는 Promise
|
|
const base64Promise = new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
try {
|
|
// Base64 데이터에서 헤더 제거 (data:image/png;base64, 부분)
|
|
const base64Data = e.target.result.split(',')[1];
|
|
resolve(base64Data);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
try {
|
|
const base64Data = await base64Promise;
|
|
serverData.IconBase64 = base64Data;
|
|
} catch (error) {
|
|
this.showError('아이콘 파일 처리 중 오류가 발생했습니다: ' + error.message);
|
|
return;
|
|
}
|
|
}
|
|
|
|
await this.submitServerData(serverData, form);
|
|
} catch (error) {
|
|
this.showError('서버 저장 중 오류가 발생했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async submitServerData(serverData, form) {
|
|
try {
|
|
let response;
|
|
const isEditMode = form.dataset.editMode === 'true';
|
|
|
|
if (isEditMode) {
|
|
// 편집 모드
|
|
const originalUser = form.dataset.originalUser;
|
|
const originalIp = form.dataset.originalIp;
|
|
response = await fetch(`${this.apiBase}/update/${encodeURIComponent(originalUser)}/${encodeURIComponent(originalIp)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(serverData)
|
|
});
|
|
} else {
|
|
// 추가 모드
|
|
response = await fetch(`${this.apiBase}/add`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(serverData)
|
|
});
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.Message || '서버 저장에 실패했습니다.');
|
|
}
|
|
|
|
this.showSuccess(isEditMode ? '서버가 성공적으로 수정되었습니다.' : '서버가 성공적으로 추가되었습니다.');
|
|
this.hideServerModal();
|
|
this.loadServerList(); // 서버 목록 새로고침 (검색 필터도 함께 업데이트)
|
|
} catch (error) {
|
|
this.showError('서버 저장 중 오류가 발생했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async editServer(user, ip) {
|
|
console.log('편집 서버 호출:', { 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();
|
|
console.log('서버 정보:', server); // 디버깅용
|
|
this.showServerModal(server);
|
|
} catch (error) {
|
|
console.error('편집 서버 오류:', 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('서버 삭제에 실패했습니다.');
|
|
|
|
this.showSuccess('서버가 성공적으로 삭제되었습니다.');
|
|
this.hideConfirmModal();
|
|
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 = ' '.repeat(depth);
|
|
let html = '';
|
|
|
|
// 카테고리 헤더
|
|
if (depth === 0) {
|
|
html += `<div class="category-header bg-gray-100 px-4 py-2 border-b border-gray-200">
|
|
<div class="flex items-center justify-between cursor-pointer" onclick="app.toggleCategory('${this.escapeHtml(categoryName)}', ${depth})">
|
|
<h3 class="text-lg font-semibold text-gray-800 flex items-center">
|
|
<svg class="w-5 h-5 mr-2 text-gray-600 category-icon transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
|
</svg>
|
|
${this.escapeHtml(categoryName)}
|
|
</h3>
|
|
<span class="text-sm text-gray-500">${categoryData.servers.length}개 서버</span>
|
|
</div>
|
|
</div>
|
|
<div id="category-${this.escapeHtml(categoryName)}-${depth}" class="category-content">`;
|
|
} else {
|
|
html += `<div class="subcategory-header bg-gray-50 px-4 py-2 border-b border-gray-200 ml-${depth * 4}">
|
|
<div class="flex items-center justify-between cursor-pointer" onclick="app.toggleCategory('${this.escapeHtml(categoryName)}', ${depth})">
|
|
<h4 class="text-md font-medium text-gray-700 flex items-center">
|
|
<svg class="w-4 h-4 mr-2 text-gray-500 category-icon transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
|
</svg>
|
|
${this.escapeHtml(categoryName)}
|
|
</h4>
|
|
<span class="text-sm text-gray-500">${categoryData.servers.length}개 서버</span>
|
|
</div>
|
|
</div>
|
|
<div id="category-${this.escapeHtml(categoryName)}-${depth}" class="category-content">`;
|
|
}
|
|
|
|
// 서버 목록
|
|
if (categoryData.servers.length > 0) {
|
|
categoryData.servers.forEach(server => {
|
|
const serverName = server.Title || server.title || `${server.User || server.user} - ${server.IP || server.ip}`;
|
|
const serverIP = server.IP || server.ip;
|
|
const userName = server.User || server.user;
|
|
const serverCategory = server.Category || server.category;
|
|
const serverDescription = server.Description || server.description;
|
|
const serverArgument = server.Argument || server.argument;
|
|
|
|
// 아이콘 URL 생성
|
|
const iconUrl = `${this.apiBase}/icon/${encodeURIComponent(userName)}/${encodeURIComponent(serverIP)}`;
|
|
|
|
// depth에 따라 margin-left 클래스 적용 (최대 ml-24)
|
|
const marginClass = `ml-${Math.min(depth * 4, 24)}`;
|
|
|
|
const serverData = {
|
|
userName: userName,
|
|
serverIP: serverIP,
|
|
serverName: this.escapeHtml(serverName),
|
|
serverCategory: this.escapeHtml(serverCategory),
|
|
serverDescription: this.escapeHtml(serverDescription),
|
|
serverArgument: this.escapeHtml(serverArgument),
|
|
iconUrl: iconUrl,
|
|
marginClass: marginClass
|
|
};
|
|
|
|
html += this.renderTemplate('serverItemTemplate', serverData);
|
|
});
|
|
}
|
|
|
|
// 하위 카테고리
|
|
const subcategories = Object.keys(categoryData.subcategories).sort();
|
|
subcategories.forEach(subcategory => {
|
|
html += this.renderCategoryNode(subcategory, categoryData.subcategories[subcategory], depth + 1);
|
|
});
|
|
|
|
html += '</div>'; // category-content 닫기
|
|
|
|
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)';
|
|
}
|
|
}
|
|
|
|
updateCategoryFilter() {
|
|
const categoryFilter = document.getElementById('categoryFilter');
|
|
const categories = new Set();
|
|
|
|
// 모든 서버에서 카테고리 수집
|
|
this.allServers.forEach(server => {
|
|
const category = server.Category || server.category;
|
|
if (category && category.trim()) {
|
|
// | 기호로 구분된 카테고리들을 분리
|
|
const categoryParts = category.split('|').map(cat => cat.trim()).filter(cat => cat.length > 0);
|
|
categoryParts.forEach(cat => categories.add(cat));
|
|
}
|
|
});
|
|
|
|
// 기존 옵션 제거 (첫 번째 "모든 카테고리" 옵션 제외)
|
|
while (categoryFilter.children.length > 1) {
|
|
categoryFilter.removeChild(categoryFilter.lastChild);
|
|
}
|
|
|
|
// 카테고리 옵션 추가
|
|
Array.from(categories).sort().forEach(category => {
|
|
const option = document.createElement('option');
|
|
option.value = category;
|
|
option.textContent = category;
|
|
categoryFilter.appendChild(option);
|
|
});
|
|
}
|
|
|
|
handleSearch(searchTerm) {
|
|
const clearSearchBtn = document.getElementById('clearSearchBtn');
|
|
|
|
// 검색어가 있으면 X 버튼 표시
|
|
if (searchTerm.trim()) {
|
|
clearSearchBtn.classList.remove('hidden');
|
|
} else {
|
|
clearSearchBtn.classList.add('hidden');
|
|
}
|
|
|
|
this.applyFilters();
|
|
}
|
|
|
|
handleCategoryFilter(selectedCategory) {
|
|
this.applyFilters();
|
|
}
|
|
|
|
clearAllFilters() {
|
|
document.getElementById('searchInput').value = '';
|
|
document.getElementById('categoryFilter').value = '';
|
|
document.getElementById('clearSearchBtn').classList.add('hidden');
|
|
this.applyFilters();
|
|
}
|
|
|
|
applyFilters() {
|
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
|
|
const selectedCategory = document.getElementById('categoryFilter').value;
|
|
|
|
// 필터링 적용
|
|
this.filteredServers = this.allServers.filter(server => {
|
|
// 검색어 필터
|
|
if (searchTerm) {
|
|
const searchFields = [
|
|
server.Title || server.title || '',
|
|
server.IP || server.ip || '',
|
|
server.Category || server.category || '',
|
|
server.Description || server.description || ''
|
|
].map(field => field.toLowerCase());
|
|
|
|
const matchesSearch = searchFields.some(field => field.includes(searchTerm));
|
|
if (!matchesSearch) return false;
|
|
}
|
|
|
|
// 카테고리 필터
|
|
if (selectedCategory) {
|
|
const serverCategory = server.Category || server.category || '';
|
|
const categoryParts = serverCategory.split('|').map(cat => cat.trim());
|
|
const matchesCategory = categoryParts.includes(selectedCategory);
|
|
if (!matchesCategory) return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// 검색 결과 정보 업데이트
|
|
this.updateSearchInfo();
|
|
|
|
// 필터링된 서버 목록 렌더링
|
|
this.renderServerList(this.filteredServers);
|
|
}
|
|
|
|
updateSearchInfo() {
|
|
const searchInfo = document.getElementById('searchInfo');
|
|
const searchResultCount = document.getElementById('searchResultCount');
|
|
const searchTerm = document.getElementById('searchInput').value.trim();
|
|
const selectedCategory = document.getElementById('categoryFilter').value;
|
|
|
|
if (searchTerm || selectedCategory) {
|
|
searchResultCount.textContent = this.filteredServers.length;
|
|
searchInfo.classList.remove('hidden');
|
|
} else {
|
|
searchInfo.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
escapeRegex(string) {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
|
|
}
|
|
|
|
// 앱 초기화
|
|
const app = new VNCServerApp();
|
|
|
|
// C#에서 호출할 수 있도록 전역 함수로 등록
|
|
window.receiveFilePath = function(filePath) {
|
|
app.receiveFilePath(filePath);
|
|
};
|