add search
This commit is contained in:
@@ -37,21 +37,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 툴바 -->
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<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 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>
|
||||
|
||||
@@ -271,6 +306,19 @@
|
||||
</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>
|
||||
@@ -1,6 +1,8 @@
|
||||
class VNCServerApp {
|
||||
constructor() {
|
||||
this.apiBase = '/api/vncserver';
|
||||
this.allServers = []; // 모든 서버 데이터 저장
|
||||
this.filteredServers = []; // 필터링된 서버 데이터
|
||||
this.init();
|
||||
|
||||
// 개발용: Ctrl+R로 강제 새로고침
|
||||
@@ -84,6 +86,24 @@ class VNCServerApp {
|
||||
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() {
|
||||
@@ -104,7 +124,16 @@ class VNCServerApp {
|
||||
if (!response.ok) throw new Error('서버 목록을 불러올 수 없습니다.');
|
||||
|
||||
const servers = await response.json();
|
||||
this.renderServerList(servers);
|
||||
|
||||
// 모든 서버 데이터 저장
|
||||
this.allServers = servers;
|
||||
this.filteredServers = [...servers];
|
||||
|
||||
// 카테고리 필터 옵션 업데이트
|
||||
this.updateCategoryFilter();
|
||||
|
||||
// 서버 목록 렌더링
|
||||
this.renderServerList(this.filteredServers);
|
||||
} catch (error) {
|
||||
this.showError('서버 목록을 불러오는 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
@@ -116,7 +145,17 @@ class VNCServerApp {
|
||||
console.log('서버 데이터:', servers); // 디버깅용
|
||||
|
||||
if (servers.length === 0) {
|
||||
serverList.innerHTML = this.renderTemplate('emptyServerListTemplate', {});
|
||||
// 검색 필터가 적용된 상태인지 확인
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -505,7 +544,7 @@ class VNCServerApp {
|
||||
|
||||
this.showSuccess(isEditMode ? '서버가 성공적으로 수정되었습니다.' : '서버가 성공적으로 추가되었습니다.');
|
||||
this.hideServerModal();
|
||||
this.loadServerList();
|
||||
this.loadServerList(); // 서버 목록 새로고침 (검색 필터도 함께 업데이트)
|
||||
} catch (error) {
|
||||
this.showError('서버 저장 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
@@ -541,9 +580,9 @@ class VNCServerApp {
|
||||
|
||||
if (!response.ok) throw new Error('서버 삭제에 실패했습니다.');
|
||||
|
||||
const result = await response.json();
|
||||
this.showSuccess(result.message);
|
||||
this.loadServerList();
|
||||
this.showSuccess('서버가 성공적으로 삭제되었습니다.');
|
||||
this.hideConfirmModal();
|
||||
this.loadServerList(); // 서버 목록 새로고침 (검색 필터도 함께 업데이트)
|
||||
} catch (error) {
|
||||
this.showError('서버 삭제 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
@@ -723,6 +762,113 @@ class VNCServerApp {
|
||||
}
|
||||
}
|
||||
|
||||
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, '\\$&');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user