Files
sam-react-prod/src/components/hr/EmployeeManagement/EmployeeDialog.tsx
유병철 c2ed71540f feat(WEB): DatePicker 공통화 및 공정관리/작업자화면 대폭 개선
DatePicker 공통화:
- date-picker.tsx 공통 컴포넌트 신규 추가
- 전체 폼 컴포넌트 DatePicker 통일 적용 (50+ 파일)
- DateRangeSelector 개선

공정관리:
- RuleModal 대폭 리팩토링 (-592줄 → 간소화)
- ProcessForm, StepForm 개선
- ProcessDetail 수정, actions 확장

작업자화면:
- WorkerScreen 기능 대폭 확장 (+543줄)
- WorkItemCard 개선
- types 확장

회계/인사/영업/품질:
- BadDebtDetail, BillDetail, DepositDetail, SalesDetail 등 DatePicker 적용
- EmployeeForm, VacationDialog 등 DatePicker 적용
- OrderRegistration, QuoteRegistration DatePicker 적용
- InspectionCreate, InspectionDetail DatePicker 적용

공사관리/CEO대시보드:
- BiddingDetail, ContractDetail, HandoverReport 등 DatePicker 적용
- ScheduleDetailModal, TodayIssueSection 개선

기타:
- WorkOrderCreate/Edit/Detail/List 개선
- ShipmentCreate/Edit, ReceivingDetail 개선
- calendar, calendarEvents 수정
- datepicker 마이그레이션 체크리스트 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:48:00 +09:00

