324 lines
23 KiB
HTML
324 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
<meta http-equiv="Pragma" content="no-cache">
|
|
<meta http-equiv="Expires" content="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-2">
|
|
|
|
<!-- 상태 표시 -->
|
|
<div id="status" class="mb-4"></div>
|
|
|
|
<!-- 서버 목록 -->
|
|
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-4">
|
|
<div id="serverList">
|
|
<!-- 서버 목록이 여기에 동적으로 로드됩니다 -->
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- 검색 및 필터 -->
|
|
<div class="bg-white rounded-lg shadow-md p-4 mb-4">
|
|
<div class="flex flex-col md:flex-row gap-4">
|
|
<!-- 검색 입력창 -->
|
|
<div class="flex-1">
|
|
<div class="relative">
|
|
<input type="text" id="searchInput" placeholder="서버 검색 (제목, IP, 카테고리, 설명)"
|
|
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
<svg class="absolute left-3 top-2.5 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
<button id="clearSearchBtn" class="absolute right-3 top-2.5 text-gray-400 hover:text-gray-600 hidden">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 필터 옵션 -->
|
|
<div class="flex gap-2">
|
|
<select id="categoryFilter" class="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
<option value="">모든 카테고리</option>
|
|
</select>
|
|
<button id="clearFiltersBtn" class="px-4 py-2 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 transition duration-200">
|
|
필터 초기화
|
|
</button>
|
|
<!-- 서버 추가 버튼 -->
|
|
<button id="addServerBtn" class="bg-primary hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition duration-200 shadow-sm">
|
|
<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>
|
|
<!-- 설정 버튼 -->
|
|
<button id="settingsBtn" class="bg-secondary hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition duration-200 shadow-sm">
|
|
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
</svg>
|
|
설정
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 검색 결과 정보 -->
|
|
<div id="searchInfo" class="mt-3 text-sm text-gray-600 hidden">
|
|
<span id="searchResultCount">0</span>개의 서버가 검색되었습니다.
|
|
</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 class="block text-sm font-medium text-gray-700 mb-2">서버 아이콘</label>
|
|
<div class="flex items-center space-x-4">
|
|
<div class="w-16 h-16 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center bg-gray-50">
|
|
<img id="iconPreview" src="" alt="" class="w-12 h-12 object-contain hidden">
|
|
<svg id="iconPlaceholder" class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<input type="file" id="iconFile" accept="image/*" class="hidden">
|
|
<button type="button" id="selectIconBtn" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
아이콘 선택
|
|
</button>
|
|
<button type="button" id="removeIconBtn" class="w-full mt-1 px-3 py-1 text-xs text-red-600 hover:text-red-800 focus:outline-none hidden">
|
|
아이콘 제거
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-1">PNG, JPG, GIF 형식 지원 (최대 1MB, 128x128 자동 리사이즈)</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<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>
|
|
<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>
|
|
<div class="mb-4">
|
|
<label for="serverTitle" class="block text-sm font-medium text-gray-700 mb-2">제목</label>
|
|
<input type="text" id="serverTitle" 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="grid grid-cols-2 gap-4 mb-4">
|
|
<div >
|
|
<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>
|
|
<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>
|
|
|
|
|
|
<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="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="settingsModal" 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-lg w-full">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-semibold text-gray-800">설정</h3>
|
|
</div>
|
|
<form id="settingsForm" class="p-6">
|
|
<div class="mb-4">
|
|
<label for="userName" class="block text-sm font-medium text-gray-700 mb-2">사용자 이름 <span class="text-red-500">*</span></label>
|
|
<input type="text" id="userName" 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" placeholder="사용자 이름을 입력하세요" required>
|
|
<p class="text-sm text-gray-500 mt-1">현재 사용자의 이름을 설정하세요. (필수)</p>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="vncViewerPath" class="block text-sm font-medium text-gray-700 mb-2">VNC Viewer 경로</label>
|
|
<div class="flex space-x-2">
|
|
<input type="text" id="vncViewerPath" class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="C:\Program Files\TightVNC\tvnviewer.exe">
|
|
<button type="button" id="browseVncBtn" class="px-4 py-2 bg-secondary text-white rounded-md hover:bg-gray-600 transition duration-200">찾아보기</button>
|
|
</div>
|
|
<p class="text-sm text-gray-500 mt-1">VNC Viewer 실행 파일의 경로를 설정하세요.</p>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="vncArgument" class="block text-sm font-medium text-gray-700 mb-2">VNC 기본 인수</label>
|
|
<input type="text" id="vncArgument" 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" placeholder="-useclipboard=no -scale=auto -showcontrols=yes">
|
|
<p class="text-sm text-gray-500 mt-1">VNC 연결 시 사용할 기본 인수를 설정하세요.</p>
|
|
</div>
|
|
<div class="mb-6">
|
|
<label for="webServerPort" class="block text-sm font-medium text-gray-700 mb-2">웹 서버 포트</label>
|
|
<input type="number" id="webServerPort" value="8080" 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">
|
|
<p class="text-sm text-gray-500 mt-1">내장 웹 서버의 포트 번호를 설정하세요.</p>
|
|
</div>
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" id="settingsCancelBtn" 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>
|
|
|
|
<!-- 숨겨진 템플릿들 -->
|
|
<template id="serverItemTemplate">
|
|
<div class="server-item px-4 py-3 hover:bg-gray-50 transition duration-200 border-b border-gray-100 relative overflow-hidden group {{marginClass}}">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex-1 cursor-pointer" onclick="app.connectToServer('{{userName}}', '{{serverIP}}')">
|
|
<div class="flex items-center space-x-3">
|
|
<!-- 서버 아이콘 -->
|
|
<div class="w-8 h-8 flex-shrink-0">
|
|
<img id="icon-{{userName}}-{{serverIP}}" src="{{iconUrl}}" alt="" class="w-8 h-8 object-contain rounded" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
|
<div class="w-8 h-8 bg-gray-200 rounded flex items-center justify-center" style="display: none;">
|
|
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
|
<h4 class="text-lg font-medium text-gray-900 hover:text-blue-600 transition duration-200">{{serverName}}</h4>
|
|
<span class="text-xs text-gray-400 opacity-0 group-hover:opacity-100 transition duration-200">클릭하여 연결</span>
|
|
</div>
|
|
<div class="mt-2 text-sm text-gray-600">
|
|
<p><span class="font-medium">IP:</span> {{serverIP}}</p>
|
|
{{#if serverCategory}}<p class="mt-1"><span class="font-medium">카테고리:</span> {{serverCategory}}</p>{{/if}}
|
|
{{#if serverDescription}}<p class="mt-1"><span class="font-medium">설명:</span> {{serverDescription}}</p>{{/if}}
|
|
{{#if serverArgument}}<p class="mt-1"><span class="font-medium">인수:</span> {{serverArgument}}</p>{{/if}}
|
|
</div>
|
|
</div>
|
|
<div class="action-buttons flex space-x-2 transform translate-x-full transition-transform duration-300 ease-in-out group-hover:translate-x-0">
|
|
<button onclick="app.connectToServer('{{userName}}', '{{serverIP}}')" class="bg-primary hover:bg-blue-700 text-white px-3 py-1 rounded text-sm transition duration-200 shadow-sm">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
|
</svg>
|
|
</button>
|
|
<button onclick="app.editServer('{{userName}}', '{{serverIP}}')" class="bg-secondary hover:bg-gray-600 text-white px-3 py-1 rounded text-sm transition duration-200 shadow-sm">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
</svg>
|
|
</button>
|
|
<button onclick="app.deleteServer('{{userName}}', '{{serverIP}}', '{{serverName}}')" class="bg-danger hover:bg-red-700 text-white px-3 py-1 rounded text-sm transition duration-200 shadow-sm">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template id="categoryHeaderTemplate">
|
|
<div class="category-header px-4 py-2 {{bgClass}} border-b border-gray-200 cursor-pointer hover:bg-gray-100 transition duration-200" onclick="app.toggleCategory('{{categoryName}}', {{depth}})">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-2">
|
|
{{#if hasContent}}
|
|
<svg class="w-4 h-4 text-gray-500 category-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
{{/if}}
|
|
<h3 class="font-medium text-gray-700">{{categoryName}}</h3>
|
|
<span class="text-sm text-gray-500">({{totalItems}})</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template id="emptyServerListTemplate">
|
|
<div class="px-4 py-6 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>
|
|
</template>
|
|
|
|
<template id="userNameRequiredTemplate">
|
|
<div class="px-4 py-6 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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
</svg>
|
|
<p class="text-lg font-medium mb-2">사용자 설정이 필요합니다</p>
|
|
<p class="mb-4">서버 목록을 보려면 먼저 사용자 이름을 설정해주세요.</p>
|
|
<button onclick="app.showSettingsModal()" class="bg-primary hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition duration-200">
|
|
설정하기
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<template id="emptySearchResultTemplate">
|
|
<div class="px-4 py-8 text-center">
|
|
<svg class="w-16 h-16 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">검색 결과가 없습니다</h3>
|
|
<p class="text-gray-600 mb-4">검색 조건을 변경하거나 필터를 초기화해보세요.</p>
|
|
<button onclick="app.clearAllFilters()" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-blue-700 transition duration-200">
|
|
필터 초기화
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<script src="js/app.js?v=1.0"></script>
|
|
</body>
|
|
</html> |