add search

This commit is contained in:
backuppc
2025-07-09 09:06:30 +09:00
parent 18b2a20806
commit 33a22e0ac7
2 changed files with 215 additions and 21 deletions

View File

@@ -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>

View File

@@ -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, '\\$&');
}
}