Files
TK-BOM-Project/frontend/src/pages/BOMWorkspacePage.jsx
Hyungi Ahn 5a21ef8f6c feat: 리비전 관리 시스템 완전 개편
변동이력 관리로 전환:
- 도면번호 기준 변경 추적
- 리비전 업로드 시 전체 자재 저장 (차이분만 저장 방식 폐지)
- 구매신청 정보 수량 기반 상속

리비전 변경 감지:
- 수량/재질/크기/카테고리 변경 감지
- 변경 유형: specification_changed, quantity_changed, added, removed
- 도면별 변경사항 추적

누락 도면 처리:
- 리비전 업로드 시 누락된 도면 자동 감지
- 3가지 선택 옵션: 일부 업로드 / 도면 삭제 / 취소
- 구매신청 여부에 따라 다른 처리 (재고품 vs 숨김)

자재 상태 관리:
- revision_status 컬럼 추가 (active/inventory/deleted_not_purchased/changed)
- 재고품: 연노랑색 배경, '재고품' 배지
- 변경됨: 파란색 테두리, '변경됨' 배지
- 삭제됨: 자동 숨김

구매신청 정보 상속:
- 수량 기반 상속 (그룹별 개수만큼만)
- Rev.0에서 3개 구매 → Rev.1에서 처음 3개만 상속, 추가분은 미구매
- 도면번호 정확히 일치하는 경우에만 상속

기타 개선:
- 구매신청 관리 페이지 수량 표시 개선 (3 EA, 소수점 제거)
- 도면번호/라인번호 파싱 및 저장 (DWG_NAME, LINE_NUM 컬럼)
- SPECIAL 카테고리 도면번호 표시
- 마이그레이션 스크립트 추가 (29_add_revision_status.sql)
2025-10-14 14:30:34 +09:00

895 lines
33 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, 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 (
<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 파일 목록 ({Object.keys(groupedFiles).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>
{Object.entries(groupedFiles).map(([bomName, bomFiles]) => {
// 최신 리비전 파일을 대표로 선택
const latestFile = bomFiles[0]; // 이미 최신순으로 정렬됨
return (
<div
key={latestFile.id}
style={{
padding: '12px 16px',
borderBottom: '1px solid #f0f0f0',
cursor: 'pointer',
background: selectedFile?.id === latestFile.id ? '#f0f9ff' : 'transparent',
transition: 'background-color 0.2s ease'
}}
onClick={() => setSelectedFile(latestFile)}
onMouseEnter={(e) => {
if (selectedFile?.id !== latestFile.id) {
e.target.style.background = '#f8f9fa';
}
}}
onMouseLeave={(e) => {
if (selectedFile?.id !== latestFile.id) {
e.target.style.background = 'transparent';
}
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ flex: 1 }}>
{/* BOM 이름 (인라인 편집) */}
{editingFile === latestFile.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(latestFile, 'bom_name');
}}
>
{bomName}
</div>
)}
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
📄 {bomFiles.length > 1 ? `${bomFiles.length}개 리비전` : latestFile.revision || 'Rev.0'}
{bomFiles.reduce((sum, f) => sum + (f.parsed_count || 0), 0)} 자재 (최신: {latestFile.revision || 'Rev.0'})
</div>
</div>
<div style={{ display: 'flex', gap: '4px', marginLeft: '12px' }}>
{bomFiles.length === 1 ? (
<button
onClick={(e) => {
e.stopPropagation();
viewMaterials(latestFile);
}}
style={{
padding: '4px 8px',
background: '#48bb78',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '11px'
}}
>
📋 자재
</button>
) : (
<div style={{ position: 'relative' }}>
<select
onChange={(e) => {
const selectedFileId = e.target.value;
const selectedFile = bomFiles.find(f => f.id.toString() === selectedFileId);
if (selectedFile) {
viewMaterials(selectedFile);
}
}}
onClick={(e) => e.stopPropagation()}
style={{
padding: '4px 8px',
background: '#48bb78',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '11px'
}}
defaultValue=""
>
<option value="" disabled>📋 자재 선택</option>
{bomFiles.map(file => (
<option key={file.id} value={file.id} style={{ color: 'black' }}>
{file.revision || 'Rev.0'} ({file.parsed_count || 0})
</option>
))}
</select>
</div>
)}
<button
onClick={(e) => {
e.stopPropagation();
handleRevisionUpload(latestFile);
}}
style={{
padding: '4px 8px',
background: 'white',
color: '#4299e1',
border: '1px solid #4299e1',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '11px'
}}
>
📝 리비전
</button>
<button
onClick={(e) => {
e.stopPropagation();
if (window.confirm(`${bomName} BOM을 삭제하시겠습니까? (모든 리비전이 삭제됩니다)`)) {
// 모든 리비전 파일 삭제
bomFiles.forEach(file => 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={() => handleRevisionUpload(selectedFile)}
disabled={uploading}
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;