diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..b9d355d
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000..fb8304f
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,180 @@
+import React, { useState, useEffect } from 'react';
+import {
+ AppBar,
+ Toolbar,
+ Typography,
+ Container,
+ Box,
+ Tab,
+ Tabs,
+ ThemeProvider,
+ createTheme,
+ CssBaseline
+} from '@mui/material';
+import {
+ Dashboard as DashboardIcon,
+ Upload as UploadIcon,
+ List as ListIcon,
+ Assignment as ProjectIcon
+} from '@mui/icons-material';
+
+import Dashboard from './components/Dashboard';
+import FileUpload from './components/FileUpload';
+import MaterialList from './components/MaterialList';
+import ProjectManager from './components/ProjectManager';
+
+// Material-UI 테마 설정
+const theme = createTheme({
+ palette: {
+ primary: {
+ main: '#1976d2',
+ },
+ secondary: {
+ main: '#dc004e',
+ },
+ background: {
+ default: '#f5f5f5',
+ },
+ },
+ typography: {
+ h4: {
+ fontWeight: 600,
+ },
+ h6: {
+ fontWeight: 500,
+ },
+ },
+});
+
+function TabPanel({ children, value, index, ...other }) {
+ return (
+
+ {value === index && {children}}
+
+ );
+}
+
+function App() {
+ const [tabValue, setTabValue] = useState(0);
+ const [projects, setProjects] = useState([]);
+ const [selectedProject, setSelectedProject] = useState(null);
+
+ const handleTabChange = (event, newValue) => {
+ setTabValue(newValue);
+ };
+
+ // 프로젝트 목록 로드
+ useEffect(() => {
+ fetchProjects();
+ }, []);
+
+ const fetchProjects = async () => {
+ try {
+ const response = await fetch('http://localhost:8000/api/projects');
+ if (response.ok) {
+ const data = await response.json();
+ setProjects(data);
+ if (data.length > 0 && !selectedProject) {
+ setSelectedProject(data[0]);
+ }
+ }
+ } catch (error) {
+ console.error('프로젝트 로드 실패:', error);
+ }
+ };
+
+ return (
+
+
+
+ {/* 상단 앱바 */}
+
+
+
+ TK-MP BOM 관리 시스템
+
+
+ {selectedProject ? `프로젝트: ${selectedProject.name}` : '프로젝트 없음'}
+
+
+
+
+ {/* 탭 네비게이션 */}
+
+
+
+ }
+ label="대시보드"
+ iconPosition="start"
+ />
+ }
+ label="프로젝트 관리"
+ iconPosition="start"
+ />
+ }
+ label="파일 업로드"
+ iconPosition="start"
+ />
+ }
+ label="자재 목록"
+ iconPosition="start"
+ />
+
+
+
+
+ {/* 메인 콘텐츠 */}
+
+
+
+
+
+
+
+
+
+
+ {
+ // 업로드 성공 시 대시보드로 이동
+ setTabValue(0);
+ }}
+ />
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx
new file mode 100644
index 0000000..aeff6d6
--- /dev/null
+++ b/frontend/src/components/Dashboard.jsx
@@ -0,0 +1,128 @@
+import React, { useState, useEffect } from 'react';
+import { Typography, Box, Card, CardContent, Grid, CircularProgress } from '@mui/material';
+
+function Dashboard({ selectedProject, projects }) {
+ const [stats, setStats] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ if (selectedProject) {
+ fetchMaterialStats();
+ }
+ }, [selectedProject]);
+
+ const fetchMaterialStats = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch(`http://localhost:8000/api/files/materials/summary?project_id=${selectedProject.id}`);
+ if (response.ok) {
+ const data = await response.json();
+ setStats(data.summary);
+ }
+ } catch (error) {
+ console.error('통계 로드 실패:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ 📊 대시보드
+
+
+
+
+
+
+
+ 프로젝트 현황
+
+
+ {projects.length}
+
+
+ 총 프로젝트 수
+
+
+ 선택된 프로젝트: {selectedProject ? selectedProject.project_name : '없음'}
+
+
+
+
+
+
+
+
+
+ 자재 현황
+
+ {loading ? (
+
+
+
+ ) : stats ? (
+
+
+ {stats.total_items.toLocaleString()}
+
+
+ 총 자재 수
+
+
+ 고유 품목: {stats.unique_descriptions}개
+
+
+ 고유 사이즈: {stats.unique_sizes}개
+
+
+ 총 수량: {stats.total_quantity.toLocaleString()}
+
+
+ ) : (
+
+ 프로젝트를 선택하면 자재 현황을 확인할 수 있습니다.
+
+ )}
+
+
+
+
+ {selectedProject && (
+
+
+
+
+ 📋 프로젝트 상세 정보
+
+
+
+ 프로젝트 코드
+ {selectedProject.official_project_code}
+
+
+ 프로젝트명
+ {selectedProject.project_name}
+
+
+ 상태
+ {selectedProject.status}
+
+
+ 생성일
+
+ {new Date(selectedProject.created_at).toLocaleDateString()}
+
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default Dashboard;
diff --git a/frontend/src/components/FileUpload.jsx b/frontend/src/components/FileUpload.jsx
new file mode 100644
index 0000000..dcb69d8
--- /dev/null
+++ b/frontend/src/components/FileUpload.jsx
@@ -0,0 +1,313 @@
+import React, { useState, useCallback } from 'react';
+import {
+ Typography,
+ Box,
+ Card,
+ CardContent,
+ Button,
+ LinearProgress,
+ Alert,
+ List,
+ ListItem,
+ ListItemText,
+ ListItemIcon,
+ Chip,
+ Paper,
+ Divider
+} from '@mui/material';
+import {
+ CloudUpload,
+ AttachFile,
+ CheckCircle,
+ Error as ErrorIcon,
+ Description
+} from '@mui/icons-material';
+import { useDropzone } from 'react-dropzone';
+
+function FileUpload({ selectedProject, onUploadSuccess }) {
+ const [uploading, setUploading] = useState(false);
+ const [uploadProgress, setUploadProgress] = useState(0);
+ const [uploadResult, setUploadResult] = useState(null);
+ const [error, setError] = useState('');
+
+ const onDrop = useCallback((acceptedFiles) => {
+ if (!selectedProject) {
+ setError('프로젝트를 먼저 선택해주세요.');
+ return;
+ }
+
+ if (acceptedFiles.length > 0) {
+ uploadFile(acceptedFiles[0]);
+ }
+ }, [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 uploadFile = async (file) => {
+ setUploading(true);
+ setUploadProgress(0);
+ setError('');
+ setUploadResult(null);
+
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('project_id', selectedProject.id);
+ formData.append('revision', 'Rev.0');
+
+ try {
+ const xhr = new XMLHttpRequest();
+
+ // 업로드 진행률 추적
+ xhr.upload.addEventListener('progress', (event) => {
+ if (event.lengthComputable) {
+ const progress = Math.round((event.loaded / event.total) * 100);
+ setUploadProgress(progress);
+ }
+ });
+
+ // Promise로 XMLHttpRequest 래핑
+ const uploadPromise = new Promise((resolve, reject) => {
+ xhr.onload = () => {
+ if (xhr.status === 200) {
+ resolve(JSON.parse(xhr.responseText));
+ } else {
+ reject(new Error(`HTTP ${xhr.status}: ${xhr.responseText}`));
+ }
+ };
+ xhr.onerror = () => reject(new Error('Network error'));
+ });
+
+ xhr.open('POST', 'http://localhost:8000/api/files/upload');
+ xhr.send(formData);
+
+ const result = await uploadPromise;
+
+ if (result.success) {
+ setUploadResult(result);
+ if (onUploadSuccess) {
+ onUploadSuccess(result);
+ }
+ } else {
+ setError(result.message || '업로드에 실패했습니다.');
+ }
+
+ } catch (error) {
+ console.error('업로드 실패:', error);
+ setError(`업로드 실패: ${error.message}`);
+ } finally {
+ setUploading(false);
+ setUploadProgress(0);
+ }
+ };
+
+ const handleFileSelect = (event) => {
+ const file = event.target.files[0];
+ if (file) {
+ uploadFile(file);
+ }
+ };
+
+ const resetUpload = () => {
+ setUploadResult(null);
+ setError('');
+ setUploadProgress(0);
+ };
+
+ if (!selectedProject) {
+ return (
+
+
+ 📁 파일 업로드
+
+
+
+
+
+ 프로젝트를 선택해주세요
+
+
+ 프로젝트 관리 탭에서 프로젝트를 선택한 후 파일을 업로드할 수 있습니다.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ 📁 파일 업로드
+
+
+
+ {selectedProject.project_name} ({selectedProject.official_project_code})
+
+
+ {error && (
+ setError('')}>
+ {error}
+
+ )}
+
+ {uploadResult ? (
+
+
+
+
+
+ 업로드 성공!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+
+ {uploadResult.sample_materials && uploadResult.sample_materials.length > 0 && (
+
+
+ 샘플 자재 (처음 3개):
+
+ {uploadResult.sample_materials.map((material, index) => (
+
+ {index + 1}. {material.original_description} - {material.quantity} {material.unit}
+ {material.size_spec && ` (${material.size_spec})`}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ ) : (
+
+
+ {uploading ? (
+
+
+
+ 파일 업로드 중...
+
+
+
+
+ {uploadProgress}% 완료
+
+
+
+ ) : (
+ <>
+
+
+
+
+ {isDragActive
+ ? "파일을 여기에 놓으세요!"
+ : "Excel 파일을 드래그하거나 클릭하여 선택"
+ }
+
+
+ 지원 형식: .xlsx, .xls, .csv (최대 10MB)
+
+ }
+ component="span"
+ >
+ 파일 선택
+
+
+
+
+
+ 💡 업로드 팁:
+
+
+ • BOM(Bill of Materials) 파일을 업로드하면 자동으로 자재 정보를 추출합니다
+
+
+ • 자재명, 수량, 사이즈, 재질 등이 자동으로 분류됩니다
+
+
+ >
+ )}
+
+
+ )}
+
+ );
+}
+
+export default FileUpload;
diff --git a/frontend/src/components/MaterialList.jsx b/frontend/src/components/MaterialList.jsx
new file mode 100644
index 0000000..5b2aef4
--- /dev/null
+++ b/frontend/src/components/MaterialList.jsx
@@ -0,0 +1,245 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Typography,
+ Box,
+ Card,
+ CardContent,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ TablePagination,
+ CircularProgress,
+ Alert,
+ Chip
+} from '@mui/material';
+import { Inventory } from '@mui/icons-material';
+
+function MaterialList({ selectedProject }) {
+ const [materials, setMaterials] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(25);
+ const [totalCount, setTotalCount] = useState(0);
+
+ useEffect(() => {
+ if (selectedProject) {
+ fetchMaterials();
+ } else {
+ setMaterials([]);
+ setTotalCount(0);
+ }
+ }, [selectedProject, page, rowsPerPage]);
+
+ const fetchMaterials = async () => {
+ setLoading(true);
+ setError('');
+
+ try {
+ const skip = page * rowsPerPage;
+ const response = await fetch(
+ `http://localhost:8000/api/files/materials?project_id=${selectedProject.id}&skip=${skip}&limit=${rowsPerPage}`
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ setMaterials(data.materials || []);
+ setTotalCount(data.total_count || 0);
+ } else {
+ setError('자재 데이터를 불러오는데 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('자재 조회 실패:', error);
+ setError('네트워크 오류가 발생했습니다.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleChangePage = (event, newPage) => {
+ setPage(newPage);
+ };
+
+ const handleChangeRowsPerPage = (event) => {
+ setRowsPerPage(parseInt(event.target.value, 10));
+ setPage(0);
+ };
+
+ const getItemTypeColor = (itemType) => {
+ const colors = {
+ 'PIPE': 'primary',
+ 'FITTING': 'secondary',
+ 'VALVE': 'success',
+ 'FLANGE': 'warning',
+ 'BOLT': 'info',
+ 'OTHER': 'default'
+ };
+ return colors[itemType] || 'default';
+ };
+
+ if (!selectedProject) {
+ return (
+
+
+ 📋 자재 목록
+
+
+
+
+
+ 프로젝트를 선택해주세요
+
+
+ 프로젝트 관리 탭에서 프로젝트를 선택하면 자재 목록을 확인할 수 있습니다.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ 📋 자재 목록 (그룹핑)
+
+
+
+ {selectedProject.project_name} ({selectedProject.official_project_code})
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {loading ? (
+
+
+
+
+ 자재 데이터 로딩 중...
+
+
+
+ ) : materials.length === 0 ? (
+
+
+
+
+ 자재 데이터가 없습니다
+
+
+ 파일 업로드 탭에서 BOM 파일을 업로드해주세요.
+
+
+
+ ) : (
+
+
+
+
+ 총 {totalCount.toLocaleString()}개 자재 그룹
+
+
+
+
+
+
+
+
+ 번호
+ 유형
+ 자재명
+ 총 수량
+ 단위
+ 사이즈
+ 재질
+ 라인 수
+
+
+
+ {materials.map((material, index) => (
+
+
+ {page * rowsPerPage + index + 1}
+
+
+
+
+
+
+ {material.original_description}
+
+
+
+
+ {material.quantity.toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+ {material.material_grade || '-'}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ `${from}-${to} / 총 ${count !== -1 ? count : to} 개`
+ }
+ />
+
+
+ )}
+
+ );
+}
+
+export default MaterialList;
diff --git a/frontend/src/components/ProjectManager.jsx b/frontend/src/components/ProjectManager.jsx
new file mode 100644
index 0000000..68fa608
--- /dev/null
+++ b/frontend/src/components/ProjectManager.jsx
@@ -0,0 +1,187 @@
+import React, { useState } from 'react';
+import {
+ Typography,
+ Box,
+ Card,
+ CardContent,
+ Button,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ Alert,
+ CircularProgress
+} from '@mui/material';
+import { Add, Assignment } from '@mui/icons-material';
+
+function ProjectManager({ projects, selectedProject, setSelectedProject, onProjectsChange }) {
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [projectCode, setProjectCode] = useState('');
+ const [projectName, setProjectName] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const handleCreateProject = async () => {
+ if (!projectCode.trim() || !projectName.trim()) {
+ setError('프로젝트 코드와 이름을 모두 입력해주세요.');
+ return;
+ }
+
+ setLoading(true);
+ setError('');
+
+ try {
+ const response = await fetch('http://localhost:8000/api/projects', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ },
+ body: JSON.stringify({
+ official_project_code: projectCode.trim(),
+ project_name: projectName.trim(),
+ design_project_code: projectCode.trim(),
+ is_code_matched: true,
+ status: 'active'
+ })
+ });
+
+ if (response.ok) {
+ const newProject = await response.json();
+ onProjectsChange();
+ setSelectedProject(newProject);
+ setDialogOpen(false);
+ setProjectCode('');
+ setProjectName('');
+ setError('');
+ } else {
+ const errorData = await response.json();
+ setError(errorData.detail || '프로젝트 생성에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('프로젝트 생성 실패:', error);
+ setError('네트워크 오류가 발생했습니다. 백엔드 서버가 실행 중인지 확인해주세요.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCloseDialog = () => {
+ setDialogOpen(false);
+ setProjectCode('');
+ setProjectName('');
+ setError('');
+ };
+
+ return (
+
+
+
+ 🗂️ 프로젝트 관리
+
+ }
+ onClick={() => setDialogOpen(true)}
+ >
+ 새 프로젝트
+
+
+
+ {projects.length === 0 ? (
+
+
+
+
+ 프로젝트가 없습니다
+
+
+ 새 프로젝트를 생성하여 시작하세요!
+
+ }
+ onClick={() => setDialogOpen(true)}
+ >
+ 첫 번째 프로젝트 생성
+
+
+
+ ) : (
+
+ {projects.map((project) => (
+ setSelectedProject(project)}
+ >
+
+
+ {project.project_name || project.official_project_code}
+
+
+ 코드: {project.official_project_code}
+
+
+ 상태: {project.status} | 생성일: {new Date(project.created_at).toLocaleDateString()}
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
+
+export default ProjectManager;
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..1e8cba0
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,34 @@
+body {
+ margin: 0;
+ padding: 0;
+ font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #f5f5f5;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+#root {
+ min-height: 100vh;
+}
+
+/* 스크롤바 스타일링 */
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #a8a8a8;
+}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000..54b39dd
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/frontend/src/pages/FileUploadPage.jsx b/frontend/src/pages/FileUploadPage.jsx
new file mode 100644
index 0000000..d195533
--- /dev/null
+++ b/frontend/src/pages/FileUploadPage.jsx
@@ -0,0 +1,455 @@
+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 (
+
+
+ {/* 헤더 */}
+
+
+ 🏗️ 도면 자재 분석 시스템
+
+
+ Phase 1: 파일 분석 → 4단계 자동 분류 → 체계적 DB 저장
+
+
+
+ {/* Phase 1 핵심 프로세스 플로우 */}
+
+
🎯 Phase 1: 핵심 기능 처리 과정
+
+
+
+
+
+
다양한 형식
+
xlsx,xls,xlsm,csv
+
+
+
+
+
+
자동 구조 인식
+
컬럼 자동 판별
+
+
+
+
+
+
4단계 자동 분류
+
대분류→세부→재질→사이즈
+
+
+
+
+
+
체계적 저장
+
버전관리+이력추적
+
+
+
+
+
+ {/* 왼쪽: 업로드 영역 */}
+
+ {/* 프로젝트 선택 */}
+
+
📋 프로젝트 선택
+
+
+
+ {/* 업로드 영역 */}
+
+
+
+
+ {uploadStatus === 'idle' && (
+ <>
+
+
+ 자재 목록 파일을 업로드하세요
+
+
+ 드래그 앤 드롭하거나 클릭하여 파일을 선택하세요
+
+
+ 지원 형식: Excel (.xlsx, .xls, .xlsm), CSV, 텍스트
+
+
+ >
+ )}
+
+ {(uploadStatus === 'uploading' || uploadStatus === 'analyzing' || uploadStatus === 'classifying') && (
+
+
+
+ {analysisStep}
+
+
+
{uploadProgress}% 완료
+
+ )}
+
+ {uploadStatus === 'success' && (
+
+
+
+ 분석 및 분류 완료!
+
+
{analysisStep}
+
+
+
+
+
+ )}
+
+ {uploadStatus === 'error' && (
+
+
+
+ 업로드 실패
+
+
{analysisStep}
+
+
+ )}
+
+
+
+
+ {/* 오른쪽: 4단계 분류 시스템 설명 & 미리보기 */}
+
+ {/* 4단계 분류 시스템 */}
+
+
+
+ 4단계 자동 분류 시스템
+
+
+
+
1단계: 대분류
+
파이프 / 피팅류 / 볼트(너트) / 밸브 / 계기류
+
+
+
2단계: 세부분류
+
90도 엘보우 / 용접목 플랜지 / SEAMLESS 파이프
+
+
+
3단계: 재질 인식
+
A333-6 (저온용 배관) / A105 (단조 탄소강) / S355 / SM490
+
+
+
4단계: 사이즈 표준화
+
6.0" → 6인치, 규격 통일 및 단위 자동 결정
+
+
+
+
+ {/* 분류 결과 미리보기 */}
+ {classificationPreview && (
+
+
+
+ 분류 결과 미리보기
+
+
+
+
{classificationPreview.totalItems}
+
총 자재 수
+
+
+
+
대분류별 분포
+
+ {Object.entries(classificationPreview.categories).map(([category, count]) => (
+
+ {category}
+
+ {count}개
+
+
+ ))}
+
+
+
+
+
인식된 재질
+
+ {classificationPreview.materials.map((material, index) => (
+
+ {material}
+
+ ))}
+
+
+
+
+
표준화된 사이즈
+
+ {classificationPreview.sizes.map((size, index) => (
+
+ {size}
+
+ ))}
+
+
+
+
+ )}
+
+ {/* 데이터베이스 저장 정보 */}
+
+
+
+ 체계적 DB 저장
+
+
+
+
+
+
버전 관리 (Rev.0, Rev.1, Rev.2)
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default FileUploadPage;