Files
sam-react-prod/src/components/hr/EmployeeManagement/EmployeeForm.tsx

628 lines
23 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Users, Plus, Trash2, ArrowLeft, Save, Camera, User } from 'lucide-react';
import type {
Employee,
EmployeeFormData,
DepartmentPosition,
FieldSettings,
} from './types';
import {
EMPLOYMENT_TYPE_LABELS,
GENDER_LABELS,
USER_ROLE_LABELS,
USER_ACCOUNT_STATUS_LABELS,
EMPLOYEE_STATUS_LABELS,
DEFAULT_FIELD_SETTINGS,
} from './types';
interface EmployeeFormProps {
mode: 'create' | 'edit';
employee?: Employee;
onSave: (data: EmployeeFormData) => void;
fieldSettings?: FieldSettings;
}
const initialFormData: EmployeeFormData = {
name: '',
residentNumber: '',
phone: '',
email: '',
salary: '',
bankAccount: { bankName: '', accountNumber: '', accountHolder: '' },
profileImage: '',
employeeCode: '',
gender: '',
address: { zipCode: '', address1: '', address2: '' },
hireDate: '',
employmentType: '',
rank: '',
status: 'active',
departmentPositions: [],
hasUserAccount: false,
userId: '',
password: '',
confirmPassword: '',
role: 'user',
accountStatus: 'active',
};
export function EmployeeForm({
mode,
employee,
onSave,
fieldSettings = DEFAULT_FIELD_SETTINGS,
}: EmployeeFormProps) {
const router = useRouter();
const [formData, setFormData] = useState<EmployeeFormData>(initialFormData);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const title = mode === 'create' ? '사원 등록' : '사원 수정';
// 데이터 초기화
useEffect(() => {
if (employee && mode === 'edit') {
setFormData({
name: employee.name,
residentNumber: employee.residentNumber || '',
phone: employee.phone || '',
email: employee.email || '',
salary: employee.salary?.toString() || '',
bankAccount: employee.bankAccount || { bankName: '', accountNumber: '', accountHolder: '' },
profileImage: employee.profileImage || '',
employeeCode: employee.employeeCode || '',
gender: employee.gender || '',
address: employee.address || { zipCode: '', address1: '', address2: '' },
hireDate: employee.hireDate || '',
employmentType: employee.employmentType || '',
rank: employee.rank || '',
status: employee.status,
departmentPositions: employee.departmentPositions || [],
hasUserAccount: !!employee.userInfo,
userId: employee.userInfo?.userId || '',
password: '',
confirmPassword: '',
role: employee.userInfo?.role || 'user',
accountStatus: employee.userInfo?.accountStatus || 'active',
});
}
}, [employee, mode]);
// 입력 변경 핸들러
const handleChange = (field: keyof EmployeeFormData, value: unknown) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
// 부서/직책 추가
const handleAddDepartmentPosition = () => {
const newDP: DepartmentPosition = {
id: String(Date.now()),
departmentId: '',
departmentName: '',
positionId: '',
positionName: '',
};
setFormData(prev => ({
...prev,
departmentPositions: [...prev.departmentPositions, newDP],
}));
};
// 부서/직책 삭제
const handleRemoveDepartmentPosition = (id: string) => {
setFormData(prev => ({
...prev,
departmentPositions: prev.departmentPositions.filter(dp => dp.id !== id),
}));
};
// 부서/직책 변경
const handleDepartmentPositionChange = (id: string, field: keyof DepartmentPosition, value: string) => {
setFormData(prev => ({
...prev,
departmentPositions: prev.departmentPositions.map(dp =>
dp.id === id ? { ...dp, [field]: value } : dp
),
}));
};
// 저장
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(formData);
};
// 취소
const handleCancel = () => {
router.back();
};
// 프로필 이미지 업로드 핸들러
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setPreviewImage(reader.result as string);
handleChange('profileImage', reader.result as string);
};
reader.readAsDataURL(file);
}
};
// 프로필 이미지 삭제 핸들러
const handleRemoveImage = () => {
setPreviewImage(null);
handleChange('profileImage', '');
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<PageLayout>
<PageHeader
title={title}
description={mode === 'create' ? '새로운 사원 정보를 입력합니다' : '사원 정보를 수정합니다'}
icon={Users}
/>
<form onSubmit={handleSubmit} className="space-y-6">
{/* 사원 정보 - 프로필 사진 + 기본 정보 */}
<Card>
<CardHeader className="bg-black text-white rounded-t-lg">
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row gap-6">
{/* 프로필 사진 영역 */}
{fieldSettings.showProfileImage && (
<div className="flex flex-col items-center gap-3">
<div className="relative w-32 h-32 rounded-full border-2 border-dashed border-gray-300 bg-gray-50 flex items-center justify-center overflow-hidden">
{previewImage || formData.profileImage ? (
<Image
src={previewImage || formData.profileImage}
alt="프로필 사진"
fill
className="object-cover"
/>
) : (
<User className="w-12 h-12 text-gray-400" />
)}
</div>
<div className="flex gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
id="profile-image-input"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
>
<Camera className="w-4 h-4 mr-1" />
</Button>
{(previewImage || formData.profileImage) && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleRemoveImage}
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
</div>
)}
{/* 기본 정보 필드들 */}
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="이름을 입력하세요"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="residentNumber"></Label>
<Input
id="residentNumber"
value={formData.residentNumber}
onChange={(e) => handleChange('residentNumber', e.target.value)}
placeholder="000000-0000000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
id="phone"
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
placeholder="010-0000-0000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="email@company.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="salary"></Label>
<Input
id="salary"
type="number"
value={formData.salary}
onChange={(e) => handleChange('salary', e.target.value)}
placeholder="연봉 (원)"
/>
</div>
</div>
</div>
{/* 급여 계좌 */}
<div className="space-y-2 mt-6">
<Label></Label>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<Input
value={formData.bankAccount.bankName}
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, bankName: e.target.value })}
placeholder="은행명"
/>
<Input
value={formData.bankAccount.accountNumber}
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, accountNumber: e.target.value })}
placeholder="계좌번호"
/>
<Input
value={formData.bankAccount.accountHolder}
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, accountHolder: e.target.value })}
placeholder="예금주"
/>
</div>
</div>
</CardContent>
</Card>
{/* 선택 정보 (사원 상세) */}
{(fieldSettings.showEmployeeCode || fieldSettings.showGender || fieldSettings.showAddress) && (
<Card>
<CardHeader className="bg-black text-white rounded-t-lg">
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent className="pt-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{fieldSettings.showEmployeeCode && (
<div className="space-y-2">
<Label htmlFor="employeeCode"></Label>
<Input
id="employeeCode"
value={formData.employeeCode}
onChange={(e) => handleChange('employeeCode', e.target.value)}
placeholder="자동생성 또는 직접입력"
/>
</div>
)}
{fieldSettings.showGender && (
<div className="space-y-2">
<Label htmlFor="gender"></Label>
<Select
value={formData.gender}
onValueChange={(value) => handleChange('gender', value)}
>
<SelectTrigger>
<SelectValue placeholder="성별 선택" />
</SelectTrigger>
<SelectContent>
{Object.entries(GENDER_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
{fieldSettings.showAddress && (
<div className="space-y-2">
<Label></Label>
<div className="flex gap-2">
<Input
value={formData.address.zipCode}
onChange={(e) => handleChange('address', { ...formData.address, zipCode: e.target.value })}
placeholder="우편번호"
className="w-32"
/>
<Button type="button" variant="outline" size="sm">
</Button>
</div>
<Input
value={formData.address.address1}
onChange={(e) => handleChange('address', { ...formData.address, address1: e.target.value })}
placeholder="기본주소"
/>
<Input
value={formData.address.address2}
onChange={(e) => handleChange('address', { ...formData.address, address2: e.target.value })}
placeholder="상세주소"
/>
</div>
)}
</CardContent>
</Card>
)}
{/* 인사 정보 */}
<Card>
<CardHeader className="bg-black text-white rounded-t-lg">
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent className="pt-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{fieldSettings.showHireDate && (
<div className="space-y-2">
<Label htmlFor="hireDate"></Label>
<Input
id="hireDate"
type="date"
value={formData.hireDate}
onChange={(e) => handleChange('hireDate', e.target.value)}
/>
</div>
)}
{fieldSettings.showEmploymentType && (
<div className="space-y-2">
<Label htmlFor="employmentType"></Label>
<Select
value={formData.employmentType}
onValueChange={(value) => handleChange('employmentType', value)}
>
<SelectTrigger>
<SelectValue placeholder="고용형태 선택" />
</SelectTrigger>
<SelectContent>
{Object.entries(EMPLOYMENT_TYPE_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{fieldSettings.showRank && (
<div className="space-y-2">
<Label htmlFor="rank"></Label>
<Input
id="rank"
value={formData.rank}
onChange={(e) => handleChange('rank', e.target.value)}
placeholder="직급 입력"
/>
</div>
)}
{fieldSettings.showStatus && (
<div className="space-y-2">
<Label htmlFor="status"></Label>
<Select
value={formData.status}
onValueChange={(value) => handleChange('status', value)}
>
<SelectTrigger>
<SelectValue placeholder="상태 선택" />
</SelectTrigger>
<SelectContent>
{Object.entries(EMPLOYEE_STATUS_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
{/* 부서/직책 */}
{(fieldSettings.showDepartment || fieldSettings.showPosition) && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>/</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddDepartmentPosition}
>
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
{formData.departmentPositions.length === 0 ? (
<p className="text-sm text-muted-foreground py-4 text-center border rounded-md">
/
</p>
) : (
<div className="space-y-2">
{formData.departmentPositions.map((dp) => (
<div key={dp.id} className="flex items-center gap-2">
<Input
value={dp.departmentName}
onChange={(e) => handleDepartmentPositionChange(dp.id, 'departmentName', e.target.value)}
placeholder="부서명"
className="flex-1"
/>
<Input
value={dp.positionName}
onChange={(e) => handleDepartmentPositionChange(dp.id, 'positionName', e.target.value)}
placeholder="직책"
className="flex-1"
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveDepartmentPosition(dp.id)}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
</div>
))}
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* 사용자 정보 */}
<Card>
<CardHeader className="bg-black text-white rounded-t-lg">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-medium"> </CardTitle>
<div className="flex items-center gap-2">
<Switch
id="hasUserAccount"
checked={formData.hasUserAccount}
onCheckedChange={(checked) => handleChange('hasUserAccount', checked)}
className="data-[state=checked]:bg-white data-[state=checked]:text-black"
/>
<Label htmlFor="hasUserAccount" className="text-sm font-normal text-white">
</Label>
</div>
</div>
</CardHeader>
{formData.hasUserAccount && (
<CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="userId"> *</Label>
<Input
id="userId"
value={formData.userId}
onChange={(e) => handleChange('userId', e.target.value)}
placeholder="사용자 아이디"
required={formData.hasUserAccount}
/>
</div>
{mode === 'create' && (
<>
<div className="space-y-2">
<Label htmlFor="password"> *</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => handleChange('password', e.target.value)}
placeholder="비밀번호"
required={formData.hasUserAccount}
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword"> </Label>
<Input
id="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={(e) => handleChange('confirmPassword', e.target.value)}
placeholder="비밀번호 확인"
/>
</div>
</>
)}
<div className="space-y-2">
<Label htmlFor="role"></Label>
<Select
value={formData.role}
onValueChange={(value) => handleChange('role', value)}
>
<SelectTrigger>
<SelectValue placeholder="권한 선택" />
</SelectTrigger>
<SelectContent>
{Object.entries(USER_ROLE_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="accountStatus"></Label>
<Select
value={formData.accountStatus}
onValueChange={(value) => handleChange('accountStatus', value)}
>
<SelectTrigger>
<SelectValue placeholder="상태 선택" />
</SelectTrigger>
<SelectContent>
{Object.entries(USER_ACCOUNT_STATUS_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
)}
</Card>
{/* 버튼 영역 */}
<div className="flex items-center justify-between">
<Button type="button" variant="outline" onClick={handleCancel}>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<Button type="submit">
<Save className="w-4 h-4 mr-2" />
{mode === 'create' ? '등록' : '저장'}
</Button>
</div>
</form>
</PageLayout>
);
}