feat(tkuser): 사용자 목록 검색 + 부서 필터 기능 추가
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user