사용자 관리
This commit is contained in:
@@ -31,6 +31,7 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
|
|||||||
showMockRouteEdit: false,
|
showMockRouteEdit: false,
|
||||||
showTCPServerEdit: false,
|
showTCPServerEdit: false,
|
||||||
showQueueEdit: false,
|
showQueueEdit: false,
|
||||||
|
showUserEdit: false,
|
||||||
selectedItem: null,
|
selectedItem: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,6 +60,10 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
|
|||||||
case 'queue-edit':
|
case 'queue-edit':
|
||||||
setDialogState(prev => ({ ...prev, showQueueEdit: true , selectedItem: item}));
|
setDialogState(prev => ({ ...prev, showQueueEdit: true , selectedItem: item}));
|
||||||
break;
|
break;
|
||||||
|
case 'user-add':
|
||||||
|
case 'user-edit':
|
||||||
|
setDialogState(prev => ({ ...prev, showUserEdit: true , selectedItem: item}));
|
||||||
|
break;
|
||||||
case 'socket-add':
|
case 'socket-add':
|
||||||
case 'socket-edit':
|
case 'socket-edit':
|
||||||
setDialogState(prev => ({ ...prev, showTCPServerEdit: true , selectedItem: item}));
|
setDialogState(prev => ({ ...prev, showTCPServerEdit: true , selectedItem: item}));
|
||||||
@@ -132,6 +137,7 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
|
|||||||
showMockRouteEdit={dialogState.showMockRouteEdit}
|
showMockRouteEdit={dialogState.showMockRouteEdit}
|
||||||
showTCPServerEdit={dialogState.showTCPServerEdit}
|
showTCPServerEdit={dialogState.showTCPServerEdit}
|
||||||
showQueueEdit={dialogState.showQueueEdit}
|
showQueueEdit={dialogState.showQueueEdit}
|
||||||
|
showUserEdit={dialogState.showUserEdit}
|
||||||
selectedItem={dialogState.selectedItem}
|
selectedItem={dialogState.selectedItem}
|
||||||
onDialogChange={handleDialogChange}
|
onDialogChange={handleDialogChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const DataTable = ({
|
|||||||
// Data and columns
|
// Data and columns
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
|
idField = 'id', // New prop to specify which field to use as ID
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
selectable = false,
|
selectable = false,
|
||||||
@@ -39,14 +40,19 @@ const DataTable = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = React.useState(false);
|
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) => {
|
const handleSelectAll = React.useCallback((checked) => {
|
||||||
if (!onSelectIds) return;
|
if (!onSelectIds) return;
|
||||||
if (checked) {
|
if (checked) {
|
||||||
onSelectIds(data.content.map(item => item.id || item.queueName));
|
onSelectIds(data.content.map(item => getRowId(item)));
|
||||||
} else {
|
} else {
|
||||||
onSelectIds([]);
|
onSelectIds([]);
|
||||||
}
|
}
|
||||||
}, [data.content, onSelectIds]);
|
}, [data.content, onSelectIds, getRowId]);
|
||||||
|
|
||||||
const handleSelectItem = React.useCallback((id, checked) => {
|
const handleSelectItem = React.useCallback((id, checked) => {
|
||||||
if (!onSelectIds) return;
|
if (!onSelectIds) return;
|
||||||
@@ -90,14 +96,14 @@ const DataTable = ({
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
)}
|
)}
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableHead key={column.id || column.key}>{column.label}</TableHead>
|
<TableHead key={column.key}>{column.label}</TableHead>
|
||||||
))}
|
))}
|
||||||
{renderRowActions && <TableHead className="text-center">Actions</TableHead>}
|
{renderRowActions && <TableHead className="text-center">Actions</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.content.map((row) => {
|
{data.content.map((row) => {
|
||||||
const rowId = row.id || row.queueName;
|
const rowId = getRowId(row);
|
||||||
return (
|
return (
|
||||||
<TableRow key={rowId}>
|
<TableRow key={rowId}>
|
||||||
{selectable && (
|
{selectable && (
|
||||||
@@ -109,7 +115,7 @@ const DataTable = ({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableCell key={`${rowId}-${column.id || column.key}`}>
|
<TableCell key={`${rowId}-${column.key}`}>
|
||||||
{column.render ? column.render(row[column.key], row) : row[column.key]}
|
{column.render ? column.render(row[column.key], row) : row[column.key]}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import VariableModal from '@/components/api-tester/VariableModal.jsx';
|
|||||||
import MockRouteEditDialog from '@/components/mock-route/MockRouteEditDialog.jsx';
|
import MockRouteEditDialog from '@/components/mock-route/MockRouteEditDialog.jsx';
|
||||||
import TCPServerEditDialog from '@/components/tcp-servers/TCPServerEditDialog.jsx';
|
import TCPServerEditDialog from '@/components/tcp-servers/TCPServerEditDialog.jsx';
|
||||||
import QueueEditDialog from '@/components/queues/QueueEditDialog.jsx';
|
import QueueEditDialog from '@/components/queues/QueueEditDialog.jsx';
|
||||||
|
import UserEditDialog from '@/components/users/UserEditDialog.jsx';
|
||||||
|
|
||||||
const DialogManager = ({
|
const DialogManager = ({
|
||||||
showAddApi,
|
showAddApi,
|
||||||
@@ -15,6 +16,7 @@ const DialogManager = ({
|
|||||||
showMockRouteEdit,
|
showMockRouteEdit,
|
||||||
showTCPServerEdit,
|
showTCPServerEdit,
|
||||||
showQueueEdit,
|
showQueueEdit,
|
||||||
|
showUserEdit,
|
||||||
selectedItem,
|
selectedItem,
|
||||||
onDialogChange
|
onDialogChange
|
||||||
}) => {
|
}) => {
|
||||||
@@ -51,6 +53,11 @@ const DialogManager = ({
|
|||||||
onOpenChange={(isOpen) => onDialogChange('showQueueEdit', isOpen)}
|
onOpenChange={(isOpen) => onDialogChange('showQueueEdit', isOpen)}
|
||||||
item={selectedItem}
|
item={selectedItem}
|
||||||
/>
|
/>
|
||||||
|
<UserEditDialog
|
||||||
|
open={showUserEdit}
|
||||||
|
onOpenChange={(isOpen) => onDialogChange('showUserEdit', isOpen)}
|
||||||
|
item={selectedItem}
|
||||||
|
/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{user.esntlId == null ? '내부 사용자 등록' : '내부 사용자 수정'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
사용자 정보를 입력하거나 수정할 수 있습니다. 필수 항목을 모두 입력해주세요.
|
||||||
|
</DialogDescription>
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-500 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<ScrollArea className="max-h-[calc(90vh-200px)]">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4 p-1">
|
||||||
|
{user.esntlId && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>고유ID</Label>
|
||||||
|
<Input value={user.esntlId} disabled />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="userId" className="must">사용자 ID</Label>
|
||||||
|
<Input
|
||||||
|
id="userId"
|
||||||
|
value={user.userId}
|
||||||
|
onChange={(e) => handleChange('userId', e.target.value)}
|
||||||
|
disabled={!!user.esntlId}
|
||||||
|
required
|
||||||
|
className={validationErrors.userId ? 'border-red-500' : ''}
|
||||||
|
/>
|
||||||
|
{validationErrors.userId && (
|
||||||
|
<div className="text-red-500 text-sm">{validationErrors.userId}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!user.esntlId && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password" className="must">비밀번호</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={user.password}
|
||||||
|
onChange={(e) => handleChange('password', e.target.value)}
|
||||||
|
required
|
||||||
|
className={validationErrors.password ? 'border-red-500' : ''}
|
||||||
|
/>
|
||||||
|
{validationErrors.password && (
|
||||||
|
<div className="text-red-500 text-sm">{validationErrors.password}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password2" className="must">비밀번호 확인</Label>
|
||||||
|
<Input
|
||||||
|
id="password2"
|
||||||
|
type="password"
|
||||||
|
value={user.password2}
|
||||||
|
onChange={(e) => handleChange('password2', e.target.value)}
|
||||||
|
required
|
||||||
|
className={validationErrors.password2 ? 'border-red-500' : ''}
|
||||||
|
/>
|
||||||
|
{validationErrors.password2 && (
|
||||||
|
<div className="text-red-500 text-sm">{validationErrors.password2}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="username" className="must">이름</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
value={user.username}
|
||||||
|
onChange={(e) => handleChange('username', e.target.value)}
|
||||||
|
required
|
||||||
|
className={validationErrors.username ? 'border-red-500' : ''}
|
||||||
|
/>
|
||||||
|
{validationErrors.username && (
|
||||||
|
<div className="text-red-500 text-sm">{validationErrors.username}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="must">권한</Label>
|
||||||
|
<Select
|
||||||
|
value={user.userSecurity}
|
||||||
|
onValueChange={(value) => handleChange('userSecurity', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="권한 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ROLE_ADMIN">관리자</SelectItem>
|
||||||
|
<SelectItem value="ROLE_USER">사용자</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="mobilePhone" className="must">연락처</Label>
|
||||||
|
<Input
|
||||||
|
id="mobilePhone"
|
||||||
|
value={user.mobilePhone}
|
||||||
|
onChange={(e) => handleChange('mobilePhone', e.target.value)}
|
||||||
|
required
|
||||||
|
className={validationErrors.mobilePhone ? 'border-red-500' : ''}
|
||||||
|
/>
|
||||||
|
{validationErrors.mobilePhone && (
|
||||||
|
<div className="text-red-500 text-sm">{validationErrors.mobilePhone}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user.esntlId && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="must">상태</Label>
|
||||||
|
<Select
|
||||||
|
value={user.status}
|
||||||
|
onValueChange={(value) => handleChange('status', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="상태 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.entries(USER_STATUS).map(([value, label]) => (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="must">잠금여부</Label>
|
||||||
|
<Select
|
||||||
|
value={user.lockAt}
|
||||||
|
onValueChange={(value) => handleChange('lockAt', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="잠금여부 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="N">해제</SelectItem>
|
||||||
|
<SelectItem value="Y">잠금</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>잠금 횟수</Label>
|
||||||
|
<Input value={user.lockCnt} disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user.createdAt && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>등록 일시</Label>
|
||||||
|
<Input value={user.createdAt} disabled />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{user.lastLockDate && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>잠금최종시점</Label>
|
||||||
|
<Input value={user.lastLockDate} disabled />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{user.lastPasswordChangeDate && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>비밀번호 변경일자</Label>
|
||||||
|
<Input value={user.lastPasswordChangeDate} disabled />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<DialogFooter className="flex justify-end space-x-2">
|
||||||
|
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
|
닫기
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" onClick={handleSubmit}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserEditDialog;
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
<Badge variant={value === 'ACTIVE' ? 'success' : 'destructive'}>
|
||||||
|
{value}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userSecurity',
|
||||||
|
label: '권한',
|
||||||
|
render: (value) => (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{value === 'ROLE_ADMIN' ? '관리자' : '사용자'}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderRowActions = (row) => (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onMenuClick('user-edit', null, row)}
|
||||||
|
>
|
||||||
|
수정
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePasswordReset(row.esntlId)}
|
||||||
|
>
|
||||||
|
비밀번호 변경
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableActions = (
|
||||||
|
<>
|
||||||
|
<Button variant="destructive" onClick={handleSearch}>조회</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={selectedIds.length === 0}
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => onMenuClick('user-add')}>등록</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-red-500">
|
||||||
|
Error loading users: {error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>내부 사용자 목록</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center space-x-4 mb-4">
|
||||||
|
<div className="flex-1 max-w-sm">
|
||||||
|
<Input
|
||||||
|
placeholder="사용자 이름 검색"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{tableActions}
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
idField="esntlId"
|
||||||
|
data={users}
|
||||||
|
columns={columns}
|
||||||
|
loading={loading}
|
||||||
|
selectable
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onSelectIds={setSelectedIds}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
renderRowActions={renderRowActions}
|
||||||
|
deleteDialogTitle="사용자 삭제"
|
||||||
|
deleteDialogDescription="선택한 사용자를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserList;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const menuConfig = [
|
|||||||
id: 'system',
|
id: 'system',
|
||||||
label: '시스템 관리',
|
label: '시스템 관리',
|
||||||
items: [
|
items: [
|
||||||
{id: 'user-management', label: '사용자 관리', action: 'user-management', screen: 'user-management'},
|
{id: 'user-list', label: '사용자 관리', action: 'user-list', screen: 'user-list'},
|
||||||
{type: 'separator'},
|
{type: 'separator'},
|
||||||
{id: 'server-management', label: '서버 관리', action: 'server-management', screen: 'server-management'},
|
{id: 'server-management', label: '서버 관리', action: 'server-management', screen: 'server-management'},
|
||||||
{id: 'server-add', label: '서버 추가', action: 'server-add', screen: 'server-add'},
|
{id: 'server-add', label: '서버 추가', action: 'server-add', screen: 'server-add'},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import LoadTestManager from '../components/load-tester/LoadTestManager.jsx';
|
|||||||
import MockRouteList from '../components/mock-route/MockRouteList.jsx';
|
import MockRouteList from '../components/mock-route/MockRouteList.jsx';
|
||||||
import TCPServerList from '@/components/tcp-servers/TCPServerList.jsx';
|
import TCPServerList from '@/components/tcp-servers/TCPServerList.jsx';
|
||||||
import QueueList from '@/components/queues/QueueList.jsx';
|
import QueueList from '@/components/queues/QueueList.jsx';
|
||||||
|
import UserList from '@/components/users/UserList.jsx';
|
||||||
|
|
||||||
export const SCREENS = {
|
export const SCREENS = {
|
||||||
'api-tester': APITester,
|
'api-tester': APITester,
|
||||||
@@ -11,4 +12,5 @@ export const SCREENS = {
|
|||||||
'response-list': MockRouteList,
|
'response-list': MockRouteList,
|
||||||
'socket-list': TCPServerList,
|
'socket-list': TCPServerList,
|
||||||
'queue-list': QueueList,
|
'queue-list': QueueList,
|
||||||
|
'user-list': UserList,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {serverService} from '@/services/serverService';
|
|||||||
import {authService} from '@/services/authService';
|
import {authService} from '@/services/authService';
|
||||||
import APIClient from '@/services/APIClient.js';
|
import APIClient from '@/services/APIClient.js';
|
||||||
import {queueService} from '@/services/queueService.js';
|
import {queueService} from '@/services/queueService.js';
|
||||||
|
import {userService} from '@/services/userService.js';
|
||||||
|
|
||||||
const APIContext = createContext(null);
|
const APIContext = createContext(null);
|
||||||
|
|
||||||
@@ -43,6 +44,14 @@ export const APIProvider = ({
|
|||||||
last: true
|
last: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [users, setUsers] = useState({
|
||||||
|
content: [],
|
||||||
|
totalPages: 0,
|
||||||
|
number: 0,
|
||||||
|
first: true,
|
||||||
|
last: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
const collections$ = collectionService(contextPath);
|
const collections$ = collectionService(contextPath);
|
||||||
@@ -50,6 +59,7 @@ export const APIProvider = ({
|
|||||||
const servers$ = serverService();
|
const servers$ = serverService();
|
||||||
const auth$ = authService();
|
const auth$ = authService();
|
||||||
const queues$ = queueService(contextPath);
|
const queues$ = queueService(contextPath);
|
||||||
|
const users$ = userService(contextPath);
|
||||||
|
|
||||||
const handleLogout = useCallback(async () => {
|
const handleLogout = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -363,6 +373,79 @@ export const APIProvider = ({
|
|||||||
}
|
}
|
||||||
}, [loadQueues]);
|
}, [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 = {
|
const contextValue = {
|
||||||
apiClient,
|
apiClient,
|
||||||
@@ -393,7 +476,13 @@ export const APIProvider = ({
|
|||||||
loadQueues,
|
loadQueues,
|
||||||
deleteQueues,
|
deleteQueues,
|
||||||
startQueue,
|
startQueue,
|
||||||
stopQueue
|
stopQueue,
|
||||||
|
users,
|
||||||
|
loadUsers,
|
||||||
|
deleteUsers,
|
||||||
|
createUser,
|
||||||
|
updateUser,
|
||||||
|
resetPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -39,7 +39,7 @@ public class UserRegisterDTO implements Serializable {
|
|||||||
/**
|
/**
|
||||||
* 사용자상태코드
|
* 사용자상태코드
|
||||||
*/
|
*/
|
||||||
private UserStatus status;
|
private UserStatus status = UserStatus.ACTIVE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이동전화번호
|
* 이동전화번호
|
||||||
|
|||||||
Reference in New Issue
Block a user