"use client"; import React, { useState, useEffect } from "react"; import { UserProfile, CreateUserRequest, UpdateUserRequest, UserListFilters, UserListSort, PaginationParams, UserListResponse, UserStats, BulkUserOperation, Role, } from "./types"; // 사용자 목록 컴포넌트 interface UserListProps { onUserSelect?: (user: UserProfile) => void; onUserEdit?: (user: UserProfile) => void; onUserDelete?: (user: UserProfile) => void; filters?: UserListFilters; showActions?: boolean; } export function UserList({ onUserSelect, onUserEdit, onUserDelete, filters = {}, showActions = true, }: UserListProps) { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [pagination, setPagination] = useState({ page: 1, limit: 20, }); const [sort, setSort] = useState({ field: "createdAt", direction: "desc", }); const [selectedUsers, setSelectedUsers] = useState([]); useEffect(() => { fetchUsers(); }, [filters, pagination, sort]); const fetchUsers = async () => { try { setLoading(true); const queryParams = new URLSearchParams({ page: pagination.page.toString(), limit: pagination.limit.toString(), sortField: sort.field, sortDirection: sort.direction, ...Object.fromEntries( Object.entries(filters).filter(([, value]) => value !== undefined) ), }); const response = await fetch(`/api/users?${queryParams}`); if (!response.ok) { throw new Error("사용자 목록을 불러오는데 실패했습니다"); } const data: UserListResponse = await response.json(); setUsers(data.users); } catch (err) { setError( err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다" ); } finally { setLoading(false); } }; const handleSort = (field: keyof UserProfile) => { setSort((prev) => ({ field: field as UserListSort["field"], direction: prev.field === field && prev.direction === "asc" ? "desc" : "asc", })); }; const handleSelectUser = (userId: string) => { setSelectedUsers((prev) => prev.includes(userId) ? prev.filter((id) => id !== userId) : [...prev, userId] ); }; const handleSelectAll = () => { setSelectedUsers( selectedUsers.length === users.length ? [] : users.map((user) => user.id) ); }; const getRoleColor = (role: Role) => { switch (role) { case Role.ADMIN: return "bg-red-100 text-red-800"; case Role.EDITOR: return "bg-blue-100 text-blue-800"; case Role.VIEWER: return "bg-green-100 text-green-800"; case Role.GUEST: return "bg-gray-100 text-gray-800"; default: return "bg-gray-100 text-gray-800"; } }; const getStatusColor = (isActive: boolean) => { return isActive ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"; }; if (loading) { return (
); } if (error) { return (

오류 발생

{error}

); } return (
{/* 헤더 */}

사용자 목록

총 {users.length}명의 사용자

{selectedUsers.length > 0 && (
{ setSelectedUsers([]); fetchUsers(); }} />
)}
{/* 테이블 */}
{showActions && ( )} {users.map((user) => ( {showActions && ( )} ))}
0 } onChange={handleSelectAll} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" /> handleSort("name")} > 이름 {sort.field === "name" && ( {sort.direction === "asc" ? "↑" : "↓"} )} handleSort("email")} > 이메일 {sort.field === "email" && ( {sort.direction === "asc" ? "↑" : "↓"} )} 역할 상태 handleSort("lastLogin")} > 마지막 로그인 {sort.field === "lastLogin" && ( {sort.direction === "asc" ? "↑" : "↓"} )} 작업
handleSelectUser(user.id)} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" /> onUserSelect?.(user)} >
{user.name.charAt(0).toUpperCase()}
{user.name}
{user.tenant && (
{user.tenant.name}
)}
{user.email}
{user.emailVerified ? ( 인증됨 ) : ( 미인증 )}
{user.role} {user.isActive ? "활성" : "비활성"} {user.accountLockedUntil && new Date(user.accountLockedUntil) > new Date() && (
잠김
)}
{user.lastLogin ? new Date(user.lastLogin).toLocaleDateString("ko-KR") : "없음"}
{/* 페이지네이션 */}
페이지 {pagination.page}
); } // 사용자 생성/편집 폼 컴포넌트 interface UserFormProps { user?: UserProfile; onSubmit: (data: CreateUserRequest | UpdateUserRequest) => Promise; onCancel: () => void; loading?: boolean; } export function UserForm({ user, onSubmit, onCancel, loading = false, }: UserFormProps) { const [formData, setFormData] = useState({ name: user?.name || "", email: user?.email || "", password: "", role: user?.role || Role.VIEWER, isActive: user?.isActive ?? true, tenantId: user?.tenantId || "", sendVerificationEmail: true, }); const [errors, setErrors] = useState>({}); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setErrors({}); try { if (user) { // 편집 모드 const updateData: UpdateUserRequest = { name: formData.name, email: formData.email, role: formData.role, isActive: formData.isActive, tenantId: formData.tenantId || undefined, }; await onSubmit(updateData); } else { // 생성 모드 const createData: CreateUserRequest = { name: formData.name, email: formData.email, password: formData.password, role: formData.role, tenantId: formData.tenantId || undefined, sendVerificationEmail: formData.sendVerificationEmail, }; await onSubmit(createData); } } catch (error) { if (error instanceof Error) { setErrors({ general: error.message }); } } }; const handleChange = (field: string, value: string | boolean) => { setFormData((prev) => ({ ...prev, [field]: value })); if (errors[field]) { setErrors((prev) => ({ ...prev, [field]: "" })); } }; return (

{user ? "사용자 편집" : "새 사용자 생성"}

{errors.general && (
{errors.general}
)}
handleChange("name", e.target.value)} className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" required /> {errors.name && (

{errors.name}

)}
handleChange("email", e.target.value)} className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" required /> {errors.email && (

{errors.email}

)}
{!user && (
handleChange("password", e.target.value)} className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" required minLength={8} /> {errors.password && (

{errors.password}

)}
)}
handleChange("isActive", e.target.checked)} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
{!user && (
handleChange("sendVerificationEmail", e.target.checked) } className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
)}
); } // 대량 작업 버튼 컴포넌트 interface BulkActionButtonProps { selectedUsers: string[]; onComplete: () => void; } function BulkActionButton({ selectedUsers, onComplete, }: BulkActionButtonProps) { const [showMenu, setShowMenu] = useState(false); const [loading, setLoading] = useState(false); const handleBulkOperation = async ( operation: BulkUserOperation["operation"], data?: { role?: Role } ) => { try { setLoading(true); const response = await fetch("/api/users/bulk", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ userIds: selectedUsers, operation, data, }), }); if (!response.ok) { throw new Error("대량 작업에 실패했습니다"); } onComplete(); setShowMenu(false); } catch (error) { console.error("대량 작업 오류:", error); } finally { setLoading(false); } }; return (
{showMenu && (

)}
); } // 사용자 통계 컴포넌트 interface UserStatsProps { stats?: UserStats; loading?: boolean; } export function UserStatsComponent({ stats, loading = false }: UserStatsProps) { if (loading) { return (
{[...Array(4)].map((_, i) => (
))}
); } if (!stats) return null; const statItems = [ { label: "전체 사용자", value: stats.totalUsers, color: "text-blue-600" }, { label: "활성 사용자", value: stats.activeUsers, color: "text-green-600" }, { label: "인증된 사용자", value: stats.verifiedUsers, color: "text-purple-600", }, { label: "잠긴 계정", value: stats.lockedUsers, color: "text-red-600" }, ]; return (
{statItems.map((item, index) => (
{item.value.toLocaleString()}
{item.label}
))}
); }