212 lines
7.4 KiB
TypeScript
212 lines
7.4 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { Search, X, Users, Check } from 'lucide-react';
|
|
import { comms } from '@/communication';
|
|
import { GroupUser } from '@/types';
|
|
|
|
interface UserSearchDialogProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSelect: (user: GroupUser) => void;
|
|
title?: string;
|
|
excludeUsers?: string[]; // 제외할 사용자 ID 목록
|
|
initialSearchKey?: string; // 초기 검색어
|
|
}
|
|
|
|
export function UserSearchDialog({
|
|
isOpen,
|
|
onClose,
|
|
onSelect,
|
|
title = '사용자 검색',
|
|
excludeUsers = [],
|
|
initialSearchKey = '',
|
|
}: UserSearchDialogProps) {
|
|
const [users, setUsers] = useState<GroupUser[]>([]);
|
|
const [filteredUsers, setFilteredUsers] = useState<GroupUser[]>([]);
|
|
const [searchKey, setSearchKey] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [selectedUser, setSelectedUser] = useState<GroupUser | null>(null);
|
|
|
|
// 사용자 목록 로드
|
|
const loadUsers = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const result = await comms.getUserList('%');
|
|
if (Array.isArray(result)) {
|
|
// 제외 목록에 없고, 계정 사용 중이고, 퇴사하지 않은 사용자만 표시
|
|
const filtered = result.filter(
|
|
(u: GroupUser) =>
|
|
u.useUserState &&
|
|
!excludeUsers.includes(u.id) &&
|
|
!u.outdate // 퇴사일이 없는 사용자만 (재직 중)
|
|
);
|
|
setUsers(filtered);
|
|
setFilteredUsers(filtered);
|
|
}
|
|
} catch (error) {
|
|
console.error('사용자 목록 로드 실패:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [excludeUsers]);
|
|
|
|
// 다이얼로그 열릴 때 데이터 로드
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
loadUsers();
|
|
setSearchKey(initialSearchKey); // 초기 검색어 설정
|
|
setSelectedUser(null);
|
|
}
|
|
}, [isOpen, loadUsers, initialSearchKey]);
|
|
|
|
// 검색 필터링
|
|
useEffect(() => {
|
|
if (!searchKey.trim()) {
|
|
setFilteredUsers(users);
|
|
} else {
|
|
const key = searchKey.toLowerCase();
|
|
setFilteredUsers(
|
|
users.filter(
|
|
(u) =>
|
|
u.id.toLowerCase().includes(key) ||
|
|
(u.name || '').toLowerCase().includes(key) ||
|
|
(u.email || '').toLowerCase().includes(key)
|
|
)
|
|
);
|
|
}
|
|
}, [searchKey, users]);
|
|
|
|
// 선택 확정
|
|
const handleConfirm = () => {
|
|
if (selectedUser) {
|
|
onSelect(selectedUser);
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
// ESC 키로 닫기
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && isOpen) {
|
|
onClose();
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [isOpen, onClose]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
>
|
|
<div
|
|
className="dialog-container rounded-xl w-full max-w-lg max-h-[80vh] overflow-hidden flex flex-col transition-all duration-300"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* 헤더 */}
|
|
<div className="dialog-header p-4 flex items-center justify-between shrink-0">
|
|
<div className="flex items-center gap-2">
|
|
<Users className="w-5 h-5 text-primary-400" />
|
|
<h2 className="dialog-title">{title}</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-white/60 hover:text-white transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 검색 */}
|
|
<div className="p-4 border-b border-white/10 shrink-0">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/50" />
|
|
<input
|
|
type="text"
|
|
value={searchKey}
|
|
onChange={(e) => setSearchKey(e.target.value)}
|
|
placeholder="사번, 이름, 이메일로 검색..."
|
|
autoFocus
|
|
className="w-full pl-10 pr-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 사용자 목록 */}
|
|
<div className="flex-1 overflow-y-auto p-2">
|
|
{loading ? (
|
|
<div className="text-center py-8">
|
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary-400 mx-auto mb-2"></div>
|
|
<p className="text-white/50">로딩 중...</p>
|
|
</div>
|
|
) : filteredUsers.length === 0 ? (
|
|
<div className="text-center py-8 text-white/50">
|
|
{searchKey ? '검색 결과가 없습니다' : '사용자가 없습니다'}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{filteredUsers.map((user) => (
|
|
<button
|
|
key={user.id}
|
|
onClick={() => setSelectedUser(user)}
|
|
onDoubleClick={() => {
|
|
setSelectedUser(user);
|
|
onSelect(user);
|
|
onClose();
|
|
}}
|
|
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${selectedUser?.id === user.id
|
|
? 'bg-primary-500/30 border border-primary-400/50'
|
|
: 'bg-white/5 hover:bg-white/10 border border-transparent'
|
|
}`}
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-white/80 text-sm">{user.id}</span>
|
|
<span className="font-medium text-white">{user.name}</span>
|
|
{user.grade && (
|
|
<span className="text-xs text-white/50 bg-white/10 px-1.5 py-0.5 rounded">
|
|
{user.grade}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-white/50 mt-0.5 truncate">
|
|
{user.email || '-'} {user.processs && `| ${user.processs}`}
|
|
</div>
|
|
</div>
|
|
{selectedUser?.id === user.id && (
|
|
<Check className="w-5 h-5 text-primary-400 shrink-0" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 푸터 */}
|
|
<div className="dialog-footer p-4 flex items-center justify-between shrink-0">
|
|
<span className="text-sm text-white/50">
|
|
{filteredUsers.length}명 {selectedUser && `| 선택: ${selectedUser.id} (${selectedUser.name})`}
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white transition-colors"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
onClick={handleConfirm}
|
|
disabled={!selectedUser}
|
|
className="px-4 py-2 bg-primary-500 hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg text-white transition-colors"
|
|
>
|
|
선택
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|