456 lines
20 KiB
JavaScript
456 lines
20 KiB
JavaScript
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;
|