feat: add employee fields (birthDate, education, residence) + emergency contacts

This commit is contained in:
pray4skylark
2026-06-02 18:11:05 +09:00
parent 65a82db0e0
commit d205a447a1
12 changed files with 747 additions and 112 deletions
+201 -84
View File
@@ -30,6 +30,14 @@ const statusBadgeColor: Record<EmpStatus, string> = {
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) {
@@ -41,17 +49,38 @@ interface CreateForm {
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[]>([]);
@@ -61,11 +90,10 @@ export default function AccountsPage() {
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [targetEmployee, setTargetEmployee] = useState<Employee | null>(null);
const [createForm, setCreateForm] = useState<CreateForm>({
loginId: '', name: '', password: '', email: '', roleCode: 'EMPLOYEE', empStatus: 'ACTIVE',
});
const [createForm, setCreateForm] = useState<CreateForm>({ ...emptyCreateForm });
const [editForm, setEditForm] = useState<EditForm>({
name: '', email: '', roleCode: 'EMPLOYEE', empStatus: 'ACTIVE',
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);
@@ -104,7 +132,7 @@ export default function AccountsPage() {
: employees;
const openCreate = () => {
setCreateForm({ loginId: '', name: '', password: '', email: '', roleCode: 'EMPLOYEE', empStatus: 'ACTIVE' });
setCreateForm({ ...emptyCreateForm });
setCreateError(null);
setShowCreateModal(true);
};
@@ -130,7 +158,20 @@ export default function AccountsPage() {
const openEdit = (emp: Employee) => {
setTargetEmployee(emp);
setEditForm({ name: emp.name, email: emp.email, roleCode: emp.roleCode, empStatus: emp.empStatus });
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);
};
@@ -171,6 +212,11 @@ export default function AccountsPage() {
}
};
// 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">
@@ -212,9 +258,12 @@ export default function AccountsPage() {
<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">이메일</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>
@@ -230,7 +279,7 @@ export default function AccountsPage() {
<span className="font-medium text-text-main">{emp.name}</span>
</div>
</td>
<td className="px-4 sm:px-6 py-3 text-text-muted">{emp.email || '-'}</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]}
@@ -241,6 +290,15 @@ export default function AccountsPage() {
{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">
@@ -266,7 +324,7 @@ export default function AccountsPage() {
{/* 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-md mx-4 p-6" onClick={(e) => e.stopPropagation()}>
<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">
@@ -274,70 +332,91 @@ export default function AccountsPage() {
</button>
</div>
<form onSubmit={handleCreate} className="space-y-3">
<div>
<label className="text-xs font-medium text-text-muted">로그인 ID *</label>
<input
value={createForm.loginId}
onChange={(e) => setCreateForm((f) => ({ ...f, loginId: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="login_id"
/>
{/* 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="text-xs font-medium text-text-muted">이름 *</label>
<input
value={createForm.name}
onChange={(e) => setCreateForm((f) => ({ ...f, name: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="홍길동"
/>
</div>
<div>
<label className="text-xs font-medium text-text-muted">비밀번호 *</label>
<input
type="password"
value={createForm.password}
onChange={(e) => setCreateForm((f) => ({ ...f, password: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="••••••••"
/>
</div>
<div>
<label className="text-xs font-medium text-text-muted">이메일</label>
<input
type="email"
value={createForm.email}
onChange={(e) => setCreateForm((f) => ({ ...f, email: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="email@example.com"
/>
<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="text-xs font-medium text-text-muted">권한</label>
<select
value={createForm.roleCode}
onChange={(e) => setCreateForm((f) => ({ ...f, roleCode: e.target.value as RoleCode }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-primary"
>
<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="text-xs font-medium text-text-muted">상태</label>
<select
value={createForm.empStatus}
onChange={(e) => setCreateForm((f) => ({ ...f, empStatus: e.target.value as EmpStatus }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-primary"
>
<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}
@@ -359,7 +438,7 @@ export default function AccountsPage() {
{/* 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-md mx-4 p-6" onClick={(e) => e.stopPropagation()}>
<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">
@@ -367,49 +446,87 @@ export default function AccountsPage() {
</button>
</div>
<form onSubmit={handleUpdate} className="space-y-3">
{/* Basic Info */}
<div>
<label className="text-xs font-medium text-text-muted">이름 *</label>
<input
value={editForm.name}
onChange={(e) => setEditForm((f) => ({ ...f, name: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label className="text-xs font-medium text-text-muted">이메일</label>
<input
type="email"
value={editForm.email}
onChange={(e) => setEditForm((f) => ({ ...f, email: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
/>
<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="text-xs font-medium text-text-muted">권한</label>
<select
value={editForm.roleCode}
onChange={(e) => setEditForm((f) => ({ ...f, roleCode: e.target.value as RoleCode }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-primary"
>
<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="text-xs font-medium text-text-muted">상태</label>
<select
value={editForm.empStatus}
onChange={(e) => setEditForm((f) => ({ ...f, empStatus: e.target.value as EmpStatus }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-primary"
>
<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}