import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { Typography, Box, Card, CardContent, Button, LinearProgress, List, ListItem, ListItemText, ListItemIcon, Chip, Paper, Divider, Stepper, Step, StepLabel, StepContent, Alert, Grid } from '@mui/material'; import { CloudUpload, AttachFile, CheckCircle, Error as ErrorIcon, Description, AutoAwesome, Category, Science, Compare } from '@mui/icons-material'; import { useDropzone } from 'react-dropzone'; import { uploadFile as uploadFileApi, fetchMaterialsSummary } from '../api'; import Toast from './Toast'; import { useNavigate } from 'react-router-dom'; function FileUpload({ selectedProject, onUploadSuccess }) { console.log('=== FileUpload 컴포넌트 렌더링 ==='); console.log('selectedProject:', selectedProject); const navigate = useNavigate(); const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadResult, setUploadResult] = useState(null); const [error, setError] = useState(''); const [showSuccess, setShowSuccess] = useState(false); const [showError, setShowError] = useState(false); const [materialsSummary, setMaterialsSummary] = useState(null); const [toast, setToast] = useState({ open: false, message: '', type: 'info' }); const [uploadSteps, setUploadSteps] = useState([ { label: '파일 업로드', completed: false, active: false }, { label: '데이터 파싱', completed: false, active: false }, { label: '자재 분류', completed: false, active: false }, { label: '분류기 실행', completed: false, active: false }, { label: '데이터베이스 저장', completed: false, active: false } ]); const onDrop = useCallback((acceptedFiles) => { console.log('=== FileUpload: onDrop 함수 호출됨 ==='); console.log('받은 파일들:', acceptedFiles); console.log('선택된 프로젝트:', selectedProject); if (!selectedProject) { console.log('프로젝트가 선택되지 않음'); setToast({ open: true, message: '프로젝트를 먼저 선택해주세요.', type: 'warning' }); return; } if (acceptedFiles.length > 0) { console.log('파일 업로드 시작'); uploadFile(acceptedFiles[0]); } else { console.log('선택된 파일이 없음'); } }, [selectedProject]); const { getRootProps, getInputProps, isDragActive, acceptedFiles } = useDropzone({ onDrop, accept: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], 'application/vnd.ms-excel': ['.xls'], 'text/csv': ['.csv'] }, multiple: false, maxSize: 10 * 1024 * 1024 // 10MB }); const updateUploadStep = (stepIndex, completed = false, active = false) => { setUploadSteps(prev => prev.map((step, index) => ({ ...step, completed: index < stepIndex ? true : (index === stepIndex ? completed : false), active: index === stepIndex ? active : false }))); }; const uploadFile = async (file) => { console.log('=== FileUpload: uploadFile 함수 시작 ==='); console.log('파일 정보:', { name: file.name, size: file.size, type: file.type }); console.log('선택된 프로젝트:', selectedProject); setUploading(true); setUploadProgress(0); setError(''); setUploadResult(null); setMaterialsSummary(null); console.log('업로드 시작:', { fileName: file.name, fileSize: file.size, jobNo: selectedProject?.job_no, projectName: selectedProject?.project_name }); // 업로드 단계 초기화 setUploadSteps([ { label: '파일 업로드', completed: false, active: true }, { label: '데이터 파싱', completed: false, active: false }, { label: '자재 분류', completed: false, active: false }, { label: '분류기 실행', completed: false, active: false }, { label: '데이터베이스 저장', completed: false, active: false } ]); const formData = new FormData(); formData.append('file', file); formData.append('job_no', selectedProject.job_no); formData.append('revision', 'Rev.0'); // 새 BOM은 항상 Rev.0 formData.append('bom_name', file.name); // BOM 이름으로 파일명 사용 formData.append('bom_type', 'excel'); // 파일 타입 formData.append('description', ''); // 설명 (빈 문자열) console.log('FormData 내용:', { fileName: file.name, jobNo: selectedProject.job_no, revision: 'Rev.0', // 새 BOM은 항상 Rev.0 bomName: file.name, bomType: 'excel' }); try { // 1단계: 파일 업로드 updateUploadStep(0, true, false); updateUploadStep(1, false, true); console.log('API 호출 시작: /upload'); const response = await uploadFileApi(formData, { onUploadProgress: (event) => { if (event.lengthComputable) { const progress = Math.round((event.loaded / event.total) * 100); setUploadProgress(progress); console.log('업로드 진행률:', progress + '%'); } } }); console.log('API 응답:', response.data); const result = response.data; console.log('응답 데이터 구조:', { success: result.success, file_id: result.file_id, message: result.message, hasFileId: 'file_id' in result }); // 2단계: 데이터 파싱 완료 updateUploadStep(1, true, false); updateUploadStep(2, false, true); if (result.success) { // 3단계: 자재 분류 완료 updateUploadStep(2, true, false); updateUploadStep(3, false, true); // 4단계: 분류기 실행 완료 updateUploadStep(3, true, false); updateUploadStep(4, false, true); // 5단계: 데이터베이스 저장 완료 updateUploadStep(4, true, false); setUploadResult(result); setToast({ open: true, message: '파일 업로드 및 분류가 성공했습니다!', type: 'success' }); console.log('업로드 성공 결과:', result); console.log('파일 ID:', result.file_id); console.log('선택된 프로젝트:', selectedProject); // 업로드 성공 후 자재 통계 미리보기 호출 try { const summaryRes = await fetchMaterialsSummary({ file_id: result.file_id }); if (summaryRes.data && summaryRes.data.success) { setMaterialsSummary(summaryRes.data.summary); } } catch (e) { // 통계 조회 실패는 무시(UX만) } if (onUploadSuccess) { console.log('onUploadSuccess 콜백 호출'); onUploadSuccess(result); } // 파일 목록 갱신을 위한 이벤트 발생 console.log('파일 업로드 이벤트 발생:', { fileId: result.file_id, jobNo: selectedProject.job_no }); try { window.dispatchEvent(new CustomEvent('fileUploaded', { detail: { fileId: result.file_id, jobNo: selectedProject.job_no } })); console.log('CustomEvent dispatch 성공'); } catch (error) { console.error('CustomEvent dispatch 실패:', error); } } else { setToast({ open: true, message: result.message || '업로드에 실패했습니다.', type: 'error' }); } } catch (error) { console.error('업로드 실패:', error); // 에러 타입별 상세 메시지 let errorMessage = '업로드에 실패했습니다.'; if (error.response) { // 서버 응답이 있는 경우 const status = error.response.status; const data = error.response.data; switch (status) { case 400: errorMessage = `잘못된 요청: ${data?.detail || '파일 형식이나 데이터를 확인해주세요.'}`; break; case 413: errorMessage = '파일 크기가 너무 큽니다. (최대 10MB)'; break; case 422: errorMessage = `데이터 검증 실패: ${data?.detail || '파일 내용을 확인해주세요.'}`; break; case 500: errorMessage = '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; break; default: errorMessage = `서버 오류 (${status}): ${data?.detail || error.message}`; } } else if (error.request) { // 네트워크 오류 errorMessage = '서버에 연결할 수 없습니다. 네트워크 연결을 확인해주세요.'; } else { // 기타 오류 errorMessage = `오류 발생: ${error.message}`; } setToast({ open: true, message: errorMessage, type: 'error' }); } finally { setUploading(false); setUploadProgress(0); } }; const handleFileSelect = (event) => { const file = event.target.files[0]; if (file) { uploadFile(file); } }; const resetUpload = () => { setUploadResult(null); setUploadProgress(0); setUploadSteps([ { label: '파일 업로드', completed: false, active: false }, { label: '데이터 파싱', completed: false, active: false }, { label: '자재 분류', completed: false, active: false }, { label: '분류기 실행', completed: false, active: false }, { label: '데이터베이스 저장', completed: false, active: false } ]); setToast({ open: false, message: '', type: 'info' }); }; const getClassificationStats = () => { if (!uploadResult?.classification_stats) return null; const stats = uploadResult.classification_stats; const total = Object.values(stats).reduce((sum, count) => sum + count, 0); return Object.entries(stats) .filter(([category, count]) => count > 0) .map(([category, count]) => ({ category, count, percentage: total > 0 ? Math.round((count / total) * 100) : 0 })) .sort((a, b) => b.count - a.count); }; return ( 📁 파일 업로드 {selectedProject.project_name} ({selectedProject.official_project_code}) {/* 전역 Toast */} setToast({ open: false, message: '', type: 'info' })} /> {uploading && ( 업로드 및 분류 진행 중... {uploadSteps.map((step, index) => ( {step.completed ? ( ) : step.active ? ( ) : ( )} {step.label} {step.active && ( )} ))} {uploadProgress > 0 && ( 파일 업로드 진행률: {uploadProgress}% )} )} {uploadResult ? ( 업로드 및 분류 성공! 📊 업로드 결과 🏷️ 분류 결과 {getClassificationStats() && ( {getClassificationStats().map((stat, index) => ( {stat.count}개 ({stat.percentage}%) ))} )} {materialsSummary && ( 💡 자재 통계 미리보기:
• 총 자재 수: {materialsSummary.total_items || 0}개
• 고유 자재: {materialsSummary.unique_descriptions || 0}종류
• 총 수량: {materialsSummary.total_quantity || 0}개
)} {uploadResult?.revision && uploadResult.revision !== 'Rev.0' && uploadResult.revision !== undefined && ( )}
) : ( <> console.log('드래그 앤 드롭 영역 클릭됨')} sx={{ p: 4, textAlign: 'center', border: 2, borderStyle: 'dashed', borderColor: isDragActive ? 'primary.main' : 'grey.300', bgcolor: isDragActive ? 'primary.50' : 'grey.50', cursor: 'pointer', transition: 'all 0.2s ease', '&:hover': { borderColor: 'primary.main', bgcolor: 'primary.50' } }} > {isDragActive ? "파일을 여기에 놓으세요!" : "Excel 파일을 드래그하거나 클릭하여 선택" } 지원 형식: .xlsx, .xls, .csv (최대 10MB) 💡 업로드 및 분류 프로세스: • BOM(Bill of Materials) 파일을 업로드하면 자동으로 자재 정보를 추출합니다 • 각 자재는 8가지 분류기(볼트, 플랜지, 피팅, 가스켓, 계기, 파이프, 밸브, 재질)로 자동 분류됩니다 • 분류 결과는 신뢰도 점수와 함께 저장되며, 필요시 수동 검증이 가능합니다 )}
); } FileUpload.propTypes = { selectedProject: PropTypes.shape({ id: PropTypes.string.isRequired, project_name: PropTypes.string.isRequired, official_project_code: PropTypes.string.isRequired, }).isRequired, onUploadSuccess: PropTypes.func, }; export default FileUpload;