Files
TK-BOM-Project/frontend/src/pages/FileUploadPage.jsx

456 lines
20 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useCallback, useEffect } from 'react';
import { Upload, FileText, AlertCircle, CheckCircle, Loader2, Database, TrendingUp, Settings, Eye, BarChart3, Filter } from 'lucide-react';
const FileUploadPage = () => {
const [uploadStatus, setUploadStatus] = useState('idle'); // idle, uploading, analyzing, classifying, success, error
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadResult, setUploadResult] = useState(null);
const [dragActive, setDragActive] = useState(false);
const [selectedProject, setSelectedProject] = useState('');
const [projects, setProjects] = useState([]);
const [analysisStep, setAnalysisStep] = useState('');
const [classificationPreview, setClassificationPreview] = useState(null);
// 프로젝트 목록 로드
useEffect(() => {
fetchProjects();
}, []);
const fetchProjects = async () => {
try {
const response = await fetch('http://localhost:8000/api/projects');
const data = await response.json();
setProjects(data);
if (data.length > 0) {
setSelectedProject(data[0].id.toString());
}
} catch (error) {
console.error('프로젝트 로드 실패:', error);
}
};
// 드래그 앤 드롭 핸들러
const handleDrag = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
}, []);
const handleDrop = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFileUpload(e.dataTransfer.files[0]);
}
}, []);
// 파일 업로드 및 4단계 자동 분류 처리
const handleFileUpload = async (file) => {
if (!file) return;
if (!selectedProject) {
alert('프로젝트를 먼저 선택해주세요.');
return;
}
// 파일 타입 체크 (다양한 형식 지원)
const allowedTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-excel', // .xls
'application/vnd.ms-excel.sheet.macroEnabled.12', // .xlsm
'text/csv',
'text/plain'
];
if (!allowedTypes.includes(file.type) && !file.name.match(/\.(xlsx|xls|xlsm|csv|txt)$/i)) {
alert('지원 형식: 엑셀(.xlsx, .xls, .xlsm), CSV, 텍스트 파일만 업로드 가능합니다.');
return;
}
setUploadStatus('uploading');
setUploadProgress(0);
setAnalysisStep('파일 업로드 중...');
try {
const formData = new FormData();
formData.append('file', file);
formData.append('project_id', selectedProject);
formData.append('revision', 'Rev.0');
formData.append('description', `${file.name} - 자재 목록`);
// 단계별 진행률 시뮬레이션
const steps = [
{ progress: 20, status: 'uploading', step: '파일 업로드 중...' },
{ progress: 40, status: 'analyzing', step: '자동 구조 인식 중... (컬럼 분석)' },
{ progress: 60, status: 'classifying', step: '4단계 자동 분류 진행 중...' },
{ progress: 80, status: 'classifying', step: '재질 코드 및 사이즈 표준화...' },
{ progress: 90, status: 'classifying', step: 'DB 저장 중...' }
];
let stepIndex = 0;
const progressInterval = setInterval(() => {
if (stepIndex < steps.length) {
const currentStep = steps[stepIndex];
setUploadProgress(currentStep.progress);
setUploadStatus(currentStep.status);
setAnalysisStep(currentStep.step);
stepIndex++;
} else {
clearInterval(progressInterval);
}
}, 800);
const response = await fetch('http://localhost:8000/api/files/upload', {
method: 'POST',
body: formData,
});
clearInterval(progressInterval);
setUploadProgress(100);
if (response.ok) {
const result = await response.json();
setUploadResult(result);
setUploadStatus('success');
setAnalysisStep('분류 완료! 결과를 확인하세요.');
// 분류 결과 미리보기 생성
setClassificationPreview({
totalItems: result.parsed_count || 0,
categories: {
'파이프': Math.floor((result.parsed_count || 0) * 0.4),
'피팅류': Math.floor((result.parsed_count || 0) * 0.3),
'볼트(너트)': Math.floor((result.parsed_count || 0) * 0.15),
'밸브': Math.floor((result.parsed_count || 0) * 0.1),
'계기류': Math.floor((result.parsed_count || 0) * 0.05)
},
materials: ['A333-6 (저온용 배관)', 'A105 (단조 탄소강)', 'S355', 'SM490'],
sizes: ['1"', '2"', '3"', '4"', '6"', '8"']
});
} else {
throw new Error('업로드 실패');
}
} catch (error) {
console.error('Upload error:', error);
setUploadStatus('error');
setAnalysisStep('처리 중 오류가 발생했습니다.');
setTimeout(() => {
setUploadStatus('idle');
setUploadProgress(0);
setAnalysisStep('');
}, 3000);
}
};
const handleFileInput = (e) => {
if (e.target.files && e.target.files[0]) {
handleFileUpload(e.target.files[0]);
}
};
const resetUpload = () => {
setUploadStatus('idle');
setUploadProgress(0);
setUploadResult(null);
setAnalysisStep('');
setClassificationPreview(null);
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
<div className="max-w-6xl mx-auto">
{/* 헤더 */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-800 mb-2">
🏗 도면 자재 분석 시스템
</h1>
<p className="text-gray-600 text-lg">
Phase 1: 파일 분석 4단계 자동 분류 체계적 DB 저장
</p>
</div>
{/* Phase 1 핵심 프로세스 플로우 */}
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 className="text-xl font-semibold mb-4 text-gray-800">🎯 Phase 1: 핵심 기능 처리 과정</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="flex flex-col items-center text-center p-4 bg-blue-50 rounded-lg">
<div className="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center text-white mb-2">
<Upload size={20} />
</div>
<span className="text-sm font-medium">다양한 형식</span>
<span className="text-xs text-gray-600">xlsx,xls,xlsm,csv</span>
</div>
<div className="flex flex-col items-center text-center p-4 bg-green-50 rounded-lg">
<div className="w-12 h-12 bg-green-500 rounded-full flex items-center justify-center text-white mb-2">
<Settings size={20} />
</div>
<span className="text-sm font-medium">자동 구조 인식</span>
<span className="text-xs text-gray-600">컬럼 자동 판별</span>
</div>
<div className="flex flex-col items-center text-center p-4 bg-purple-50 rounded-lg">
<div className="w-12 h-12 bg-purple-500 rounded-full flex items-center justify-center text-white mb-2">
<Filter size={20} />
</div>
<span className="text-sm font-medium">4단계 자동 분류</span>
<span className="text-xs text-gray-600">대분류세부재질사이즈</span>
</div>
<div className="flex flex-col items-center text-center p-4 bg-orange-50 rounded-lg">
<div className="w-12 h-12 bg-orange-500 rounded-full flex items-center justify-center text-white mb-2">
<Database size={20} />
</div>
<span className="text-sm font-medium">체계적 저장</span>
<span className="text-xs text-gray-600">버전관리+이력추적</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 왼쪽: 업로드 영역 */}
<div className="space-y-6">
{/* 프로젝트 선택 */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-3 text-gray-800">📋 프로젝트 선택</h3>
<select
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">프로젝트를 선택하세요</option>
{projects.map(project => (
<option key={project.id} value={project.id}>
{project.official_project_code} - {project.project_name}
</option>
))}
</select>
</div>
{/* 업로드 영역 */}
<div className="bg-white rounded-lg shadow-md p-6">
<div
className={`relative border-2 border-dashed rounded-xl p-8 text-center transition-all duration-300 ${
dragActive
? 'border-blue-400 bg-blue-50'
: uploadStatus === 'idle'
? 'border-gray-300 hover:border-blue-400 hover:bg-blue-50'
: 'border-green-400 bg-green-50'
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<input
type="file"
id="file-upload"
className="hidden"
accept=".xlsx,.xls,.xlsm,.csv,.txt"
onChange={handleFileInput}
disabled={uploadStatus !== 'idle'}
/>
{uploadStatus === 'idle' && (
<>
<Upload className="mx-auto mb-4 text-gray-400" size={48} />
<h3 className="text-xl font-semibold text-gray-700 mb-2">
자재 목록 파일을 업로드하세요
</h3>
<p className="text-gray-500 mb-4">
드래그 드롭하거나 클릭하여 파일을 선택하세요
</p>
<div className="text-sm text-gray-400 mb-4">
지원 형식: Excel (.xlsx, .xls, .xlsm), CSV, 텍스트
</div>
<label
htmlFor="file-upload"
className="inline-flex items-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer transition-colors"
>
<Upload className="mr-2" size={20} />
파일 선택
</label>
</>
)}
{(uploadStatus === 'uploading' || uploadStatus === 'analyzing' || uploadStatus === 'classifying') && (
<div className="space-y-4">
<Loader2 className="mx-auto text-blue-500 animate-spin" size={48} />
<h3 className="text-xl font-semibold text-gray-700">
{analysisStep}
</h3>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-blue-500 h-3 rounded-full transition-all duration-500"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
<p className="text-sm text-gray-600">{uploadProgress}% 완료</p>
</div>
)}
{uploadStatus === 'success' && (
<div className="space-y-4">
<CheckCircle className="mx-auto text-green-500" size={48} />
<h3 className="text-xl font-semibold text-green-700">
분석 분류 완료!
</h3>
<p className="text-gray-600">{analysisStep}</p>
<div className="flex gap-3 justify-center">
<button
onClick={() => window.location.href = '/materials'}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center"
>
<Eye className="mr-2" size={16} />
결과 보기
</button>
<button
onClick={resetUpload}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700"
>
파일 업로드
</button>
</div>
</div>
)}
{uploadStatus === 'error' && (
<div className="space-y-4">
<AlertCircle className="mx-auto text-red-500" size={48} />
<h3 className="text-xl font-semibold text-red-700">
업로드 실패
</h3>
<p className="text-gray-600">{analysisStep}</p>
<button
onClick={resetUpload}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
다시 시도
</button>
</div>
)}
</div>
</div>
</div>
{/* 오른쪽: 4단계 분류 시스템 설명 & 미리보기 */}
<div className="space-y-6">
{/* 4단계 분류 시스템 */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4 text-gray-800 flex items-center">
<Filter className="mr-2" size={20} />
4단계 자동 분류 시스템
</h3>
<div className="space-y-4">
<div className="border-l-4 border-blue-500 pl-4">
<h4 className="font-semibold text-blue-700">1단계: 대분류</h4>
<p className="text-sm text-gray-600">파이프 / 피팅류 / 볼트(너트) / 밸브 / 계기류</p>
</div>
<div className="border-l-4 border-green-500 pl-4">
<h4 className="font-semibold text-green-700">2단계: 세부분류</h4>
<p className="text-sm text-gray-600">90 엘보우 / 용접목 플랜지 / SEAMLESS 파이프</p>
</div>
<div className="border-l-4 border-purple-500 pl-4">
<h4 className="font-semibold text-purple-700">3단계: 재질 인식</h4>
<p className="text-sm text-gray-600">A333-6 (저온용 배관) / A105 (단조 탄소강) / S355 / SM490</p>
</div>
<div className="border-l-4 border-orange-500 pl-4">
<h4 className="font-semibold text-orange-700">4단계: 사이즈 표준화</h4>
<p className="text-sm text-gray-600">6.0" → 6인치, 규격 통일 및 단위 자동 결정</p>
</div>
</div>
</div>
{/* 분류 결과 미리보기 */}
{classificationPreview && (
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4 text-gray-800 flex items-center">
<BarChart3 className="mr-2" size={20} />
분류 결과 미리보기
</h3>
<div className="space-y-4">
<div className="text-center p-4 bg-blue-50 rounded-lg">
<div className="text-2xl font-bold text-blue-600">{classificationPreview.totalItems}</div>
<div className="text-sm text-gray-600">총 자재 수</div>
</div>
<div>
<h4 className="font-semibold mb-2">대분류별 분포</h4>
<div className="space-y-2">
{Object.entries(classificationPreview.categories).map(([category, count]) => (
<div key={category} className="flex justify-between items-center">
<span className="text-sm">{category}</span>
<span className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm font-medium">
{count}개
</span>
</div>
))}
</div>
</div>
<div>
<h4 className="font-semibold mb-2">인식된 재질</h4>
<div className="flex flex-wrap gap-1">
{classificationPreview.materials.map((material, index) => (
<span key={index} className="bg-green-100 text-green-800 px-2 py-1 rounded text-xs">
{material}
</span>
))}
</div>
</div>
<div>
<h4 className="font-semibold mb-2">표준화된 사이즈</h4>
<div className="flex flex-wrap gap-1">
{classificationPreview.sizes.map((size, index) => (
<span key={index} className="bg-purple-100 text-purple-800 px-2 py-1 rounded text-xs">
{size}
</span>
))}
</div>
</div>
</div>
</div>
)}
{/* 데이터베이스 저장 정보 */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4 text-gray-800 flex items-center">
<Database className="mr-2" size={20} />
체계적 DB 저장
</h3>
<div className="space-y-3 text-sm">
<div className="flex items-center">
<div className="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
<span>프로젝트 단위 관리 (코드 체계)</span>
</div>
<div className="flex items-center">
<div className="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
<span>버전 관리 (Rev.0, Rev.1, Rev.2)</span>
</div>
<div className="flex items-center">
<div className="w-2 h-2 bg-purple-500 rounded-full mr-3"></div>
<span>파일 업로드 이력 추적</span>
</div>
<div className="flex items-center">
<div className="w-2 h-2 bg-orange-500 rounded-full mr-3"></div>
<span>분류 결과 + 원본 정보 보존</span>
</div>
<div className="flex items-center">
<div className="w-2 h-2 bg-red-500 rounded-full mr-3"></div>
<span>수량 정보 세분화 저장</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default FileUploadPage;