feat: 관리자 전용 직원 수정/삭제 모달 및 신규 파견 등록/종료 기능 추가
This commit is contained in:
@@ -10,6 +10,7 @@ import { useAuthStore } from '@/stores';
|
||||
import employeeService, { type PortfolioEntry, type CreateDispatchRequest } from '@/services/employeeService';
|
||||
import projectService from '@/services/projectService';
|
||||
import type { EmployeeSearchResult, Project } from '@/types';
|
||||
import CreateDispatchModal from '@/components/dispatch/CreateDispatchModal';
|
||||
|
||||
function initialColor(name: string) {
|
||||
const colors = [
|
||||
@@ -275,7 +276,27 @@ export default function DispatchPage() {
|
||||
<span>공수 {entry.contributionRate}%</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs bg-primary/10 text-primary px-2.5 py-1 rounded-full font-medium">{entry.contributionRate}%</span>
|
||||
{(user?.roleCode === 'MANAGER' || user?.roleCode === 'ADMIN') && !entry.endDate && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!confirm(`'${entry.projectName}' 파견을 종료하시겠습니까?`)) return;
|
||||
try {
|
||||
await employeeService.endDispatch(entry.dispatchId);
|
||||
alert('파견이 종료되었습니다.');
|
||||
fetchPortfolio();
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.message || '파견 종료 실패');
|
||||
}
|
||||
}}
|
||||
className="px-2.5 py-1 bg-red-100 text-red-700 hover:bg-red-200 rounded-full text-xs font-semibold transition-colors"
|
||||
>
|
||||
파견 종료
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6 py-4 space-y-3">
|
||||
|
||||
@@ -8,6 +8,7 @@ import apiClient from '@/services/apiClient';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { useAuthStore } from '@/stores';
|
||||
import type { Employee, RoleCode, EmpStatus, DispatchStatus } from '@/types';
|
||||
import EmployeeEditModal from '@/components/employees/EmployeeEditModal';
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
ACTIVE: 'bg-status-resolved-bg text-status-resolved-text',
|
||||
@@ -36,6 +37,7 @@ export default function EmployeesPage() {
|
||||
|
||||
// Detail Modal State
|
||||
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null);
|
||||
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
|
||||
const [detailDispatch, setDetailDispatch] = useState<any[]>([]);
|
||||
const [detailEquipment, setDetailEquipment] = useState<any[]>([]);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
@@ -580,7 +582,21 @@ export default function EmployeesPage() {
|
||||
</td>
|
||||
<td className="py-4 px-4 sm:px-6 text-text-muted truncate max-w-[150px] sm:max-w-none">{emp.email}</td>
|
||||
<td className="py-4 px-4 sm:px-6">
|
||||
<span className="text-xs text-text-muted">View details →</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-text-muted hover:text-primary transition-colors">상세 →</span>
|
||||
{(user?.roleCode === 'ADMIN' || user?.roleCode === 'MANAGER') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingEmployee(emp);
|
||||
}}
|
||||
className="px-2 py-1 bg-primary/10 text-primary hover:bg-primary/20 rounded text-xs font-semibold transition-colors"
|
||||
>
|
||||
수정
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -606,9 +622,23 @@ export default function EmployeesPage() {
|
||||
<p className="text-xs text-text-muted truncate">{roleLabel[emp.roleCode] || emp.roleCode}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[10px] font-medium px-2.5 py-1 rounded-full ${statusColor[emp.empStatus] || 'bg-gray-100 text-gray-600'}`}>
|
||||
{emp.empStatus === 'ACTIVE' ? '재직' : emp.empStatus === 'ON_LEAVE' ? '휴직' : '퇴사'}
|
||||
</span>
|
||||
{(user?.roleCode === 'ADMIN' || user?.roleCode === 'MANAGER') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingEmployee(emp);
|
||||
}}
|
||||
className="px-2 py-1 bg-primary/10 text-primary hover:bg-primary/20 rounded text-xs font-semibold"
|
||||
>
|
||||
수정
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-text-muted">
|
||||
<div className="truncate">Email: {emp.email}</div>
|
||||
@@ -620,6 +650,16 @@ export default function EmployeesPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Employee Edit Modal */}
|
||||
<EmployeeEditModal
|
||||
isOpen={!!editingEmployee}
|
||||
onClose={() => setEditingEmployee(null)}
|
||||
employee={editingEmployee}
|
||||
onSuccess={() => {
|
||||
fetchEmployees();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, AlertCircle, Search } from 'lucide-react';
|
||||
import employeeService from '@/services/employeeService';
|
||||
import projectService from '@/services/projectService';
|
||||
import type { Employee, Project, CreateDispatchRequest } from '@/types';
|
||||
|
||||
interface CreateDispatchModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export default function CreateDispatchModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: CreateDispatchModalProps) {
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
|
||||
const [employeeId, setEmployeeId] = useState<number | ''>('');
|
||||
const [projectId, setProjectId] = useState<number | ''>('');
|
||||
const [roleTitle, setRoleTitle] = useState('');
|
||||
const [contributionRate, setContributionRate] = useState<number>(100);
|
||||
const [individualTech, setIndividualTech] = useState('');
|
||||
const [performedTasks, setPerformedTasks] = useState('');
|
||||
const [startDate, setStartDate] = useState(new Date().toISOString().split('T')[0]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadData();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setFetching(true);
|
||||
setError(null);
|
||||
const [empRes, projRes] = await Promise.all([
|
||||
employeeService.getAll({ size: 200 }),
|
||||
projectService.getAll(),
|
||||
]);
|
||||
|
||||
if (empRes.success && empRes.data) {
|
||||
setEmployees(empRes.data.content || []);
|
||||
}
|
||||
if (projRes.success && projRes.data) {
|
||||
setProjects(projRes.data || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load dispatch form options', err);
|
||||
setError('직원 및 프로젝트 목록을 불러오지 못했습니다.');
|
||||
} finally {
|
||||
setFetching(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!employeeId) {
|
||||
setError('파견할 직원을 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (!projectId) {
|
||||
setError('파견 프로젝트를 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (!roleTitle.trim()) {
|
||||
setError('담당 역할을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const request: CreateDispatchRequest = {
|
||||
employeeId: Number(employeeId),
|
||||
projectId: Number(projectId),
|
||||
roleTitle: roleTitle.trim(),
|
||||
contributionRate: Number(contributionRate) || 100,
|
||||
individualTech: individualTech.trim(),
|
||||
performedTasks: performedTasks.trim(),
|
||||
startDate,
|
||||
};
|
||||
|
||||
const res = await employeeService.createDispatch(request);
|
||||
if (res.success) {
|
||||
onSuccess();
|
||||
onClose();
|
||||
// Reset form
|
||||
setEmployeeId('');
|
||||
setProjectId('');
|
||||
setRoleTitle('');
|
||||
setContributionRate(100);
|
||||
setIndividualTech('');
|
||||
setPerformedTasks('');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || '파견 등록 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
|
||||
<div className="bg-surface border border-border rounded-2xl shadow-soft w-full max-w-lg overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border bg-surface-container-low">
|
||||
<h2 className="font-outfit text-lg font-semibold text-text-main">
|
||||
신규 파견(투입) 등록
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-text-muted hover:text-text-main rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body Form */}
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4 max-h-[80vh] overflow-y-auto">
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-red-50 text-red-600 text-sm rounded-xl border border-red-200">
|
||||
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fetching ? (
|
||||
<div className="py-8 text-center text-text-muted text-sm">목록 로딩 중...</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Employee Selection */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">
|
||||
직원 선택 *
|
||||
</label>
|
||||
<select
|
||||
value={employeeId}
|
||||
onChange={(e) => setEmployeeId(e.target.value ? Number(e.target.value) : '')}
|
||||
className="w-full px-3 py-2.5 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
required
|
||||
>
|
||||
<option value="">-- 직원을 선택하세요 --</option>
|
||||
{employees.map((emp) => (
|
||||
<option key={emp.empId} value={emp.empId}>
|
||||
{emp.name} ({emp.email}) - {emp.dispatchStatus === 'DISPATCHED' ? '파견중' : '대기'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Project Selection */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">
|
||||
프로젝트 선택 *
|
||||
</label>
|
||||
<select
|
||||
value={projectId}
|
||||
onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : '')}
|
||||
className="w-full px-3 py-2.5 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
required
|
||||
>
|
||||
<option value="">-- 프로젝트를 선택하세요 --</option>
|
||||
{projects.map((proj) => (
|
||||
<option key={proj.projectId} value={proj.projectId}>
|
||||
[{proj.contractCompany}] {proj.projectName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Role Title & Contribution Rate */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">
|
||||
담당 역할 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={roleTitle}
|
||||
onChange={(e) => setRoleTitle(e.target.value)}
|
||||
placeholder="예: 백엔드 개발, PL, QA"
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">
|
||||
투입 기여도 (%) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={10}
|
||||
max={100}
|
||||
value={contributionRate}
|
||||
onChange={(e) => setContributionRate(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Start Date */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">
|
||||
투입 시작일 *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tech Stack */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">
|
||||
사용 기술 스택
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={individualTech}
|
||||
onChange={(e) => setIndividualTech(e.target.value)}
|
||||
placeholder="예: Java 17, Spring Boot, React, PostgreSQL"
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Performed Tasks */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">
|
||||
수행 업무 요약
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={performedTasks}
|
||||
onChange={(e) => setPerformedTasks(e.target.value)}
|
||||
placeholder="담당 업무에 대한 설명을 입력하세요"
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-border text-text-main rounded-xl text-xs font-bold hover:bg-surface-container-low transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || fetching}
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-primary text-white rounded-xl text-xs font-bold hover:bg-primary/90 disabled:opacity-50 transition-colors shadow-sm"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{loading ? '등록중...' : '파견 등록'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, AlertCircle, Trash2, Key } from 'lucide-react';
|
||||
import employeeService from '@/services/employeeService';
|
||||
import type { Employee, RoleCode, EmpStatus } from '@/types';
|
||||
|
||||
interface EmployeeEditModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
employee: Employee | null;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export default function EmployeeEditModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
employee,
|
||||
onSuccess,
|
||||
}: EmployeeEditModalProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [position, setPosition] = useState('');
|
||||
const [joinedDate, setJoinedDate] = useState('');
|
||||
const [roleCode, setRoleCode] = useState<RoleCode>('EMPLOYEE');
|
||||
const [empStatus, setEmpStatus] = useState<EmpStatus>('ACTIVE');
|
||||
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [showResetPassword, setShowResetPassword] = useState(false);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (employee) {
|
||||
setName(employee.name || '');
|
||||
setEmail(employee.email || '');
|
||||
setPhone(employee.phone || '');
|
||||
setPosition(employee.position || '');
|
||||
setJoinedDate(employee.joinedDate || '');
|
||||
setRoleCode(employee.roleCode || 'EMPLOYEE');
|
||||
setEmpStatus(employee.empStatus || 'ACTIVE');
|
||||
setNewPassword('');
|
||||
setShowResetPassword(false);
|
||||
setError(null);
|
||||
}
|
||||
}, [employee]);
|
||||
|
||||
if (!isOpen || !employee) return null;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) {
|
||||
setError('이름을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Update employee information
|
||||
const updateData: Partial<Employee> = {
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
position,
|
||||
joinedDate,
|
||||
roleCode,
|
||||
empStatus,
|
||||
};
|
||||
|
||||
const res = await employeeService.update(employee.empId, updateData);
|
||||
|
||||
// Reset password if requested
|
||||
if (showResetPassword && newPassword.trim()) {
|
||||
await employeeService.resetPassword(employee.empId, newPassword.trim());
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
onSuccess();
|
||||
onClose();
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || '직원 정보 수정 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm(`정말로 직원 '${employee.name}' 님을 삭제(퇴사) 처리하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await employeeService.delete(employee.empId);
|
||||
if (res.success) {
|
||||
onSuccess();
|
||||
onClose();
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || '직원 삭제에 실패했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
|
||||
<div className="bg-surface border border-border rounded-2xl shadow-soft w-full max-w-lg overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border bg-surface-container-low">
|
||||
<h2 className="font-outfit text-lg font-semibold text-text-main">
|
||||
직원 정보 수정 — {employee.name}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-text-muted hover:text-text-main rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form Body */}
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4 max-h-[80vh] overflow-y-auto">
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-red-50 text-red-600 text-sm rounded-xl border border-red-200">
|
||||
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">이름 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">이메일</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">전화번호</label>
|
||||
<input
|
||||
type="text"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="010-0000-0000"
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">직급 / 직책</label>
|
||||
<input
|
||||
type="text"
|
||||
value={position}
|
||||
onChange={(e) => setPosition(e.target.value)}
|
||||
placeholder="예: 수석연구원, 팀장"
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">시스템 권한</label>
|
||||
<select
|
||||
value={roleCode}
|
||||
onChange={(e) => setRoleCode(e.target.value as RoleCode)}
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
>
|
||||
<option value="EMPLOYEE">일반 직원 (EMPLOYEE)</option>
|
||||
<option value="MANAGER">매니저 (MANAGER)</option>
|
||||
<option value="ADMIN">최고 관리자 (ADMIN)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">재직 상태</label>
|
||||
<select
|
||||
value={empStatus}
|
||||
onChange={(e) => setEmpStatus(e.target.value as EmpStatus)}
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
>
|
||||
<option value="ACTIVE">재직 (ACTIVE)</option>
|
||||
<option value="ON_LEAVE">휴직 (ON_LEAVE)</option>
|
||||
<option value="RESIGNED">퇴사 (RESIGNED)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1.5">입사일</label>
|
||||
<input
|
||||
type="date"
|
||||
value={joinedDate}
|
||||
onChange={(e) => setJoinedDate(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reset Password Toggle */}
|
||||
<div className="pt-2 border-t border-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowResetPassword(!showResetPassword)}
|
||||
className="flex items-center gap-2 text-xs font-semibold text-primary hover:underline"
|
||||
>
|
||||
<Key className="w-3.5 h-3.5" />
|
||||
{showResetPassword ? '비밀번호 재설정 취소' : '비밀번호 강제 재설정'}
|
||||
</button>
|
||||
|
||||
{showResetPassword && (
|
||||
<div className="mt-3">
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="새 초기 비밀번호를 입력하세요 (예: admin123)"
|
||||
className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-2 text-red-600 hover:bg-red-50 rounded-xl text-xs font-bold transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
직원 삭제
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-border text-text-main rounded-xl text-xs font-bold hover:bg-surface-container-low transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-primary text-white rounded-xl text-xs font-bold hover:bg-primary/90 disabled:opacity-50 transition-colors shadow-sm"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{loading ? '저장중...' : '저장'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user