feat(tkuser): 사용자 목록 검색 + 부서 필터 기능 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-25 08:15:33 +09:00
parent f09aa0875a
commit a40c1e0f18
2 changed files with 61 additions and 7 deletions

View File

@@ -149,7 +149,16 @@
<!-- 사용자 목록 (넓게) -->
<div class="lg:col-span-3 bg-white rounded-xl shadow-sm p-5">
<h2 class="text-base font-semibold text-gray-800 mb-4"><i class="fas fa-users text-slate-400 mr-2"></i>사용자 목록</h2>
<h2 class="text-base font-semibold text-gray-800 mb-3">
<i class="fas fa-users text-slate-400 mr-2"></i>사용자 목록
<span id="activeUserCount" class="text-xs font-normal text-gray-400 ml-1"></span>
</h2>
<div class="flex gap-2 mb-3">
<input type="text" id="userSearchInput" class="input-field flex-1 px-3 py-1.5 rounded-lg text-sm" placeholder="이름/아이디 검색">
<select id="userDeptFilter" class="input-field px-2 py-1.5 rounded-lg text-sm">
<option value="">전체 부서</option>
</select>
</div>
<div id="userList" class="space-y-2 max-h-[420px] overflow-y-auto">
<div class="text-gray-400 text-center py-8"><i class="fas fa-spinner fa-spin text-2xl"></i><p class="mt-2 text-sm">로딩 중...</p></div>
</div>
@@ -2351,7 +2360,7 @@
<!-- JS: Tabs -->
<script src="/static/js/tkuser-tabs.js?v=2026032301"></script>
<!-- JS: Individual modules -->
<script src="/static/js/tkuser-users.js?v=2026032303"></script>
<script src="/static/js/tkuser-users.js?v=2026032501"></script>
<script src="/static/js/tkuser-projects.js?v=2026031401"></script>
<script src="/static/js/tkuser-departments.js?v=2026032302"></script>
<script src="/static/js/tkuser-issue-types.js?v=2026031401"></script>

View File

@@ -112,7 +112,7 @@ async function loadUsers() {
try {
const r = await api('/users'); users = r.data || r;
displayUsers(); updatePermissionUserSelect();
populateUserDeptSelects();
populateUserDeptSelects(); populateUserDeptFilter();
const hireDateInput = document.getElementById('newHireDate');
if (hireDateInput && !hireDateInput.value) hireDateInput.value = getSeoulToday();
} catch (err) {
@@ -129,6 +129,15 @@ function populateUserDeptSelects() {
sel.value = val;
});
}
// 필터용 부서 셀렉트 (populateUserDeptSelects는 모달 폼용)
function populateUserDeptFilter() {
const sel = document.getElementById('userDeptFilter');
if (!sel) return;
const val = sel.value;
sel.innerHTML = '<option value="">전체 부서</option>';
departmentsCache.forEach(d => { const o = document.createElement('option'); o.value = d.department_id; o.textContent = d.department_name; sel.appendChild(o); });
sel.value = val;
}
function renderUserRow(u, isResigned) {
return `<div class="flex items-center justify-between p-2.5 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="flex-1 min-w-0">
@@ -151,12 +160,38 @@ function renderUserRow(u, isResigned) {
}
function displayUsers() {
const activeUsers = users.filter(u => u.is_active !== 0 && u.is_active !== false);
const resignedUsers = users.filter(u => u.is_active === 0 || u.is_active === false);
const searchTerm = (document.getElementById('userSearchInput')?.value || '').trim().toLowerCase();
const deptFilterVal = document.getElementById('userDeptFilter')?.value || '';
let filtered = users;
if (searchTerm) {
filtered = filtered.filter(u => {
const name = (u.name || '').toLowerCase();
const username = (u.username || '').toLowerCase();
return name.includes(searchTerm) || username.includes(searchTerm);
});
}
if (deptFilterVal) {
const deptId = parseInt(deptFilterVal);
filtered = filtered.filter(u => u.department_id === deptId);
}
const activeUsers = filtered.filter(u => u.is_active !== 0 && u.is_active !== false);
const resignedUsers = filtered.filter(u => u.is_active === 0 || u.is_active === false);
const c = document.getElementById('userList');
if (!activeUsers.length) { c.innerHTML = '<p class="text-gray-400 text-center py-4 text-sm">등록된 사용자가 없습니다.</p>'; }
else { c.innerHTML = activeUsers.map(u => renderUserRow(u, false)).join(''); }
const countEl = document.getElementById('activeUserCount');
if (countEl) countEl.textContent = `(${activeUsers.length}명)`;
if (!activeUsers.length) {
c.innerHTML = searchTerm || deptFilterVal
? '<p class="text-gray-400 text-center py-4 text-sm">검색 결과가 없습니다.</p>'
: '<p class="text-gray-400 text-center py-4 text-sm">등록된 사용자가 없습니다.</p>';
} else {
c.innerHTML = activeUsers.map(u => renderUserRow(u, false)).join('');
}
const resignedSection = document.getElementById('resignedSection');
const resignedList = document.getElementById('resignedUserList');
@@ -419,6 +454,16 @@ function populateDeptPermSelect() {
}
document.addEventListener('DOMContentLoaded', () => {
// 사용자 검색 + 부서 필터
let userSearchTimeout;
const userSearchEl = document.getElementById('userSearchInput');
if (userSearchEl) userSearchEl.addEventListener('input', () => {
clearTimeout(userSearchTimeout);
userSearchTimeout = setTimeout(displayUsers, 300);
});
const userDeptFilterEl = document.getElementById('userDeptFilter');
if (userDeptFilterEl) userDeptFilterEl.addEventListener('change', displayUsers);
const sel = document.getElementById('deptPermSelect');
if (sel) sel.addEventListener('change', async e => {
selectedDeptId = e.target.value;