diff --git a/src/app/(main)/profile/[empId]/page.tsx b/src/app/(main)/profile/[empId]/page.tsx new file mode 100644 index 0000000..41cb584 --- /dev/null +++ b/src/app/(main)/profile/[empId]/page.tsx @@ -0,0 +1,623 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { + UserCircle, + Mail, + Briefcase, + Calendar, + Phone, + Plus, + Pencil, + Trash2, + X, + AlertCircle, + ArrowLeft, + Shield, + Loader2, + ExternalLink, +} from 'lucide-react'; +import employeeService from '@/services/employeeService'; +import apiClient from '@/services/apiClient'; +import { useAuthStore } from '@/stores'; +import type { Employee, DispatchHistory } from '@/types'; + +interface EmergencyContactItem { + contactId?: number; + id?: number; + relationship?: string; + relation?: string; + contactName?: string; + name?: string; + phone: string; +} + +export default function DynamicEmployeeProfilePage() { + const params = useParams(); + const router = useRouter(); + const currentUser = useAuthStore((s) => s.user); + + const empId = Number(params?.empId); + + const [employee, setEmployee] = useState(null); + const [dispatchHistories, setDispatchHistories] = useState([]); + const [emergencyContacts, setEmergencyContacts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Phone Reveal Dialog State + const [showPhoneDialog, setShowPhoneDialog] = useState(false); + const [phoneReason, setPhoneReason] = useState(''); + const [revealedPhone, setRevealedPhone] = useState(''); + const [phoneLoading, setPhoneLoading] = useState(false); + + // Emergency Contact Modal State + const [contactModalOpen, setContactModalOpen] = useState(false); + const [editingContact, setEditingContact] = useState(null); + const [contactRelation, setContactRelation] = useState(''); + const [contactName, setContactName] = useState(''); + const [contactPhone, setContactPhone] = useState(''); + const [contactSubmitting, setContactSubmitting] = useState(false); + + const canManage = + currentUser?.roleCode === 'ADMIN' || + currentUser?.roleCode === 'MANAGER' || + currentUser?.empId === empId; + + const loadEmployeeData = async () => { + if (!empId || isNaN(empId)) { + setError('유효하지 않은 직원 ID입니다.'); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const [empRes, historyRes, contactRes] = await Promise.allSettled([ + employeeService.getById(empId), + employeeService.getDispatchHistories(empId), + apiClient.get(`/employees/${empId}/emergency-contacts`), + ]); + + if (empRes.status === 'fulfilled' && empRes.value?.success) { + setEmployee(empRes.value.data); + } else { + setError('직원 정보를 찾을 수 없습니다.'); + return; + } + + if (historyRes.status === 'fulfilled' && historyRes.value?.success) { + setDispatchHistories(Array.isArray(historyRes.value.data) ? historyRes.value.data : []); + } + + if (contactRes.status === 'fulfilled' && contactRes.value?.data) { + setEmergencyContacts(contactRes.value.data); + } + } catch (err) { + console.error(err); + setError('직원 프로필 정보를 불러오는데 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadEmployeeData(); + }, [empId]); + + // Request Phone Number (Audited) + const handleRequestPhone = async () => { + if (!phoneReason.trim()) return; + + try { + setPhoneLoading(true); + const token = localStorage.getItem('accessToken'); + const res = await fetch(`/api/v1/employees/${empId}/request-phone`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ reason: phoneReason.trim() }), + }); + const data = await res.json(); + if (data.success && data.data?.phone) { + setRevealedPhone(data.data.phone); + setShowPhoneDialog(false); + setPhoneReason(''); + } else { + alert(data.message || '전화번호 조회에 실패했습니다.'); + } + } catch { + alert('전화번호 조회 중 오류가 발생했습니다.'); + } finally { + setPhoneLoading(false); + } + }; + + // Contact CRUD + const openContactModal = (contact?: EmergencyContactItem) => { + if (contact) { + setEditingContact(contact); + setContactRelation(contact.relationship || contact.relation || ''); + setContactName(contact.contactName || contact.name || ''); + setContactPhone(contact.phone || ''); + } else { + setEditingContact(null); + setContactRelation(''); + setContactName(''); + setContactPhone(''); + } + setContactModalOpen(true); + }; + + const handleSaveContact = async (e: React.FormEvent) => { + e.preventDefault(); + if (!contactRelation.trim() || !contactName.trim() || !contactPhone.trim()) return; + + try { + setContactSubmitting(true); + const payload = { + relationship: contactRelation.trim(), + contactName: contactName.trim(), + phone: contactPhone.trim(), + }; + + const contactId = editingContact?.contactId || editingContact?.id; + + if (contactId) { + await apiClient.put(`/employees/${empId}/emergency-contacts/${contactId}`, payload); + } else { + await apiClient.post(`/employees/${empId}/emergency-contacts`, payload); + } + + const res = await apiClient.get(`/employees/${empId}/emergency-contacts`); + if (res?.data) setEmergencyContacts(res.data); + setContactModalOpen(false); + } catch (err) { + console.error(err); + alert('비상 연락처 저장에 실패했습니다.'); + } finally { + setContactSubmitting(false); + } + }; + + const handleDeleteContact = async (contactItem: EmergencyContactItem) => { + const contactId = contactItem.contactId || contactItem.id; + if (!contactId || !confirm('이 비상연락처를 삭제하시겠습니까?')) return; + + try { + await apiClient.delete(`/employees/${empId}/emergency-contacts/${contactId}`); + setEmergencyContacts((prev) => + prev.filter((c) => (c.contactId || c.id) !== contactId) + ); + } catch (err) { + console.error(err); + alert('비상연락처 삭제에 실패했습니다.'); + } + }; + + const getRoleBadge = (roleCode: string) => { + switch (roleCode) { + case 'ADMIN': + return 'bg-red-50 text-red-700 border-red-200'; + case 'MANAGER': + return 'bg-indigo-50 text-indigo-700 border-indigo-200'; + default: + return 'bg-gray-100 text-gray-700 border-gray-200'; + } + }; + + const getStatusBadge = (status: string) => { + switch (status) { + case 'ACTIVE': + return 'bg-emerald-50 text-emerald-700 border-emerald-200'; + case 'ON_LEAVE': + return 'bg-amber-50 text-amber-700 border-amber-200'; + default: + return 'bg-gray-100 text-gray-700 border-gray-200'; + } + }; + + if (loading) { + return ( +
+
+ +

직원 프로필 정보를 불러오는 중...

+
+
+ ); + } + + if (error || !employee) { + return ( +
+
+
+ +

+ 프로필을 찾을 수 없습니다 +

+

{error || '해당 직원의 정보가 존재하지 않습니다.'}

+ +
+
+
+ ); + } + + return ( +
+
+ {/* Navigation Bar */} +
+ +
직원 ID: #{employee.empId}
+
+ + {/* Profile Header Card */} +
+
+ {employee.profileImage ? ( + {employee.name} + ) : ( +
+ {employee.name[0]} +
+ )} +
+
+

+ {employee.name} +

+ + {employee.roleCode} + + + {employee.empStatus === 'ACTIVE' + ? '재직' + : employee.empStatus === 'ON_LEAVE' + ? '휴직' + : '퇴사'} + + {employee.dispatchStatus === 'DISPATCHED' && ( + + 파견중 + + )} +
+

{employee.email}

+
+
+
+ + {/* Contact Info & Details */} +
+ {/* Basic Contact Info */} +
+

+ + 기본 연락처 정보 +

+
+
+ 이메일 + {employee.email || '-'} +
+
+ 전화번호 +
+ {employee.phone ? ( + revealedPhone ? ( + {revealedPhone} + ) : ( + + ) + ) : ( + - + )} +
+
+
+ 직원 구분 + {employee.roleCode} +
+
+
+ + {/* Quick Actions / Associated Views */} +
+

+ + 연관 페이지 이동 +

+
+ + + + +
+
+
+ + {/* Emergency Contacts Section */} +
+
+

+ + 비상 연락망 +

+ {canManage && ( + + )} +
+ + {emergencyContacts.length === 0 ? ( +
+ 등록된 비상연락처 정보가 없습니다. +
+ ) : ( +
+ {emergencyContacts.map((contact, idx) => { + const nameStr = contact.contactName || contact.name || '연락처'; + const relStr = contact.relationship || contact.relation || '관계'; + return ( +
+
+

+ {nameStr} ({relStr}) +

+

{contact.phone}

+
+ {canManage && ( +
+ + +
+ )} +
+ ); + })} +
+ )} +
+ + {/* Dispatch Histories Section */} +
+

+ + 파견 프로젝트 이력 +

+ {dispatchHistories.length === 0 ? ( +
+ 파견 이력 데이터가 없습니다. +
+ ) : ( +
+ {dispatchHistories.map((dh) => ( +
+
+

+ {dh.projectName || '프로젝트'} +

+ {dh.roleTitle && ( +

{dh.roleTitle}

+ )} +
+
+ {dh.startDate} ~ {dh.endDate || '진행중'} +
+
+ ))} +
+ )} +
+
+ + {/* Phone Reveal Dialog */} + {showPhoneDialog && ( +
setShowPhoneDialog(false)} + > +
e.stopPropagation()} + > +

+ 전화번호 열람 요청 +

+

+ 개인정보 보호를 위해 전화번호 열람 사유를 입력해주세요. (감사로그 기록됨) +

+