feat: add employee fields (birthDate, education, residence) + emergency contacts
This commit is contained in:
@@ -2,18 +2,27 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { UserCircle, Mail, Briefcase, Calendar, Lock, Shield, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
||||
import { UserCircle, Mail, Briefcase, Calendar, Lock, Shield, CheckCircle, XCircle, AlertCircle, Phone, Plus, Pencil, Trash2, X, Save, GraduationCap, MapPin } from 'lucide-react';
|
||||
import employeeService from '@/services/employeeService';
|
||||
import type { ApiResponse, Employee, DispatchHistory } from '@/types';
|
||||
import type { ApiResponse, Employee, DispatchHistory, EmergencyContact } from '@/types';
|
||||
|
||||
interface EmployeeWithId extends Employee {
|
||||
empId: number;
|
||||
}
|
||||
|
||||
const EDUCATION_LABELS: Record<string, string> = {
|
||||
HIGH_SCHOOL: '고졸',
|
||||
COLLEGE: '전문대',
|
||||
BACHELOR: '학사',
|
||||
MASTER: '석사',
|
||||
DOCTOR: '박사',
|
||||
};
|
||||
|
||||
export default function ProfilePage() {
|
||||
const router = useRouter();
|
||||
const [employee, setEmployee] = useState<EmployeeWithId | null>(null);
|
||||
const [dispatchHistories, setDispatchHistories] = useState<DispatchHistory[]>([]);
|
||||
const [emergencyContacts, setEmergencyContacts] = useState<EmergencyContact[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -25,6 +34,13 @@ export default function ProfilePage() {
|
||||
const [passwordSuccess, setPasswordSuccess] = useState<string | null>(null);
|
||||
const [isChangingPassword, setIsChangingPassword] = useState(false);
|
||||
|
||||
// Emergency contact modal state
|
||||
const [contactModalOpen, setContactModalOpen] = useState(false);
|
||||
const [editingContact, setEditingContact] = useState<EmergencyContact | null>(null);
|
||||
const [contactForm, setContactForm] = useState({ name: '', relation: '', phone: '' });
|
||||
const [contactFormError, setContactFormError] = useState<string | null>(null);
|
||||
const [isSavingContact, setIsSavingContact] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile();
|
||||
}, []);
|
||||
@@ -43,6 +59,13 @@ export default function ProfilePage() {
|
||||
const data = historyRes.data;
|
||||
setDispatchHistories(Array.isArray(data) ? data : []);
|
||||
}
|
||||
|
||||
// Load emergency contacts
|
||||
const contactRes = await employeeService.getEmergencyContacts(empData.empId);
|
||||
if (contactRes.success) {
|
||||
const data = contactRes.data;
|
||||
setEmergencyContacts(Array.isArray(data) ? data : []);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError('프로필 정보를 불러오는데 실패했습니다.');
|
||||
@@ -83,6 +106,82 @@ export default function ProfilePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const openContactModal = (contact?: EmergencyContact) => {
|
||||
if (contact) {
|
||||
setEditingContact(contact);
|
||||
setContactForm({
|
||||
name: contact.name,
|
||||
relation: contact.relation || '',
|
||||
phone: contact.phone || '',
|
||||
});
|
||||
} else {
|
||||
setEditingContact(null);
|
||||
setContactForm({ name: '', relation: '', phone: '' });
|
||||
}
|
||||
setContactFormError(null);
|
||||
setContactModalOpen(true);
|
||||
};
|
||||
|
||||
const closeContactModal = () => {
|
||||
setContactModalOpen(false);
|
||||
setEditingContact(null);
|
||||
setContactForm({ name: '', relation: '', phone: '' });
|
||||
setContactFormError(null);
|
||||
};
|
||||
|
||||
const handleSaveContact = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setContactFormError(null);
|
||||
|
||||
if (!contactForm.name.trim()) {
|
||||
setContactFormError('이름을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!contactForm.phone.trim()) {
|
||||
setContactFormError('전화번호를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSavingContact(true);
|
||||
if (editingContact) {
|
||||
const res = await employeeService.updateEmergencyContact(employee!.empId, editingContact.id, contactForm);
|
||||
if (res.success) {
|
||||
const contactRes = await employeeService.getEmergencyContacts(employee!.empId);
|
||||
if (contactRes.success) {
|
||||
setEmergencyContacts(Array.isArray(contactRes.data) ? contactRes.data : []);
|
||||
}
|
||||
closeContactModal();
|
||||
}
|
||||
} else {
|
||||
const res = await employeeService.createEmergencyContact(employee!.empId, contactForm);
|
||||
if (res.success) {
|
||||
const contactRes = await employeeService.getEmergencyContacts(employee!.empId);
|
||||
if (contactRes.success) {
|
||||
setEmergencyContacts(Array.isArray(contactRes.data) ? contactRes.data : []);
|
||||
}
|
||||
closeContactModal();
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
setContactFormError(err.response?.data?.message || '저장에 실패했습니다.');
|
||||
} finally {
|
||||
setIsSavingContact(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteContact = async (contactId: number) => {
|
||||
if (!confirm('비상 연락처를 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const res = await employeeService.deleteEmergencyContact(employee!.empId, contactId);
|
||||
if (res.success) {
|
||||
setEmergencyContacts((prev) => prev.filter((c) => c.id !== contactId));
|
||||
}
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.message || '삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleBadgeColor = (roleCode: string) => {
|
||||
switch (roleCode) {
|
||||
case 'ADMIN':
|
||||
@@ -122,6 +221,11 @@ export default function ProfilePage() {
|
||||
|
||||
const formatInitial = (name: string) => name.charAt(0).toUpperCase();
|
||||
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
||||
@@ -152,7 +256,7 @@ export default function ProfilePage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-4 sm:p-6 lg:p-8">
|
||||
<div className="max-w-7xl mx-auto space-y-8">
|
||||
<div className="max-w-5xl mx-auto space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="font-outfit text-2xl font-semibold text-text-main">내 프로필</h1>
|
||||
@@ -226,6 +330,122 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Personal Info */}
|
||||
<div className="bg-surface border border-border rounded-2xl p-6 shadow-soft">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<UserCircle className="w-5 h-5 text-primary" />
|
||||
<h2 className="font-outfit text-lg font-semibold text-text-main">개인 정보</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-text-muted text-xs mb-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>생년월일</span>
|
||||
</div>
|
||||
<p className="text-text-main font-medium">{formatDate(employee.birthDate)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-text-muted text-xs mb-1">
|
||||
<GraduationCap className="w-4 h-4" />
|
||||
<span>학력</span>
|
||||
</div>
|
||||
<p className="text-text-main font-medium">{employee.education ? (EDUCATION_LABELS[employee.education] || employee.education) : '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-text-muted text-xs mb-1">
|
||||
<GraduationCap className="w-4 h-4" />
|
||||
<span>학과</span>
|
||||
</div>
|
||||
<p className="text-text-main font-medium">{employee.major || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-text-muted text-xs mb-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>졸업일</span>
|
||||
</div>
|
||||
<p className="text-text-main font-medium">{formatDate(employee.graduationDate)}</p>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<div className="flex items-center gap-2 text-text-muted text-xs mb-1">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span>현재 거주지</span>
|
||||
</div>
|
||||
<p className="text-text-main font-medium">{employee.residence || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Emergency Contacts */}
|
||||
<div className="bg-surface border border-border rounded-2xl p-6 shadow-soft">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Phone className="w-5 h-5 text-primary" />
|
||||
<h2 className="font-outfit text-lg font-semibold text-text-main">비상 연락처</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => openContactModal()}
|
||||
className="flex items-center gap-1 px-4 py-2 bg-primary text-white rounded-xl text-sm font-bold hover:bg-primary/90 transition-colors shadow-lg shadow-primary/20"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{emergencyContacts.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Phone className="w-12 h-12 text-text-muted/30 mx-auto mb-4" />
|
||||
<p className="text-text-muted">등록된 비상 연락처가 없습니다.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{emergencyContacts
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((contact) => (
|
||||
<div
|
||||
key={contact.id}
|
||||
className="flex items-center justify-between border border-border rounded-xl p-4 hover:bg-surface-container-low transition-colors"
|
||||
>
|
||||
<div className="flex-1 grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<span className="text-text-muted text-xs">이름</span>
|
||||
<p className="text-text-main font-medium">{contact.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted text-xs">관계</span>
|
||||
<p className="text-text-main font-medium">{contact.relation || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted text-xs">전화번호</span>
|
||||
<p className="text-text-main font-medium">{contact.phone || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<button
|
||||
onClick={() => openContactModal(contact)}
|
||||
className="p-2 text-text-muted hover:text-primary hover:bg-primary/10 rounded-lg transition-colors"
|
||||
title="수정"
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteContact(contact.id)}
|
||||
className="p-2 text-text-muted hover:text-error hover:bg-error/10 rounded-lg transition-colors"
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dispatch History */}
|
||||
<div className="bg-surface border border-border rounded-2xl p-6 shadow-soft">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@@ -355,6 +575,93 @@ export default function ProfilePage() {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Emergency Contact Modal */}
|
||||
{contactModalOpen && (
|
||||
<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-md">
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<h3 className="font-outfit text-lg font-semibold text-text-main">
|
||||
{editingContact ? '비상 연락처 수정' : '비상 연락처 추가'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={closeContactModal}
|
||||
className="p-2 text-text-muted hover:text-text-main hover:bg-surface-container rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveContact} className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-muted mb-2">
|
||||
이름 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={contactForm.name}
|
||||
onChange={(e) => setContactForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-surface-container-lowest text-text-main placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
placeholder="이름을 입력하세요"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-muted mb-2">
|
||||
관계
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={contactForm.relation}
|
||||
onChange={(e) => setContactForm((prev) => ({ ...prev, relation: e.target.value }))}
|
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-surface-container-lowest text-text-main placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
placeholder="예: 배우자, 부모, 형제"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-muted mb-2">
|
||||
전화번호 *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={contactForm.phone}
|
||||
onChange={(e) => setContactForm((prev) => ({ ...prev, phone: e.target.value }))}
|
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-surface-container-lowest text-text-main placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
||||
placeholder="010-0000-0000"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{contactFormError && (
|
||||
<div className="flex items-center gap-2 text-error text-sm bg-error-container/30 px-4 py-3 rounded-xl">
|
||||
<XCircle className="w-4 h-4" />
|
||||
<span>{contactFormError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeContactModal}
|
||||
className="flex-1 py-3 border border-border text-text-main rounded-xl font-bold text-sm hover:bg-surface-container transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSavingContact}
|
||||
className="flex-1 flex items-center justify-center gap-2 py-3 bg-primary text-white rounded-xl font-bold text-sm hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg shadow-primary/20"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{isSavingContact ? '저장중...' : '저장'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user