사용자 관리
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user