feat: 직원 동적 프로필 조회 페이지(/profile/[empId]) 구현하여 404 에러 해결
This commit is contained in:
@@ -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<Employee | null>(null);
|
||||
const [dispatchHistories, setDispatchHistories] = useState<DispatchHistory[]>([]);
|
||||
const [emergencyContacts, setEmergencyContacts] = useState<EmergencyContactItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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<EmergencyContactItem | null>(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<any>(`/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<any>(`/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 (
|
||||
<div className="p-4 sm:p-6 lg:p-8 flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-10 h-10 text-primary animate-spin mx-auto mb-3" />
|
||||
<p className="text-text-muted text-sm">직원 프로필 정보를 불러오는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !employee) {
|
||||
return (
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-surface rounded-2xl border border-border p-12 text-center shadow-card">
|
||||
<AlertCircle className="w-12 h-12 text-status-critical mx-auto mb-4" />
|
||||
<h3 className="font-outfit text-lg font-semibold text-text-main mb-1">
|
||||
프로필을 찾을 수 없습니다
|
||||
</h3>
|
||||
<p className="text-text-muted text-sm mb-6">{error || '해당 직원의 정보가 존재하지 않습니다.'}</p>
|
||||
<button
|
||||
onClick={() => router.push('/employees')}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-primary text-white rounded-xl text-sm font-medium hover:bg-primary/90 transition-all"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
직원 목록으로 돌아가기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Navigation Bar */}
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => router.push('/employees')}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl border border-border bg-white text-text-muted text-xs font-medium hover:bg-gray-50 transition-all"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
직원 목록
|
||||
</button>
|
||||
<div className="text-xs text-text-muted">직원 ID: #{employee.empId}</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Header Card */}
|
||||
<div className="bg-surface rounded-2xl shadow-card border border-border p-6">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-5">
|
||||
{employee.profileImage ? (
|
||||
<img
|
||||
src={employee.profileImage}
|
||||
alt={employee.name}
|
||||
className="w-20 h-20 rounded-2xl object-cover border-2 border-border"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-20 h-20 rounded-2xl bg-indigo-500 flex items-center justify-center text-white font-bold text-2xl">
|
||||
{employee.name[0]}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1.5">
|
||||
<h2 className="font-outfit text-2xl font-semibold text-text-main tracking-tight">
|
||||
{employee.name}
|
||||
</h2>
|
||||
<span
|
||||
className={`px-2.5 py-0.5 rounded-full text-xs font-semibold border ${getRoleBadge(
|
||||
employee.roleCode
|
||||
)}`}
|
||||
>
|
||||
{employee.roleCode}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2.5 py-0.5 rounded-full text-xs font-semibold border ${getStatusBadge(
|
||||
employee.empStatus
|
||||
)}`}
|
||||
>
|
||||
{employee.empStatus === 'ACTIVE'
|
||||
? '재직'
|
||||
: employee.empStatus === 'ON_LEAVE'
|
||||
? '휴직'
|
||||
: '퇴사'}
|
||||
</span>
|
||||
{employee.dispatchStatus === 'DISPATCHED' && (
|
||||
<span className="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-blue-50 text-blue-700 border border-blue-200">
|
||||
파견중
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-text-muted">{employee.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Info & Details */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Basic Contact Info */}
|
||||
<div className="bg-surface rounded-2xl shadow-card border border-border p-6">
|
||||
<h3 className="font-outfit text-base font-semibold text-text-main mb-4 flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-primary" />
|
||||
기본 연락처 정보
|
||||
</h3>
|
||||
<div className="space-y-3.5 text-sm">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2.5">
|
||||
<span className="text-text-muted">이메일</span>
|
||||
<span className="font-medium text-text-main">{employee.email || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2.5">
|
||||
<span className="text-text-muted">전화번호</span>
|
||||
<div>
|
||||
{employee.phone ? (
|
||||
revealedPhone ? (
|
||||
<span className="font-mono text-text-main font-semibold">{revealedPhone}</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowPhoneDialog(true)}
|
||||
className="inline-flex items-center gap-1 text-xs text-primary font-medium hover:underline"
|
||||
>
|
||||
<Shield className="w-3.5 h-3.5" />
|
||||
🔒 전화번호 보기
|
||||
</button>
|
||||
)
|
||||
) : (
|
||||
<span className="text-text-muted">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-text-muted">직원 구분</span>
|
||||
<span className="font-medium text-text-main">{employee.roleCode}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions / Associated Views */}
|
||||
<div className="bg-surface rounded-2xl shadow-card border border-border p-6">
|
||||
<h3 className="font-outfit text-base font-semibold text-text-main mb-4 flex items-center gap-2">
|
||||
<ExternalLink className="w-4 h-4 text-primary" />
|
||||
연관 페이지 이동
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={() => router.push(`/dispatch?empId=${empId}`)}
|
||||
className="p-3 rounded-xl border border-border bg-white text-left hover:bg-gray-50 transition-all text-xs font-medium text-text-main flex items-center justify-between"
|
||||
>
|
||||
<span>파견 관리</span>
|
||||
<span className="text-text-muted">↗</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(`/itam?userId=${empId}`)}
|
||||
className="p-3 rounded-xl border border-border bg-white text-left hover:bg-gray-50 transition-all text-xs font-medium text-text-main flex items-center justify-between"
|
||||
>
|
||||
<span>지급 장비 (ITAM)</span>
|
||||
<span className="text-text-muted">↗</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(`/issues?requesterId=${empId}`)}
|
||||
className="p-3 rounded-xl border border-border bg-white text-left hover:bg-gray-50 transition-all text-xs font-medium text-text-main flex items-center justify-between"
|
||||
>
|
||||
<span>요청 SR 티켓</span>
|
||||
<span className="text-text-muted">↗</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(`/work-reports?userId=${empId}`)}
|
||||
className="p-3 rounded-xl border border-border bg-white text-left hover:bg-gray-50 transition-all text-xs font-medium text-text-main flex items-center justify-between"
|
||||
>
|
||||
<span>업무 보고서</span>
|
||||
<span className="text-text-muted">↗</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Emergency Contacts Section */}
|
||||
<div className="bg-surface rounded-2xl shadow-card border border-border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-outfit text-base font-semibold text-text-main flex items-center gap-2">
|
||||
<Phone className="w-4 h-4 text-primary" />
|
||||
비상 연락망
|
||||
</h3>
|
||||
{canManage && (
|
||||
<button
|
||||
onClick={() => openContactModal()}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-xl bg-primary/10 text-primary text-xs font-semibold hover:bg-primary/20 transition-all"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
연락처 추가
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{emergencyContacts.length === 0 ? (
|
||||
<div className="p-8 text-center text-text-muted text-xs bg-gray-50/50 rounded-xl border border-dashed border-border">
|
||||
등록된 비상연락처 정보가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{emergencyContacts.map((contact, idx) => {
|
||||
const nameStr = contact.contactName || contact.name || '연락처';
|
||||
const relStr = contact.relationship || contact.relation || '관계';
|
||||
return (
|
||||
<div
|
||||
key={contact.contactId || contact.id || idx}
|
||||
className="p-4 rounded-xl border border-border bg-white flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-text-main">
|
||||
{nameStr} <span className="text-xs text-text-muted font-normal">({relStr})</span>
|
||||
</p>
|
||||
<p className="text-xs text-text-muted font-mono mt-0.5">{contact.phone}</p>
|
||||
</div>
|
||||
{canManage && (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => openContactModal(contact)}
|
||||
className="p-1.5 text-text-muted hover:text-primary transition-colors"
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteContact(contact)}
|
||||
className="p-1.5 text-text-muted hover:text-status-critical transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dispatch Histories Section */}
|
||||
<div className="bg-surface rounded-2xl shadow-card border border-border p-6">
|
||||
<h3 className="font-outfit text-base font-semibold text-text-main mb-4 flex items-center gap-2">
|
||||
<Briefcase className="w-4 h-4 text-primary" />
|
||||
파견 프로젝트 이력
|
||||
</h3>
|
||||
{dispatchHistories.length === 0 ? (
|
||||
<div className="p-8 text-center text-text-muted text-xs bg-gray-50/50 rounded-xl border border-dashed border-border">
|
||||
파견 이력 데이터가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{dispatchHistories.map((dh) => (
|
||||
<div
|
||||
key={dh.dispatchId}
|
||||
className="p-4 rounded-xl border border-border bg-white flex flex-col sm:flex-row sm:items-center justify-between gap-2"
|
||||
>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-text-main">
|
||||
{dh.projectName || '프로젝트'}
|
||||
</h4>
|
||||
{dh.roleTitle && (
|
||||
<p className="text-xs text-primary font-medium mt-0.5">{dh.roleTitle}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted">
|
||||
{dh.startDate} ~ {dh.endDate || '진행중'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone Reveal Dialog */}
|
||||
{showPhoneDialog && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
|
||||
onClick={() => setShowPhoneDialog(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-surface rounded-2xl shadow-xl max-w-sm w-full p-6 animate-in fade-in zoom-in duration-200"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h4 className="font-outfit text-lg font-semibold text-text-main mb-1">
|
||||
전화번호 열람 요청
|
||||
</h4>
|
||||
<p className="text-xs text-text-muted mb-4">
|
||||
개인정보 보호를 위해 전화번호 열람 사유를 입력해주세요. (감사로그 기록됨)
|
||||
</p>
|
||||
<textarea
|
||||
value={phoneReason}
|
||||
onChange={(e) => setPhoneReason(e.target.value)}
|
||||
placeholder="예: 프로젝트 비상 연락 필요"
|
||||
className="w-full border border-border rounded-xl p-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary h-24 resize-none mb-4"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setShowPhoneDialog(false)}
|
||||
className="px-4 py-2 rounded-xl text-xs text-text-muted hover:bg-gray-100 transition-all"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRequestPhone}
|
||||
disabled={!phoneReason.trim() || phoneLoading}
|
||||
className="px-4 py-2 rounded-xl text-xs font-semibold bg-primary text-white hover:bg-primary/90 transition-all disabled:opacity-50"
|
||||
>
|
||||
{phoneLoading ? '조회 중...' : '조회하기'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Emergency Contact Edit Modal */}
|
||||
{contactModalOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
|
||||
onClick={() => setContactModalOpen(false)}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSaveContact}
|
||||
className="bg-surface rounded-2xl shadow-xl max-w-sm w-full p-6 animate-in fade-in zoom-in duration-200"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="font-outfit text-lg font-semibold text-text-main">
|
||||
{editingContact ? '비상연락처 수정' : '비상연락처 추가'}
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setContactModalOpen(false)}
|
||||
className="p-1 text-text-muted hover:text-text-main"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3 mb-5">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1">
|
||||
관계 (예: 부, 모, 배우자)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={contactRelation}
|
||||
onChange={(e) => setContactRelation(e.target.value)}
|
||||
className="w-full border border-border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="관계"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1">
|
||||
성명
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={contactName}
|
||||
onChange={(e) => setContactName(e.target.value)}
|
||||
className="w-full border border-border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="이름"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-muted mb-1">
|
||||
연락처
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={contactPhone}
|
||||
onChange={(e) => setContactPhone(e.target.value)}
|
||||
className="w-full border border-border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="010-0000-0000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setContactModalOpen(false)}
|
||||
className="px-4 py-2 rounded-xl text-xs text-text-muted hover:bg-gray-100 transition-all"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={contactSubmitting}
|
||||
className="px-4 py-2 rounded-xl text-xs font-semibold bg-primary text-white hover:bg-primary/90 transition-all disabled:opacity-50"
|
||||
>
|
||||
{contactSubmitting ? '저장 중...' : '저장'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user