diff --git a/TestMasterUI/src/App.jsx b/TestMasterUI/src/App.jsx index 75da808..66b93ec 100644 --- a/TestMasterUI/src/App.jsx +++ b/TestMasterUI/src/App.jsx @@ -31,6 +31,7 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { showMockRouteEdit: false, showTCPServerEdit: false, showQueueEdit: false, + showUserEdit: false, selectedItem: null, }); @@ -59,6 +60,10 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { case 'queue-edit': setDialogState(prev => ({ ...prev, showQueueEdit: true , selectedItem: item})); break; + case 'user-add': + case 'user-edit': + setDialogState(prev => ({ ...prev, showUserEdit: true , selectedItem: item})); + break; case 'socket-add': case 'socket-edit': setDialogState(prev => ({ ...prev, showTCPServerEdit: true , selectedItem: item})); @@ -132,6 +137,7 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { showMockRouteEdit={dialogState.showMockRouteEdit} showTCPServerEdit={dialogState.showTCPServerEdit} showQueueEdit={dialogState.showQueueEdit} + showUserEdit={dialogState.showUserEdit} selectedItem={dialogState.selectedItem} onDialogChange={handleDialogChange} /> diff --git a/TestMasterUI/src/components/shared/DataTable.jsx b/TestMasterUI/src/components/shared/DataTable.jsx index f3c90ed..1f33b1b 100644 --- a/TestMasterUI/src/components/shared/DataTable.jsx +++ b/TestMasterUI/src/components/shared/DataTable.jsx @@ -18,6 +18,7 @@ const DataTable = ({ // Data and columns data, columns, + idField = 'id', // New prop to specify which field to use as ID // Selection selectable = false, @@ -39,14 +40,19 @@ const DataTable = ({ }) => { const [showDeleteDialog, setShowDeleteDialog] = React.useState(false); + // Get ID from row using the specified idField + const getRowId = React.useCallback((row) => { + return row[idField]; + }, [idField]); + const handleSelectAll = React.useCallback((checked) => { if (!onSelectIds) return; if (checked) { - onSelectIds(data.content.map(item => item.id || item.queueName)); + onSelectIds(data.content.map(item => getRowId(item))); } else { onSelectIds([]); } - }, [data.content, onSelectIds]); + }, [data.content, onSelectIds, getRowId]); const handleSelectItem = React.useCallback((id, checked) => { if (!onSelectIds) return; @@ -90,14 +96,14 @@ const DataTable = ({ )} {columns.map((column) => ( - {column.label} + {column.label} ))} {renderRowActions && Actions} {data.content.map((row) => { - const rowId = row.id || row.queueName; + const rowId = getRowId(row); return ( {selectable && ( @@ -109,7 +115,7 @@ const DataTable = ({ )} {columns.map((column) => ( - + {column.render ? column.render(row[column.key], row) : row[column.key]} ))} diff --git a/TestMasterUI/src/components/shared/DialogManager.jsx b/TestMasterUI/src/components/shared/DialogManager.jsx index 0a9b663..c2282da 100644 --- a/TestMasterUI/src/components/shared/DialogManager.jsx +++ b/TestMasterUI/src/components/shared/DialogManager.jsx @@ -6,6 +6,7 @@ import VariableModal from '@/components/api-tester/VariableModal.jsx'; import MockRouteEditDialog from '@/components/mock-route/MockRouteEditDialog.jsx'; import TCPServerEditDialog from '@/components/tcp-servers/TCPServerEditDialog.jsx'; import QueueEditDialog from '@/components/queues/QueueEditDialog.jsx'; +import UserEditDialog from '@/components/users/UserEditDialog.jsx'; const DialogManager = ({ showAddApi, @@ -15,6 +16,7 @@ const DialogManager = ({ showMockRouteEdit, showTCPServerEdit, showQueueEdit, + showUserEdit, selectedItem, onDialogChange }) => { @@ -51,6 +53,11 @@ const DialogManager = ({ onOpenChange={(isOpen) => onDialogChange('showQueueEdit', isOpen)} item={selectedItem} /> + onDialogChange('showUserEdit', isOpen)} + item={selectedItem} + /> ; } diff --git a/TestMasterUI/src/components/users/UserEditDialog.jsx b/TestMasterUI/src/components/users/UserEditDialog.jsx index e69de29..b5edfdf 100644 --- a/TestMasterUI/src/components/users/UserEditDialog.jsx +++ b/TestMasterUI/src/components/users/UserEditDialog.jsx @@ -0,0 +1,311 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { useAPI } from '@/providers/APIProvider'; +import { ScrollArea } from '@/components/ui/scroll-area'; + +// User status enum mapped to descriptions +const USER_STATUS = { + WAITING: '가입승인 대기', + ACTIVE: '활성화', + RESET_PASSWORD: '비밀번호 초기화', + INACTIVE: '비활성화', + LOCKED: '잠김', + DELETED: '삭제' +}; + +const DEFAULT_USER = { + esntlId: null, + userId: '', + password: '', + password2: '', + username: '', + userSecurity: 'ROLE_USER', + mobilePhone: '', + status: 'WAITING', // Changed default to WAITING as per business logic + lockAt: 'N', + lockCnt: 0, + createdAt: null, + lastLockDate: null, + lastPasswordChangeDate: null +}; + +const UserEditDialog = ({ open, onOpenChange, item }) => { + const { createUser, updateUser, error } = useAPI(); + const [user, setUser] = useState(DEFAULT_USER); + const [validationErrors, setValidationErrors] = useState({}); + + useEffect(() => { + if (item) { + setUser(item); + } else { + setUser(DEFAULT_USER); + } + }, [item]); + + const validateForm = () => { + const errors = {}; + + if (!user.userId) errors.userId = '사용자 ID는 필수입니다.'; + if (!user.username) errors.username = '이름은 필수입니다.'; + if (!user.mobilePhone) errors.mobilePhone = '연락처는 필수입니다.'; + + if (!user.esntlId) { // Only validate passwords for new users + if (!user.password) errors.password = '비밀번호는 필수입니다.'; + if (user.password && user.password.length < 8) { + errors.password = '비밀번호는 8자 이상이어야 합니다.'; + } + if (user.password !== user.password2) { + errors.password2 = '비밀번호가 일치하지 않습니다.'; + } + } + + setValidationErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + try { + // Create a clean copy of the user data for API submission + const userData = { + ...user, + createdAt: user.createdAt || undefined, + lastLockDate: user.lastLockDate || undefined, + lastPasswordChangeDate: user.lastPasswordChangeDate || undefined + }; + + if (user.esntlId) { + await updateUser(user.esntlId, userData); + } else { + await createUser(userData); + } + onOpenChange(false); + } catch (error) { + console.error('Failed to save user:', error); + } + }; + + const handleChange = (field, value) => { + setUser(prev => ({ ...prev, [field]: value })); + // Clear validation error when field is modified + if (validationErrors[field]) { + setValidationErrors(prev => ({ ...prev, [field]: undefined })); + } + }; + + return ( + + + + + {user.esntlId == null ? '내부 사용자 등록' : '내부 사용자 수정'} + + + 사용자 정보를 입력하거나 수정할 수 있습니다. 필수 항목을 모두 입력해주세요. + + {error && ( +
+ {error} +
+ )} +
+ + +
+ {user.esntlId && ( +
+ + +
+ )} + +
+ + handleChange('userId', e.target.value)} + disabled={!!user.esntlId} + required + className={validationErrors.userId ? 'border-red-500' : ''} + /> + {validationErrors.userId && ( +
{validationErrors.userId}
+ )} +
+ + {!user.esntlId && ( + <> +
+ + handleChange('password', e.target.value)} + required + className={validationErrors.password ? 'border-red-500' : ''} + /> + {validationErrors.password && ( +
{validationErrors.password}
+ )} +
+ +
+ + handleChange('password2', e.target.value)} + required + className={validationErrors.password2 ? 'border-red-500' : ''} + /> + {validationErrors.password2 && ( +
{validationErrors.password2}
+ )} +
+ + )} + +
+ + handleChange('username', e.target.value)} + required + className={validationErrors.username ? 'border-red-500' : ''} + /> + {validationErrors.username && ( +
{validationErrors.username}
+ )} +
+ +
+ + +
+ +
+ + handleChange('mobilePhone', e.target.value)} + required + className={validationErrors.mobilePhone ? 'border-red-500' : ''} + /> + {validationErrors.mobilePhone && ( +
{validationErrors.mobilePhone}
+ )} +
+ + {user.esntlId && ( + <> +
+ + +
+ +
+ + +
+ +
+ + +
+ + {user.createdAt && ( +
+ + +
+ )} + + {user.lastLockDate && ( +
+ + +
+ )} + + {user.lastPasswordChangeDate && ( +
+ + +
+ )} + + )} +
+
+ + + + + +
+
+ ); +}; + +export default UserEditDialog; diff --git a/TestMasterUI/src/components/users/UserList.jsx b/TestMasterUI/src/components/users/UserList.jsx index e69de29..090962f 100644 --- a/TestMasterUI/src/components/users/UserList.jsx +++ b/TestMasterUI/src/components/users/UserList.jsx @@ -0,0 +1,150 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import DataTable from '@/components/shared/DataTable'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { useAPI } from '@/providers/APIProvider'; + +const UserList = ({ onMenuClick }) => { + const { + users, + loadUsers, + deleteUsers, + loading, + error + } = useAPI(); + + const [searchTerm, setSearchTerm] = useState(''); + const [selectedIds, setSelectedIds] = useState([]); + const [page, setPage] = useState(0); + + useEffect(() => { + loadUsers({ page }); + }, [page, loadUsers]); + + const handleSearch = () => { + setPage(0); + loadUsers({ username: searchTerm, page: 0 }); + }; + + const handleDelete = async () => { + if (!selectedIds.length) return; + + const success = await deleteUsers(selectedIds); + if (success) { + setSelectedIds([]); + } + }; + + const handlePasswordReset = async (userId) => { + onMenuClick('user-password-reset', null, { esntlId: userId }); + }; + + const columns = [ + { key: 'esntlId', label: 'ID' }, + { key: 'username', label: '이름' }, + { + key: 'status', + label: '상태', + render: (value) => ( + + {value} + + ) + }, + { + key: 'userSecurity', + label: '권한', + render: (value) => ( + + {value === 'ROLE_ADMIN' ? '관리자' : '사용자'} + + ) + } + ]; + + const renderRowActions = (row) => ( +
+ + +
+ ); + + const tableActions = ( + <> + + + + + ); + + if (error) { + return ( +
+ Error loading users: {error} +
+ ); + } + + return ( +
+ + + 내부 사용자 목록 + + +
+
+ setSearchTerm(e.target.value)} + onKeyUp={(e) => { + if (e.key === 'Enter') { + handleSearch(); + } + }} + /> +
+ {tableActions} +
+ +
+
+
+ ); +}; + +export default UserList; diff --git a/TestMasterUI/src/config/menuConfig.js b/TestMasterUI/src/config/menuConfig.js index 68914d7..3114398 100644 --- a/TestMasterUI/src/config/menuConfig.js +++ b/TestMasterUI/src/config/menuConfig.js @@ -46,7 +46,7 @@ export const menuConfig = [ id: 'system', label: '시스템 관리', items: [ - {id: 'user-management', label: '사용자 관리', action: 'user-management', screen: 'user-management'}, + {id: 'user-list', label: '사용자 관리', action: 'user-list', screen: 'user-list'}, {type: 'separator'}, {id: 'server-management', label: '서버 관리', action: 'server-management', screen: 'server-management'}, {id: 'server-add', label: '서버 추가', action: 'server-add', screen: 'server-add'}, diff --git a/TestMasterUI/src/config/screenConfig.js b/TestMasterUI/src/config/screenConfig.js index ec557ed..9f30999 100644 --- a/TestMasterUI/src/config/screenConfig.js +++ b/TestMasterUI/src/config/screenConfig.js @@ -4,6 +4,7 @@ import LoadTestManager from '../components/load-tester/LoadTestManager.jsx'; import MockRouteList from '../components/mock-route/MockRouteList.jsx'; import TCPServerList from '@/components/tcp-servers/TCPServerList.jsx'; import QueueList from '@/components/queues/QueueList.jsx'; +import UserList from '@/components/users/UserList.jsx'; export const SCREENS = { 'api-tester': APITester, @@ -11,4 +12,5 @@ export const SCREENS = { 'response-list': MockRouteList, 'socket-list': TCPServerList, 'queue-list': QueueList, + 'user-list': UserList, }; diff --git a/TestMasterUI/src/providers/APIProvider.jsx b/TestMasterUI/src/providers/APIProvider.jsx index bc22ae5..5ed379d 100644 --- a/TestMasterUI/src/providers/APIProvider.jsx +++ b/TestMasterUI/src/providers/APIProvider.jsx @@ -5,6 +5,7 @@ import {serverService} from '@/services/serverService'; import {authService} from '@/services/authService'; import APIClient from '@/services/APIClient.js'; import {queueService} from '@/services/queueService.js'; +import {userService} from '@/services/userService.js'; const APIContext = createContext(null); @@ -43,6 +44,14 @@ export const APIProvider = ({ last: true }); + const [users, setUsers] = useState({ + content: [], + totalPages: 0, + number: 0, + first: true, + last: true + }); + // Initialize services const collections$ = collectionService(contextPath); @@ -50,6 +59,7 @@ export const APIProvider = ({ const servers$ = serverService(); const auth$ = authService(); const queues$ = queueService(contextPath); + const users$ = userService(contextPath); const handleLogout = useCallback(async () => { try { @@ -363,6 +373,79 @@ export const APIProvider = ({ } }, [loadQueues]); + const loadUsers = useCallback(async (params = { username: '', page: 0 }) => { + setLoading(true); + setError(null); + try { + const data = await users$.findAll(params); + setUsers(data); + return data; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, []); + + const deleteUsers = useCallback(async (ids) => { + setLoading(true); + setError(null); + try { + await users$.deleteUsers(ids); + await loadUsers(); + return true; + } catch (err) { + setError(err.message); + return false; + } finally { + setLoading(false); + } + }, [loadUsers]); + + const createUser = useCallback(async (user) => { + setLoading(true); + setError(null); + try { + const newUser = await users$.create(user); + await loadUsers(); + return newUser; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, [loadUsers]); + + const updateUser = useCallback(async (userId, user) => { + setLoading(true); + setError(null); + try { + const updatedUser = await users$.update(userId, user); + await loadUsers(); + return updatedUser; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, [loadUsers]); + + const resetPassword = useCallback(async (userId, passwordData) => { + setLoading(true); + setError(null); + try { + await users$.resetPassword(userId, passwordData); + return true; + } catch (err) { + setError(err.message); + return false; + } finally { + setLoading(false); + } + }, []); const contextValue = { apiClient, @@ -393,7 +476,13 @@ export const APIProvider = ({ loadQueues, deleteQueues, startQueue, - stopQueue + stopQueue, + users, + loadUsers, + deleteUsers, + createUser, + updateUser, + resetPassword }; return ( diff --git a/TestMasterUI/src/services/userService.js b/TestMasterUI/src/services/userService.js new file mode 100644 index 0000000..f23b614 --- /dev/null +++ b/TestMasterUI/src/services/userService.js @@ -0,0 +1,64 @@ +import { getCSRFToken } from '@/services/token.js'; + +export const userService = (contextPath = '/') => { + const headers = { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': getCSRFToken() + }; + + return { + async findAll(params = { page: 0 }) { + const searchParams = new URLSearchParams({ + page: params.page, + size: 10, + ...(params.username && { username: params.username }) + }); + + const response = await fetch(`${contextPath}api/users?${searchParams}`); + if (!response.ok) throw new Error('Failed to fetch users'); + return await response.json(); + }, + + async create(user) { + const response = await fetch(`${contextPath}api/users`, { + method: 'POST', + headers, + body: JSON.stringify(user) + }); + + if (!response.ok) throw new Error('Failed to create user'); + return await response.json(); + }, + + async update(userId, user) { + const response = await fetch(`${contextPath}api/users/update/${userId}`, { + method: 'POST', + headers, + body: JSON.stringify(user) + }); + + if (!response.ok) throw new Error('Failed to update user'); + return await response.json(); + }, + + async deleteUsers(ids) { + const response = await fetch(`${contextPath}api/users/delete`, { + method: 'POST', + headers, + body: JSON.stringify(ids) + }); + + if (!response.ok) throw new Error('Failed to delete users'); + }, + + async resetPassword(userId, passwordData) { + const response = await fetch(`${contextPath}api/users/reset-password/${userId}`, { + method: 'POST', + headers, + body: JSON.stringify(passwordData) + }); + + if (!response.ok) throw new Error('Failed to reset password'); + } + }; +}; diff --git a/src/main/java/com/eactive/testmaster/user/dto/UserRegisterDTO.java b/src/main/java/com/eactive/testmaster/user/dto/UserRegisterDTO.java index c5b075b..9da5f1c 100644 --- a/src/main/java/com/eactive/testmaster/user/dto/UserRegisterDTO.java +++ b/src/main/java/com/eactive/testmaster/user/dto/UserRegisterDTO.java @@ -39,7 +39,7 @@ public class UserRegisterDTO implements Serializable { /** * 사용자상태코드 */ - private UserStatus status; + private UserStatus status = UserStatus.ACTIVE; /** * 이동전화번호