'use client'; import { useState, useCallback, useRef } from 'react'; import { useRouter } from 'next/navigation'; 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 { Checkbox } from '@/components/ui/checkbox'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Trash2, Users, Download } from 'lucide-react'; import type { Employee, CSVEmployeeRow, CSVValidationResult } from './types'; interface CSVUploadPageProps { onUpload: (employees: Employee[]) => void; } export function CSVUploadPage({ onUpload }: CSVUploadPageProps) { const router = useRouter(); const [file, setFile] = useState(null); const [validationResults, setValidationResults] = useState([]); const [isProcessing, setIsProcessing] = useState(false); const [selectedRows, setSelectedRows] = useState>(new Set()); const fileInputRef = useRef(null); // 파일 선택 const handleFileSelect = useCallback((e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (selectedFile) { setFile(selectedFile); setValidationResults([]); setSelectedRows(new Set()); } }, []); // 파일 제거 const handleRemoveFile = useCallback(() => { setFile(null); setValidationResults([]); setSelectedRows(new Set()); if (fileInputRef.current) { fileInputRef.current.value = ''; } }, []); // 파일변환 (CSV 파싱) const handleConvert = async () => { if (!file) return; setIsProcessing(true); try { const text = await file.text(); const lines = text.split('\n').map(line => line.trim()).filter(line => line); if (lines.length < 2) { setValidationResults([]); return; } // 헤더 파싱 const headers = lines[0].split(',').map(h => h.trim()); // 데이터 파싱 및 유효성 검사 const results: CSVValidationResult[] = lines.slice(1).map((line, index) => { const values = line.split(',').map(v => v.trim()); const data: CSVEmployeeRow = { name: values[headers.indexOf('이름')] || values[headers.indexOf('name')] || '', phone: values[headers.indexOf('휴대폰')] || values[headers.indexOf('phone')] || undefined, email: values[headers.indexOf('이메일')] || values[headers.indexOf('email')] || undefined, departmentName: values[headers.indexOf('부서')] || values[headers.indexOf('department')] || undefined, positionName: values[headers.indexOf('직책')] || values[headers.indexOf('position')] || undefined, hireDate: values[headers.indexOf('입사일')] || values[headers.indexOf('hireDate')] || undefined, status: values[headers.indexOf('상태')] || values[headers.indexOf('status')] || undefined, }; // 유효성 검사 const errors: string[] = []; if (!data.name) { errors.push('이름은 필수입니다'); } if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { errors.push('이메일 형식이 올바르지 않습니다'); } if (data.phone && !/^\d{3}-\d{4}-\d{4}$/.test(data.phone)) { errors.push('휴대폰 형식이 올바르지 않습니다 (000-0000-0000)'); } return { row: index + 2, data, isValid: errors.length === 0, errors, }; }); setValidationResults(results); } catch { console.error('CSV 파싱 오류'); } finally { setIsProcessing(false); } }; // 행 선택 const handleSelectRow = useCallback((rowIndex: number, checked: boolean) => { setSelectedRows(prev => { const newSet = new Set(prev); if (checked) { newSet.add(rowIndex); } else { newSet.delete(rowIndex); } return newSet; }); }, []); // 전체 선택 const handleSelectAll = useCallback((checked: boolean) => { if (checked) { const validIndices = validationResults .filter(r => r.isValid) .map((_, index) => index); setSelectedRows(new Set(validIndices)); } else { setSelectedRows(new Set()); } }, [validationResults]); // 양식 다운로드 const handleDownloadTemplate = () => { const headers = ['이름', '휴대폰', '이메일', '부서', '직책', '입사일', '상태']; const sampleData = ['홍길동', '010-1234-5678', 'hong@company.com', '개발팀', '팀원', '2024-01-01', '재직']; const csv = [headers.join(','), sampleData.join(',')].join('\n'); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = '사원등록_양식.csv'; link.click(); }; // 업로드 실행 const handleUpload = () => { const selectedResults = validationResults.filter((_, index) => selectedRows.has(index)); const employees: Employee[] = selectedResults.map((r, index) => ({ id: String(Date.now() + index), name: r.data.name, phone: r.data.phone, email: r.data.email, status: (r.data.status === '재직' || r.data.status === 'active') ? 'active' : (r.data.status === '휴직' || r.data.status === 'leave') ? 'leave' : (r.data.status === '퇴직' || r.data.status === 'resigned') ? 'resigned' : 'active', hireDate: r.data.hireDate, departmentPositions: r.data.departmentName ? [{ id: String(Date.now() + index), departmentId: '', departmentName: r.data.departmentName, positionId: '', positionName: r.data.positionName || '', }] : [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), })); onUpload(employees); router.push('/ko/hr/employee-management'); }; const validCount = validationResults.filter(r => r.isValid).length; const isAllSelected = validCount > 0 && selectedRows.size === validCount; return (
{/* 일괄 등록 카드 */} 일괄 등록 {/* CSV 파일 선택 영역 */}
CSV 파일 CSV 파일 50MB 이하 가능
{/* 찾기 버튼 */} {/* 파일명 표시 */} {file && (
{file.name}
)} {/* 양식 다운로드 버튼 */}
{/* 파일변환 버튼 */}
{/* 테이블 상단 정보 */} {validationResults.length > 0 && (
{validationResults.length} {selectedRows.size > 0 && ( {selectedRows.size}건 선택 )}
)} {/* 데이터 테이블 */}
번호 이름 휴대폰 이메일 부서 직책 입사일 상태 오류 {validationResults.length === 0 ? ( 파일 선택 및 파일 변환이 필요합니다. ) : ( validationResults.map((result, index) => ( handleSelectRow(index, !!checked)} disabled={!result.isValid} /> {validationResults.length - index} {result.data.name || '-'} {result.data.phone || '-'} {result.data.email || '-'} {result.data.departmentName || '-'} {result.data.positionName || '-'} {result.data.hireDate || '-'} {result.data.status || '-'} {result.errors.length > 0 ? ( {result.errors.join(', ')} ) : ( 유효 )} )) )}
{/* 등록 버튼 */}
); }