582 lines
21 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { DatePicker } from '@/components/ui/date-picker';
import { Label } from '@/components/ui/label';
import { CurrencyInput } from '@/components/ui/currency-input';
import { PhoneInput } from '@/components/ui/phone-input';
import { PersonalNumberInput } from '@/components/ui/personal-number-input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { Plus, Trash2 } from 'lucide-react';
import type {
EmployeeDialogProps,
EmployeeFormData,
DepartmentPosition,
} from './types';
import {
EMPLOYMENT_TYPE_LABELS,
GENDER_LABELS,
USER_ROLE_LABELS,
USER_ACCOUNT_STATUS_LABELS,
EMPLOYEE_STATUS_LABELS,
} from './types';
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',
clockInLocation: '',
clockOutLocation: '',
resignationDate: '',
resignationReason: '',
};
export function EmployeeDialog({
open,
onOpenChange,
mode,
employee,
onSave,
fieldSettings,
}: EmployeeDialogProps) {
const [formData, setFormData] = useState<EmployeeFormData>(initialFormData);
// 모드별 타이틀
const title = {
create: '사원 등록',
edit: '사원 수정',
view: '사원 상세',
}[mode];
const isViewMode = mode === 'view';
// 데이터 초기화
useEffect(() => {
if (open && employee && mode !== 'create') {
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',
clockInLocation: employee.clockInLocation || '',
clockOutLocation: employee.clockOutLocation || '',
resignationDate: employee.resignationDate || '',
resignationReason: employee.resignationReason || '',
});
} else if (open && mode === 'create') {
setFormData(initialFormData);
}
}, [open, 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 = () => {
onSave(formData);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
{mode === 'create' ? '새로운 사원 정보를 입력합니다' : '사원 정보를 확인/수정합니다'}
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* 기본 사원정보 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold"> </h3>
<div className="grid 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)}
disabled={isViewMode}
placeholder="이름을 입력하세요"
/>
</div>
<div className="space-y-2">
<Label htmlFor="residentNumber"></Label>
<PersonalNumberInput
id="residentNumber"
value={formData.residentNumber}
onChange={(value) => handleChange('residentNumber', value)}
disabled={isViewMode}
placeholder="000000-0000000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<PhoneInput
id="phone"
value={formData.phone}
onChange={(value) => handleChange('phone', value)}
disabled={isViewMode}
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)}
disabled={isViewMode}
placeholder="email@company.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="salary"></Label>
<CurrencyInput
id="salary"
value={formData.salary ? Number(formData.salary) : undefined}
onChange={(value) => handleChange('salary', value?.toString() ?? '')}
disabled={isViewMode}
placeholder="연봉"
/>
</div>
</div>
{/* 급여 계좌 */}
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-3 gap-2">
<Input
value={formData.bankAccount.bankName}
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, bankName: e.target.value })}
disabled={isViewMode}
placeholder="은행명"
/>
<Input
value={formData.bankAccount.accountNumber}
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, accountNumber: e.target.value })}
disabled={isViewMode}
placeholder="계좌번호"
/>
<Input
value={formData.bankAccount.accountHolder}
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, accountHolder: e.target.value })}
disabled={isViewMode}
placeholder="예금주"
/>
</div>
</div>
</div>
<Separator />
{/* 선택적 필드 (설정에 따라 표시) */}
{(fieldSettings.showEmployeeCode || fieldSettings.showGender || fieldSettings.showAddress) && (
<>
<div className="space-y-4">
<h3 className="text-sm font-semibold"> </h3>
<div className="grid 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)}
disabled={isViewMode}
placeholder="자동생성 또는 직접입력"
/>
</div>
)}
{fieldSettings.showGender && (
<div className="space-y-2">
<Label htmlFor="gender"></Label>
<Select
value={formData.gender}
onValueChange={(value) => handleChange('gender', value)}
disabled={isViewMode}
>
<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 })}
disabled={isViewMode}
placeholder="우편번호"
className="w-32"
/>
<Button variant="outline" size="sm" disabled={isViewMode}>
</Button>
</div>
<Input
value={formData.address.address1}
onChange={(e) => handleChange('address', { ...formData.address, address1: e.target.value })}
disabled={isViewMode}
placeholder="기본주소"
/>
<Input
value={formData.address.address2}
onChange={(e) => handleChange('address', { ...formData.address, address2: e.target.value })}
disabled={isViewMode}
placeholder="상세주소"
/>
</div>
)}
</div>
<Separator />
</>
)}
{/* 인사 정보 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold"> </h3>
<div className="grid grid-cols-2 gap-4">
{fieldSettings.showHireDate && (
<div className="space-y-2">
<Label htmlFor="hireDate"></Label>
<DatePicker
value={formData.hireDate}
onChange={(date) => handleChange('hireDate', date)}
disabled={isViewMode}
/>
</div>
)}
{fieldSettings.showEmploymentType && (
<div className="space-y-2">
<Label htmlFor="employmentType"></Label>
<Select
value={formData.employmentType}
onValueChange={(value) => handleChange('employmentType', value)}
disabled={isViewMode}
>
<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)}
disabled={isViewMode}
placeholder="직급 입력"
/>
</div>
)}
{fieldSettings.showStatus && (
<div className="space-y-2">
<Label htmlFor="status"></Label>
<Select
value={formData.status}
onValueChange={(value) => handleChange('status', value)}
disabled={isViewMode}
>
<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>
{!isViewMode && (
<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">/ </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)}
disabled={isViewMode}
placeholder="부서명"
className="flex-1"
/>
<Input
value={dp.positionName}
onChange={(e) => handleDepartmentPositionChange(dp.id, 'positionName', e.target.value)}
disabled={isViewMode}
placeholder="직책"
className="flex-1"
/>
{!isViewMode && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveDepartmentPosition(dp.id)}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
<Separator />
{/* 사용자 정보 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold"> </h3>
{!isViewMode && (
<div className="flex items-center gap-2">
<Switch
id="hasUserAccount"
checked={formData.hasUserAccount}
onCheckedChange={(checked) => handleChange('hasUserAccount', checked)}
/>
<Label htmlFor="hasUserAccount" className="text-sm">
</Label>
</div>
)}
</div>
{(formData.hasUserAccount || (isViewMode && employee?.userInfo)) && (
<div className="grid 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)}
disabled={isViewMode}
placeholder="사용자 아이디"
/>
</div>
{!isViewMode && 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="비밀번호"
/>
</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)}
disabled={isViewMode}
>
<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)}
disabled={isViewMode}
>
<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>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
{isViewMode ? '닫기' : '취소'}
</Button>
{!isViewMode && (
<Button onClick={handleSubmit}>
{mode === 'create' ? '등록' : '저장'}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}