feat: 자재 관리 페이지 대규모 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산) - UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인) - 자재별 그룹핑 로직 개선: * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시 * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등) * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시 * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계) * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시 * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑 - 전체 카테고리 버튼 제거 (표시 복잡도 감소) - 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
This commit is contained in:
720
frontend/src/pages/BOMWorkspacePage.jsx
Normal file
720
frontend/src/pages/BOMWorkspacePage.jsx
Normal file
@@ -0,0 +1,720 @@
|
||||
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 [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('');
|
||||
|
||||
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);
|
||||
|
||||
// 기존 선택된 파일이 목록에 있는지 확인
|
||||
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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
background: '#f5f5f5',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
||||
}}>
|
||||
{/* 사이드바 - 프로젝트 정보 */}
|
||||
<div style={{
|
||||
width: `${sidebarWidth}px`,
|
||||
background: '#ffffff',
|
||||
borderRight: '1px solid #e0e0e0',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
{/* 헤더 */}
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
background: '#fafafa'
|
||||
}}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
marginBottom: '8px'
|
||||
}}
|
||||
>
|
||||
← 대시보드로
|
||||
</button>
|
||||
<h2 style={{
|
||||
margin: 0,
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
color: '#333'
|
||||
}}>
|
||||
{project?.project_name}
|
||||
</h2>
|
||||
<p style={{
|
||||
margin: '4px 0 0 0',
|
||||
fontSize: '14px',
|
||||
color: '#666'
|
||||
}}>
|
||||
{project?.official_project_code || project?.job_no}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 프로젝트 통계 */}
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
borderBottom: '1px solid #e0e0e0'
|
||||
}}>
|
||||
<div style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}>
|
||||
프로젝트 현황
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
|
||||
<div style={{ textAlign: 'center', padding: '8px', background: '#f8f9fa', borderRadius: '4px' }}>
|
||||
<div style={{ fontSize: '20px', fontWeight: '600', color: '#4299e1' }}>
|
||||
{files.length}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>BOM 파일</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', padding: '8px', background: '#f8f9fa', borderRadius: '4px' }}>
|
||||
<div style={{ fontSize: '20px', fontWeight: '600', color: '#48bb78' }}>
|
||||
{files.reduce((sum, f) => sum + (f.parsed_count || 0), 0)}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>총 자재</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 업로드 영역 */}
|
||||
<div
|
||||
style={{
|
||||
margin: '16px',
|
||||
padding: '20px',
|
||||
border: dragOver ? '2px dashed #4299e1' : '2px dashed #ddd',
|
||||
borderRadius: '8px',
|
||||
textAlign: 'center',
|
||||
background: dragOver ? '#f0f9ff' : '#fafafa',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept=".xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"
|
||||
onChange={handleFileSelect}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{uploading ? (
|
||||
<div style={{ color: '#4299e1' }}>
|
||||
📤 업로드 중...
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{ fontSize: '24px', marginBottom: '8px' }}>📁</div>
|
||||
<div style={{ fontSize: '14px', color: '#666' }}>
|
||||
Excel 파일을 드래그하거나<br />클릭하여 업로드
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메인 패널 - 파일 목록 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#ffffff'
|
||||
}}>
|
||||
{/* 툴바 */}
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
background: '#fafafa',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<h3 style={{ margin: 0, fontSize: '16px', fontWeight: '600' }}>
|
||||
BOM 파일 목록 ({files.length})
|
||||
</h3>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={loadFiles}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: loading ? '#a0aec0' : '#48bb78',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{loading ? '🔄 로딩중...' : '🔄 새로고침'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#4299e1',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
+ 파일 추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 파일 목록 */}
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
{loading ? (
|
||||
<div style={{ padding: '40px', textAlign: 'center', color: '#666' }}>
|
||||
로딩 중...
|
||||
</div>
|
||||
) : files.length === 0 ? (
|
||||
<div style={{ padding: '40px', textAlign: 'center', color: '#666' }}>
|
||||
업로드된 BOM 파일이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{files.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
cursor: 'pointer',
|
||||
background: selectedFile?.id === file.id ? '#f0f9ff' : 'transparent',
|
||||
transition: 'background-color 0.2s ease'
|
||||
}}
|
||||
onClick={() => setSelectedFile(file)}
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedFile?.id !== file.id) {
|
||||
e.target.style.background = '#f8f9fa';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedFile?.id !== file.id) {
|
||||
e.target.style.background = 'transparent';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
{/* BOM 이름 (인라인 편집) */}
|
||||
{editingFile === file.id && editingField === 'bom_name' ? (
|
||||
<input
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onBlur={saveEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveEdit();
|
||||
if (e.key === 'Escape') cancelEdit();
|
||||
}}
|
||||
style={{
|
||||
border: '1px solid #4299e1',
|
||||
borderRadius: '2px',
|
||||
padding: '2px 4px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600'
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
cursor: 'text'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startEdit(file, 'bom_name');
|
||||
}}
|
||||
>
|
||||
{file.bom_name || file.original_filename}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
|
||||
{file.original_filename} • {file.parsed_count || 0}개 자재 • {file.revision || 'Rev.0'}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#999', marginTop: '2px' }}>
|
||||
{new Date(file.created_at).toLocaleDateString('ko-KR')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '4px', marginLeft: '12px' }}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
viewMaterials(file);
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
background: '#48bb78',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px'
|
||||
}}
|
||||
>
|
||||
📋 자재
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
alert('리비전 기능 준비 중');
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
background: 'white',
|
||||
color: '#4299e1',
|
||||
border: '1px solid #4299e1',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px'
|
||||
}}
|
||||
>
|
||||
📝 리비전
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(file.id);
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
background: '#f56565',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px'
|
||||
}}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측 패널 - 상세 정보 */}
|
||||
{selectedFile && (
|
||||
<div style={{
|
||||
width: `${previewWidth}px`,
|
||||
background: '#ffffff',
|
||||
borderLeft: '1px solid #e0e0e0',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
{/* 상세 정보 헤더 */}
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
background: '#fafafa'
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: '600' }}>
|
||||
파일 상세 정보
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* 상세 정보 내용 */}
|
||||
<div style={{ padding: '16px', flex: 1, overflow: 'auto' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ fontSize: '12px', color: '#666', display: 'block', marginBottom: '4px' }}>
|
||||
BOM 이름
|
||||
</label>
|
||||
<div
|
||||
style={{
|
||||
padding: '8px',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '4px',
|
||||
cursor: 'text',
|
||||
background: '#fafafa'
|
||||
}}
|
||||
onClick={() => startEdit(selectedFile, 'bom_name')}
|
||||
>
|
||||
{selectedFile.bom_name || selectedFile.original_filename}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ fontSize: '12px', color: '#666', display: 'block', marginBottom: '4px' }}>
|
||||
파일명
|
||||
</label>
|
||||
<div style={{ fontSize: '14px', color: '#333' }}>
|
||||
{selectedFile.original_filename}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ fontSize: '12px', color: '#666', display: 'block', marginBottom: '4px' }}>
|
||||
리비전
|
||||
</label>
|
||||
<div style={{ fontSize: '14px', color: '#333' }}>
|
||||
{selectedFile.revision || 'Rev.0'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ fontSize: '12px', color: '#666', display: 'block', marginBottom: '4px' }}>
|
||||
자재 수
|
||||
</label>
|
||||
<div style={{ fontSize: '14px', color: '#333' }}>
|
||||
{selectedFile.parsed_count || 0}개
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ fontSize: '12px', color: '#666', display: 'block', marginBottom: '4px' }}>
|
||||
업로드 일시
|
||||
</label>
|
||||
<div style={{ fontSize: '14px', color: '#333' }}>
|
||||
{new Date(selectedFile.created_at).toLocaleString('ko-KR')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼들 */}
|
||||
<div style={{ marginTop: '24px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => viewMaterials(selectedFile)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
background: '#4299e1',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600'
|
||||
}}
|
||||
>
|
||||
📋 자재 목록 보기
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => alert('리비전 업로드 기능 준비 중')}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
background: 'white',
|
||||
color: '#4299e1',
|
||||
border: '1px solid #4299e1',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600'
|
||||
}}
|
||||
>
|
||||
📝 새 리비전 업로드
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(selectedFile.id)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
background: '#f56565',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600'
|
||||
}}
|
||||
>
|
||||
🗑️ 파일 삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 에러 메시지 */}
|
||||
{error && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
background: '#fed7d7',
|
||||
color: '#c53030',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #fc8181',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
{error}
|
||||
<button
|
||||
onClick={() => setError('')}
|
||||
style={{
|
||||
marginLeft: '12px',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#c53030',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BOMWorkspacePage;
|
||||
Reference in New Issue
Block a user