592 lines
28 KiB
TypeScript
592 lines
28 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Users, Plus, Pencil, Trash2, X, Search, AlertTriangle } from 'lucide-react';
|
|
import { useAuthStore } from '@/stores';
|
|
import employeeService from '@/services/employeeService';
|
|
import type { Employee, RoleCode, EmpStatus } from '@/types';
|
|
|
|
const roleLabels: Record<RoleCode, string> = {
|
|
ADMIN: '최고관리자',
|
|
MANAGER: '본사관리자',
|
|
EMPLOYEE: '일반사용자',
|
|
};
|
|
|
|
const roleBadgeColor: Record<RoleCode, string> = {
|
|
ADMIN: 'bg-purple-100 text-purple-700',
|
|
MANAGER: 'bg-indigo-100 text-indigo-700',
|
|
EMPLOYEE: 'bg-gray-100 text-gray-700',
|
|
};
|
|
|
|
const statusLabels: Record<EmpStatus, string> = {
|
|
ACTIVE: '재직',
|
|
ON_LEAVE: '휴직',
|
|
RESIGNED: '퇴사',
|
|
};
|
|
|
|
const statusBadgeColor: Record<EmpStatus, string> = {
|
|
ACTIVE: 'bg-status-resolved-bg text-status-resolved-text',
|
|
ON_LEAVE: 'bg-status-pending-bg text-status-pending-text',
|
|
RESIGNED: 'bg-status-critical-bg text-status-critical-text',
|
|
};
|
|
|
|
const educationLabels: Record<string, string> = {
|
|
HIGH_SCHOOL: '고졸',
|
|
COLLEGE: '전문대',
|
|
BACHELOR: '학사',
|
|
MASTER: '석사',
|
|
DOCTOR: '박사',
|
|
};
|
|
|
|
const initialColors = ['bg-indigo-500', 'bg-purple-500', 'bg-blue-500', 'bg-green-500', 'bg-orange-500', 'bg-pink-500'];
|
|
|
|
function getInitialColor(name: string, index: number) {
|
|
return initialColors[index % initialColors.length];
|
|
}
|
|
|
|
interface CreateForm {
|
|
loginId: string;
|
|
name: string;
|
|
password: string;
|
|
email: string;
|
|
phone: string;
|
|
roleCode: RoleCode;
|
|
empStatus: EmpStatus;
|
|
joinedDate: string;
|
|
birthDate: string;
|
|
education: string;
|
|
major: string;
|
|
graduationDate: string;
|
|
residence: string;
|
|
}
|
|
|
|
interface EditForm {
|
|
name: string;
|
|
email: string;
|
|
phone: string;
|
|
roleCode: RoleCode;
|
|
empStatus: EmpStatus;
|
|
joinedDate: string;
|
|
leftDate: string;
|
|
birthDate: string;
|
|
education: string;
|
|
major: string;
|
|
graduationDate: string;
|
|
residence: string;
|
|
}
|
|
|
|
const emptyCreateForm: CreateForm = {
|
|
loginId: '', name: '', password: '', email: '', phone: '',
|
|
roleCode: 'EMPLOYEE', empStatus: 'ACTIVE', joinedDate: '',
|
|
birthDate: '', education: '', major: '', graduationDate: '', residence: '',
|
|
};
|
|
|
|
export default function AccountsPage() {
|
|
const user = useAuthStore((s) => s.user);
|
|
const [employees, setEmployees] = useState<Employee[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchKeyword, setSearchKeyword] = useState('');
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
const [targetEmployee, setTargetEmployee] = useState<Employee | null>(null);
|
|
const [createForm, setCreateForm] = useState<CreateForm>({ ...emptyCreateForm });
|
|
const [editForm, setEditForm] = useState<EditForm>({
|
|
name: '', email: '', phone: '', roleCode: 'EMPLOYEE', empStatus: 'ACTIVE',
|
|
joinedDate: '', leftDate: '', birthDate: '', education: '', major: '', graduationDate: '', residence: '',
|
|
});
|
|
const [createError, setCreateError] = useState<string | null>(null);
|
|
const [editError, setEditError] = useState<string | null>(null);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const fetchEmployees = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const res = await employeeService.getAll({ size: 200 });
|
|
setEmployees(res.data.content);
|
|
} catch {
|
|
// silent
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchEmployees();
|
|
}, []);
|
|
|
|
if (user?.roleCode !== 'ADMIN') {
|
|
return (
|
|
<div className="p-4 sm:p-6 lg:p-8">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="bg-status-critical-bg border border-status-critical rounded-2xl p-8 text-center">
|
|
<p className="text-status-critical-text font-medium">접근 거부. 관리자 권한이 필요합니다.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const filtered = searchKeyword.trim()
|
|
? employees.filter((e) => e.name.toLowerCase().includes(searchKeyword.toLowerCase()))
|
|
: employees;
|
|
|
|
const openCreate = () => {
|
|
setCreateForm({ ...emptyCreateForm });
|
|
setCreateError(null);
|
|
setShowCreateModal(true);
|
|
};
|
|
|
|
const handleCreate = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!createForm.loginId.trim() || !createForm.name.trim() || !createForm.password.trim()) {
|
|
setCreateError('로그인 ID, 이름, 비밀번호는 필수입니다.');
|
|
return;
|
|
}
|
|
setIsSubmitting(true);
|
|
setCreateError(null);
|
|
try {
|
|
await employeeService.create(createForm);
|
|
setShowCreateModal(false);
|
|
await fetchEmployees();
|
|
} catch (err: any) {
|
|
setCreateError(err.response?.data?.message || '계정 생성에 실패했습니다.');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const openEdit = (emp: Employee) => {
|
|
setTargetEmployee(emp);
|
|
setEditForm({
|
|
name: emp.name,
|
|
email: emp.email,
|
|
phone: emp.phone || '',
|
|
roleCode: emp.roleCode,
|
|
empStatus: emp.empStatus,
|
|
joinedDate: emp.joinedDate || '',
|
|
leftDate: emp.leftDate || '',
|
|
birthDate: emp.birthDate || '',
|
|
education: emp.education || '',
|
|
major: emp.major || '',
|
|
graduationDate: emp.graduationDate || '',
|
|
residence: emp.residence || '',
|
|
});
|
|
setEditError(null);
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
const handleUpdate = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!editForm.name.trim() || !targetEmployee) return;
|
|
setIsSubmitting(true);
|
|
setEditError(null);
|
|
try {
|
|
await employeeService.update(targetEmployee.empId, editForm);
|
|
setShowEditModal(false);
|
|
await fetchEmployees();
|
|
} catch (err: any) {
|
|
setEditError(err.response?.data?.message || '수정에 실패했습니다.');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const openDelete = (emp: Employee) => {
|
|
setTargetEmployee(emp);
|
|
setShowDeleteDialog(true);
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!targetEmployee) return;
|
|
setIsSubmitting(true);
|
|
try {
|
|
await employeeService.delete(targetEmployee.empId);
|
|
setShowDeleteDialog(false);
|
|
await fetchEmployees();
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.message || '삭제에 실패했습니다.');
|
|
setShowDeleteDialog(false);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// Shared modal field style
|
|
const fieldClass = "w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary";
|
|
const labelClass = "text-xs font-medium text-text-muted";
|
|
const selectClass = "w-full px-3 py-2 border border-border rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-primary";
|
|
|
|
return (
|
|
<div className="p-4 sm:p-6 lg:p-8">
|
|
<div className="max-w-7xl mx-auto">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
|
<div className="flex items-center gap-3">
|
|
<Users className="w-6 h-6 text-primary" />
|
|
<div>
|
|
<h2 className="font-outfit text-2xl font-semibold text-text-main tracking-tight">계정 관리</h2>
|
|
<p className="text-text-muted text-sm">직원 계정 생성, 수정, 삭제 (ADMIN 전용)</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={openCreate}
|
|
className="bg-primary text-white px-4 py-2.5 rounded-xl text-sm font-semibold hover:bg-primary-hover transition-all flex items-center gap-1.5 shadow-soft"
|
|
>
|
|
<Plus className="w-4 h-4" /> 새 계정
|
|
</button>
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<div className="relative w-full sm:w-72 mb-4">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
|
|
<input
|
|
value={searchKeyword}
|
|
onChange={(e) => setSearchKeyword(e.target.value)}
|
|
className="w-full pl-9 pr-3 py-2 rounded-xl bg-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-primary transition-all"
|
|
placeholder="이름으로 검색..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
{loading ? (
|
|
<div className="bg-surface rounded-2xl border border-border shadow-card p-12 text-center text-text-muted">로딩 중...</div>
|
|
) : (
|
|
<div className="bg-surface rounded-2xl border border-border shadow-card overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left">
|
|
<thead>
|
|
<tr className="bg-gray-50/50 border-b border-border text-[10px] font-bold text-text-muted uppercase tracking-widest">
|
|
<th className="px-4 sm:px-6 py-3">이름</th>
|
|
<th className="px-4 sm:px-6 py-3 hidden md:table-cell">이메일</th>
|
|
<th className="px-4 sm:px-6 py-3">권한</th>
|
|
<th className="px-4 sm:px-6 py-3">상태</th>
|
|
<th className="px-4 sm:px-6 py-3 hidden lg:table-cell">생년월일</th>
|
|
<th className="px-4 sm:px-6 py-3 hidden lg:table-cell">학력</th>
|
|
<th className="px-4 sm:px-6 py-3 hidden xl:table-cell">거주지</th>
|
|
<th className="px-4 sm:px-6 py-3 hidden sm:table-cell">가입일</th>
|
|
<th className="px-4 sm:px-6 py-3 text-right">액션</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border text-sm">
|
|
{filtered.map((emp, idx) => (
|
|
<tr key={emp.empId} className="hover:bg-gray-50/30 transition-colors">
|
|
<td className="px-4 sm:px-6 py-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-8 h-8 rounded-full ${getInitialColor(emp.name, idx)} flex items-center justify-center text-xs font-medium text-white flex-shrink-0`}>
|
|
{emp.name[0]}
|
|
</div>
|
|
<span className="font-medium text-text-main">{emp.name}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-3 text-text-muted hidden md:table-cell">{emp.email || '-'}</td>
|
|
<td className="px-4 sm:px-6 py-3">
|
|
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${roleBadgeColor[emp.roleCode]}`}>
|
|
{roleLabels[emp.roleCode]}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-3">
|
|
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${statusBadgeColor[emp.empStatus]}`}>
|
|
{statusLabels[emp.empStatus]}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-3 text-text-muted hidden lg:table-cell">{emp.birthDate || '-'}</td>
|
|
<td className="px-4 sm:px-6 py-3 hidden lg:table-cell">
|
|
{emp.education ? (
|
|
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-indigo-50 text-indigo-700">
|
|
{educationLabels[emp.education] || emp.education}
|
|
</span>
|
|
) : '-'}
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-3 text-text-muted hidden xl:table-cell">{emp.residence || '-'}</td>
|
|
<td className="px-4 sm:px-6 py-3 text-text-muted hidden sm:table-cell">{emp.joinedDate}</td>
|
|
<td className="px-4 sm:px-6 py-3 text-right">
|
|
<div className="flex items-center justify-end gap-1">
|
|
<button onClick={() => openEdit(emp)} className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors" title="수정">
|
|
<Pencil className="w-3.5 h-3.5 text-text-muted hover:text-primary" />
|
|
</button>
|
|
<button onClick={() => openDelete(emp)} className="p-1.5 hover:bg-red-50 rounded-lg transition-colors" title="삭제">
|
|
<Trash2 className="w-3.5 h-3.5 text-text-muted hover:text-status-critical-text" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{filtered.length === 0 && (
|
|
<div className="text-center py-12 text-text-muted">검색 결과가 없습니다.</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Create Modal */}
|
|
{showCreateModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={() => setShowCreateModal(false)}>
|
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg mx-4 p-6 max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-bold text-text-main">새 계정 생성</h3>
|
|
<button onClick={() => setShowCreateModal(false)} className="p-1.5 hover:bg-gray-100 rounded-lg">
|
|
<X className="w-5 h-5 text-text-muted" />
|
|
</button>
|
|
</div>
|
|
<form onSubmit={handleCreate} className="space-y-3">
|
|
{/* Basic Info */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>로그인 ID *</label>
|
|
<input value={createForm.loginId} onChange={(e) => setCreateForm((f) => ({ ...f, loginId: e.target.value }))} className={fieldClass} placeholder="login_id" />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>이름 *</label>
|
|
<input value={createForm.name} onChange={(e) => setCreateForm((f) => ({ ...f, name: e.target.value }))} className={fieldClass} placeholder="홍길동" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>비밀번호 *</label>
|
|
<input type="password" value={createForm.password} onChange={(e) => setCreateForm((f) => ({ ...f, password: e.target.value }))} className={fieldClass} placeholder="••••••••" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>이메일</label>
|
|
<input type="email" value={createForm.email} onChange={(e) => setCreateForm((f) => ({ ...f, email: e.target.value }))} className={fieldClass} placeholder="email@example.com" />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>전화번호</label>
|
|
<input value={createForm.phone} onChange={(e) => setCreateForm((f) => ({ ...f, phone: e.target.value }))} className={fieldClass} placeholder="010-0000-0000" />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>권한</label>
|
|
<select value={createForm.roleCode} onChange={(e) => setCreateForm((f) => ({ ...f, roleCode: e.target.value as RoleCode }))} className={selectClass}>
|
|
<option value="EMPLOYEE">일반사용자</option>
|
|
<option value="MANAGER">본사관리자</option>
|
|
<option value="ADMIN">최고관리자</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>상태</label>
|
|
<select value={createForm.empStatus} onChange={(e) => setCreateForm((f) => ({ ...f, empStatus: e.target.value as EmpStatus }))} className={selectClass}>
|
|
<option value="ACTIVE">재직</option>
|
|
<option value="ON_LEAVE">휴직</option>
|
|
<option value="RESIGNED">퇴사</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>가입일</label>
|
|
<input type="date" value={createForm.joinedDate} onChange={(e) => setCreateForm((f) => ({ ...f, joinedDate: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div className="border-t border-border pt-3 mt-3">
|
|
<p className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">추가 정보</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>생년월일</label>
|
|
<input type="date" value={createForm.birthDate} onChange={(e) => setCreateForm((f) => ({ ...f, birthDate: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>학력</label>
|
|
<select value={createForm.education} onChange={(e) => setCreateForm((f) => ({ ...f, education: e.target.value }))} className={selectClass}>
|
|
<option value="">선택하세요</option>
|
|
<option value="HIGH_SCHOOL">고졸</option>
|
|
<option value="COLLEGE">전문대</option>
|
|
<option value="BACHELOR">학사</option>
|
|
<option value="MASTER">석사</option>
|
|
<option value="DOCTOR">박사</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>학과</label>
|
|
<input value={createForm.major} onChange={(e) => setCreateForm((f) => ({ ...f, major: e.target.value }))} className={fieldClass} placeholder="컴퓨터공학" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>졸업일</label>
|
|
<input type="date" value={createForm.graduationDate} onChange={(e) => setCreateForm((f) => ({ ...f, graduationDate: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>현재 거주지</label>
|
|
<input value={createForm.residence} onChange={(e) => setCreateForm((f) => ({ ...f, residence: e.target.value }))} className={fieldClass} placeholder="서울시 강남구" />
|
|
</div>
|
|
</div>
|
|
|
|
{createError && (
|
|
<div className="bg-status-critical-bg border border-status-critical text-status-critical-text rounded-lg px-3 py-2 text-sm">
|
|
{createError}
|
|
</div>
|
|
)}
|
|
<div className="flex justify-end gap-3 pt-2">
|
|
<button type="button" onClick={() => setShowCreateModal(false)} className="px-4 py-2 rounded-xl text-sm font-medium text-text-muted hover:bg-gray-100">
|
|
취소
|
|
</button>
|
|
<button type="submit" disabled={isSubmitting} className="px-4 py-2 rounded-xl text-sm font-semibold bg-primary text-white hover:bg-primary-hover disabled:opacity-50">
|
|
{isSubmitting ? '생성 중...' : '생성'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Edit Modal */}
|
|
{showEditModal && targetEmployee && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={() => setShowEditModal(false)}>
|
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg mx-4 p-6 max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-bold text-text-main">계정 수정 — {targetEmployee.name}</h3>
|
|
<button onClick={() => setShowEditModal(false)} className="p-1.5 hover:bg-gray-100 rounded-lg">
|
|
<X className="w-5 h-5 text-text-muted" />
|
|
</button>
|
|
</div>
|
|
<form onSubmit={handleUpdate} className="space-y-3">
|
|
{/* Basic Info */}
|
|
<div>
|
|
<label className={labelClass}>이름 *</label>
|
|
<input value={editForm.name} onChange={(e) => setEditForm((f) => ({ ...f, name: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>이메일</label>
|
|
<input type="email" value={editForm.email} onChange={(e) => setEditForm((f) => ({ ...f, email: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>전화번호</label>
|
|
<input value={editForm.phone} onChange={(e) => setEditForm((f) => ({ ...f, phone: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>권한</label>
|
|
<select value={editForm.roleCode} onChange={(e) => setEditForm((f) => ({ ...f, roleCode: e.target.value as RoleCode }))} className={selectClass}>
|
|
<option value="EMPLOYEE">일반사용자</option>
|
|
<option value="MANAGER">본사관리자</option>
|
|
<option value="ADMIN">최고관리자</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>상태</label>
|
|
<select value={editForm.empStatus} onChange={(e) => setEditForm((f) => ({ ...f, empStatus: e.target.value as EmpStatus }))} className={selectClass}>
|
|
<option value="ACTIVE">재직</option>
|
|
<option value="ON_LEAVE">휴직</option>
|
|
<option value="RESIGNED">퇴사</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>가입일</label>
|
|
<input type="date" value={editForm.joinedDate} onChange={(e) => setEditForm((f) => ({ ...f, joinedDate: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>퇴사일</label>
|
|
<input type="date" value={editForm.leftDate} onChange={(e) => setEditForm((f) => ({ ...f, leftDate: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div className="border-t border-border pt-3 mt-3">
|
|
<p className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">추가 정보</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>생년월일</label>
|
|
<input type="date" value={editForm.birthDate} onChange={(e) => setEditForm((f) => ({ ...f, birthDate: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>학력</label>
|
|
<select value={editForm.education} onChange={(e) => setEditForm((f) => ({ ...f, education: e.target.value }))} className={selectClass}>
|
|
<option value="">선택하세요</option>
|
|
<option value="HIGH_SCHOOL">고졸</option>
|
|
<option value="COLLEGE">전문대</option>
|
|
<option value="BACHELOR">학사</option>
|
|
<option value="MASTER">석사</option>
|
|
<option value="DOCTOR">박사</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>학과</label>
|
|
<input value={editForm.major} onChange={(e) => setEditForm((f) => ({ ...f, major: e.target.value }))} className={fieldClass} placeholder="컴퓨터공학" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>졸업일</label>
|
|
<input type="date" value={editForm.graduationDate} onChange={(e) => setEditForm((f) => ({ ...f, graduationDate: e.target.value }))} className={fieldClass} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>현재 거주지</label>
|
|
<input value={editForm.residence} onChange={(e) => setEditForm((f) => ({ ...f, residence: e.target.value }))} className={fieldClass} placeholder="서울시 강남구" />
|
|
</div>
|
|
</div>
|
|
|
|
{editError && (
|
|
<div className="bg-status-critical-bg border border-status-critical text-status-critical-text rounded-lg px-3 py-2 text-sm">
|
|
{editError}
|
|
</div>
|
|
)}
|
|
<div className="flex justify-end gap-3 pt-2">
|
|
<button type="button" onClick={() => setShowEditModal(false)} className="px-4 py-2 rounded-xl text-sm font-medium text-text-muted hover:bg-gray-100">
|
|
취소
|
|
</button>
|
|
<button type="submit" disabled={isSubmitting} className="px-4 py-2 rounded-xl text-sm font-semibold bg-primary text-white hover:bg-primary-hover disabled:opacity-50">
|
|
{isSubmitting ? '저장 중...' : '저장'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Confirmation */}
|
|
{showDeleteDialog && targetEmployee && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={() => setShowDeleteDialog(false)}>
|
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6" onClick={(e) => e.stopPropagation()}>
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="w-10 h-10 rounded-full bg-status-critical-bg flex items-center justify-center flex-shrink-0">
|
|
<AlertTriangle className="w-5 h-5 text-status-critical-text" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-bold text-text-main">계정 삭제</h3>
|
|
<p className="text-text-muted text-sm">이 작업은 되돌릴 수 없습니다</p>
|
|
</div>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-xl px-4 py-3 mb-6">
|
|
<p className="text-sm text-text-main">
|
|
<span className="font-semibold">{targetEmployee.name}</span> 계정을 삭제하시겠습니까?
|
|
</p>
|
|
<p className="text-xs text-text-muted mt-1">소프트 삭제 처리되며 시스템 접근이 차단됩니다.</p>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowDeleteDialog(false)}
|
|
disabled={isSubmitting}
|
|
className="flex-1 px-4 py-2.5 rounded-xl border border-border text-text-main text-sm font-medium hover:bg-gray-100 transition-all disabled:opacity-50"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleDelete}
|
|
disabled={isSubmitting}
|
|
className="flex-1 px-4 py-2.5 rounded-xl bg-status-critical text-white text-sm font-semibold hover:bg-status-critical-hover transition-all disabled:opacity-50"
|
|
>
|
|
{isSubmitting ? '삭제 중...' : '삭제'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|