import React, { useState, useEffect, useRef } from 'react'; import { api, fetchFiles, deleteFile as deleteFileApi } from '../api'; const BOMWorkspacePage = ({ project, onNavigate, onBack }) => { // 상태 관리 const [files, setFiles] = useState([]); const [selectedFile, setSelectedFile] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [sidebarWidth, setSidebarWidth] = useState(300); const [previewWidth, setPreviewWidth] = useState(400); const [groupedFiles, setGroupedFiles] = useState({}); // 업로드 관련 상태 const [uploading, setUploading] = useState(false); const [dragOver, setDragOver] = useState(false); const fileInputRef = useRef(null); // 편집 상태 const [editingFile, setEditingFile] = useState(null); const [editingField, setEditingField] = useState(null); const [editValue, setEditValue] = useState(''); // 파일을 BOM 이름별로 그룹화 const groupFilesByBOM = (fileList) => { const groups = {}; fileList.forEach(file => { const bomName = file.bom_name || file.original_filename; if (!groups[bomName]) { groups[bomName] = []; } groups[bomName].push(file); }); // 각 그룹 내에서 리비전 번호로 정렬 Object.keys(groups).forEach(bomName => { groups[bomName].sort((a, b) => { const revA = parseInt(a.revision?.replace('Rev.', '') || '0'); const revB = parseInt(b.revision?.replace('Rev.', '') || '0'); return revB - revA; // 최신 리비전이 위로 }); }); return groups; }; useEffect(() => { console.log('🔄 프로젝트 변경됨:', project); const jobNo = project?.official_project_code || project?.job_no; if (jobNo) { console.log('✅ 프로젝트 코드 확인:', jobNo); // 프로젝트가 변경되면 기존 선택 초기화 setSelectedFile(null); setFiles([]); loadFiles(); } else { console.warn('⚠️ 프로젝트 정보가 없습니다. 받은 프로젝트:', project); setFiles([]); setSelectedFile(null); } }, [project?.official_project_code, project?.job_no]); // 두 필드 모두 감시 const loadFiles = async () => { const jobNo = project?.official_project_code || project?.job_no; if (!jobNo) { console.warn('프로젝트 정보가 없어서 파일을 로드할 수 없습니다:', project); return; } try { setLoading(true); setError(''); // 에러 초기화 console.log('📂 파일 목록 로딩 시작:', jobNo); const response = await api.get('/files/', { params: { job_no: jobNo } }); console.log('📂 API 응답:', response.data); const fileList = Array.isArray(response.data) ? response.data : response.data?.files || []; console.log('📂 파싱된 파일 목록:', fileList); setFiles(fileList); // 파일을 그룹화 const grouped = groupFilesByBOM(fileList); setGroupedFiles(grouped); console.log('📂 그룹화된 파일:', grouped); // 기존 선택된 파일이 목록에 있는지 확인 if (selectedFile && !fileList.find(f => f.id === selectedFile.id)) { setSelectedFile(null); } // 첫 번째 파일 자동 선택 (기존 선택이 없을 때만) if (fileList.length > 0 && !selectedFile) { console.log('📂 첫 번째 파일 자동 선택:', fileList[0].original_filename); setSelectedFile(fileList[0]); } console.log('📂 파일 로딩 완료:', fileList.length, '개 파일'); } catch (err) { console.error('📂 파일 로딩 실패:', err); console.error('📂 에러 상세:', err.response?.data); setError(`파일 목록을 불러오는데 실패했습니다: ${err.response?.data?.detail || err.message}`); setFiles([]); // 에러 시 빈 배열로 초기화 } finally { setLoading(false); } }; // 드래그 앤 드롭 핸들러 const handleDragOver = (e) => { e.preventDefault(); setDragOver(true); }; const handleDragLeave = (e) => { e.preventDefault(); setDragOver(false); }; const handleDrop = async (e) => { e.preventDefault(); setDragOver(false); const droppedFiles = Array.from(e.dataTransfer.files); console.log('드롭된 파일들:', droppedFiles.map(f => ({ name: f.name, type: f.type }))); const excelFiles = droppedFiles.filter(file => { const fileName = file.name.toLowerCase(); const isExcel = fileName.endsWith('.xlsx') || fileName.endsWith('.xls'); console.log(`파일 ${file.name}: Excel 여부 = ${isExcel}`); return isExcel; }); if (excelFiles.length > 0) { console.log('업로드할 Excel 파일들:', excelFiles.map(f => f.name)); await uploadFiles(excelFiles); } else { console.log('Excel 파일이 없음. 드롭된 파일들:', droppedFiles.map(f => f.name)); alert(`Excel 파일만 업로드 가능합니다.\n업로드하려는 파일: ${droppedFiles.map(f => f.name).join(', ')}`); } }; const handleFileSelect = (e) => { const selectedFiles = Array.from(e.target.files); console.log('선택된 파일들:', selectedFiles.map(f => ({ name: f.name, type: f.type }))); const excelFiles = selectedFiles.filter(file => { const fileName = file.name.toLowerCase(); const isExcel = fileName.endsWith('.xlsx') || fileName.endsWith('.xls'); console.log(`파일 ${file.name}: Excel 여부 = ${isExcel}`); return isExcel; }); if (excelFiles.length > 0) { console.log('업로드할 Excel 파일들:', excelFiles.map(f => f.name)); uploadFiles(excelFiles); } else { console.log('Excel 파일이 없음. 선택된 파일들:', selectedFiles.map(f => f.name)); alert(`Excel 파일만 업로드 가능합니다.\n선택하려는 파일: ${selectedFiles.map(f => f.name).join(', ')}`); } }; const uploadFiles = async (filesToUpload) => { console.log('업로드 시작:', filesToUpload.map(f => ({ name: f.name, size: f.size, type: f.type }))); setUploading(true); try { for (const file of filesToUpload) { console.log(`업로드 중: ${file.name} (${file.size} bytes, ${file.type})`); const jobNo = project?.official_project_code || project?.job_no; const formData = new FormData(); formData.append('file', file); formData.append('job_no', jobNo); formData.append('bom_name', file.name.replace(/\.[^/.]+$/, "")); // 확장자 제거 console.log('FormData 내용:', { fileName: file.name, jobNo: jobNo, bomName: file.name.replace(/\.[^/.]+$/, "") }); const response = await api.post('/files/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); console.log(`업로드 성공: ${file.name}`, response.data); } await loadFiles(); // 목록 새로고침 alert(`${filesToUpload.length}개 파일이 업로드되었습니다.`); } catch (err) { console.error('업로드 실패:', err); console.error('에러 상세:', err.response?.data); setError(`파일 업로드에 실패했습니다: ${err.response?.data?.detail || err.message}`); } finally { setUploading(false); } }; // 인라인 편집 핸들러 const startEdit = (file, field) => { setEditingFile(file.id); setEditingField(field); setEditValue(file[field] || ''); }; const saveEdit = async () => { try { await api.put(`/files/${editingFile}`, { [editingField]: editValue }); // 로컬 상태 업데이트 setFiles(files.map(f => f.id === editingFile ? { ...f, [editingField]: editValue } : f )); if (selectedFile?.id === editingFile) { setSelectedFile({ ...selectedFile, [editingField]: editValue }); } cancelEdit(); } catch (err) { console.error('수정 실패:', err); alert('수정에 실패했습니다.'); } }; const cancelEdit = () => { setEditingFile(null); setEditingField(null); setEditValue(''); }; // 파일 삭제 const handleDelete = async (fileId) => { if (!window.confirm('정말로 이 파일을 삭제하시겠습니까?')) { return; } try { await deleteFileApi(fileId); setFiles(files.filter(f => f.id !== fileId)); if (selectedFile?.id === fileId) { const remainingFiles = files.filter(f => f.id !== fileId); setSelectedFile(remainingFiles.length > 0 ? remainingFiles[0] : null); } } catch (err) { console.error('삭제 실패:', err); setError('파일 삭제에 실패했습니다.'); } }; // 자재 보기 const viewMaterials = (file) => { if (onNavigate) { onNavigate('materials', { file_id: file.id, jobNo: file.job_no, bomName: file.bom_name || file.original_filename, revision: file.revision, filename: file.original_filename, selectedProject: project }); } }; // 리비전 업로드 const handleRevisionUpload = async (parentFile) => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.csv,.xlsx,.xls'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; try { setUploading(true); const jobNo = project?.official_project_code || project?.job_no; const formData = new FormData(); formData.append('file', file); formData.append('job_no', jobNo); formData.append('bom_name', parentFile.bom_name || parentFile.original_filename); formData.append('parent_file_id', parentFile.id); // 부모 파일 ID 추가 const response = await api.post('/files/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); if (response.data?.success) { // 누락된 도면 확인 if (response.data.missing_drawings && response.data.missing_drawings.requires_confirmation) { const missingDrawings = response.data.missing_drawings.drawings || []; const materialCount = response.data.missing_drawings.materials?.length || 0; const hasPreviousPurchase = response.data.missing_drawings.has_previous_purchase; const fileId = response.data.file_id; // 사용자 선택을 위한 프롬프트 메시지 let alertMessage = `⚠️ 리비전 업로드 확인\n\n` + `다음 도면이 새 파일에 없습니다:\n` + `${missingDrawings.slice(0, 5).join('\n')}` + `${missingDrawings.length > 5 ? `\n...외 ${missingDrawings.length - 5}개` : ''}\n\n` + `관련 자재: ${materialCount}개\n\n`; if (hasPreviousPurchase) { // 케이스 1: 이미 구매신청된 경우 alertMessage += `✅ 이전 리비전에서 구매신청이 진행되었습니다.\n\n` + `다음 중 선택하세요:\n\n` + `1️⃣ "일부만 업로드" - 변경된 도면만 업로드, 기존 도면 유지\n` + ` → 누락된 도면의 자재는 "재고품"으로 표시 (연노랑색)\n\n` + `2️⃣ "도면 삭제됨" - 누락된 도면 완전 삭제\n` + ` → 해당 자재 제거 및 재고품 처리\n\n` + `3️⃣ "취소" - 업로드 취소\n\n` + `숫자를 입력하세요 (1, 2, 3):`; } else { // 케이스 2: 구매신청 전인 경우 alertMessage += `⚠️ 아직 구매신청이 진행되지 않았습니다.\n\n` + `다음 중 선택하세요:\n\n` + `1️⃣ "일부만 업로드" - 변경된 도면만 업로드, 기존 도면 유지\n` + ` → 누락된 도면의 자재는 그대로 유지\n\n` + `2️⃣ "도면 삭제됨" - 누락된 도면 완전 삭제\n` + ` → 해당 자재 완전 제거 (숨김 처리)\n\n` + `3️⃣ "취소" - 업로드 취소\n\n` + `숫자를 입력하세요 (1, 2, 3):`; } const userChoice = prompt(alertMessage); if (userChoice === '3' || userChoice === null) { // 취소 선택 await api.delete(`/files/${fileId}`); alert('업로드가 취소되었습니다.'); return; } else if (userChoice === '2') { // 도면 삭제됨 - 백엔드에 삭제 처리 요청 try { await api.post(`/files/${fileId}/process-missing-drawings`, { action: 'delete', drawings: missingDrawings }); alert(`✅ ${missingDrawings.length}개 도면이 삭제 처리되었습니다.`); } catch (err) { console.error('도면 삭제 처리 실패:', err); alert('도면 삭제 처리에 실패했습니다.'); } } else if (userChoice === '1') { // 일부만 업로드 - 이미 처리됨 (기본 동작) alert(`✅ 일부 업로드로 처리되었습니다.\n누락된 ${missingDrawings.length}개 도면은 기존 상태를 유지합니다.`); } else { // 잘못된 입력 await api.delete(`/files/${fileId}`); alert('잘못된 입력입니다. 업로드가 취소되었습니다.'); return; } } alert(`리비전 ${response.data.revision} 업로드 성공!\n신규 자재: ${response.data.new_materials_count || 0}개`); await loadFiles(); } } catch (err) { console.error('리비전 업로드 실패:', err); alert('리비전 업로드에 실패했습니다: ' + (err.response?.data?.detail || err.message)); } finally { setUploading(false); } }; input.click(); }; return (
{project?.official_project_code || project?.job_no}