리비전 업로드 시 정확한 수량 차이분 계산 로직 구현
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 기존 자재와 새 자재의 수량을 비교하여 증가분만 저장 - Rev.0: 엘보 10개, Rev.1: 엘보 12개 → Rev.1에는 2개만 저장 - 완전 신규 자재는 전체 수량 저장 - 수량 감소/동일한 자재는 저장하지 않음 - 리비전별 정확한 차이분 관리 구현
This commit is contained in:
@@ -9,6 +9,7 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
const [error, setError] = useState('');
|
||||
const [sidebarWidth, setSidebarWidth] = useState(300);
|
||||
const [previewWidth, setPreviewWidth] = useState(400);
|
||||
const [groupedFiles, setGroupedFiles] = useState({});
|
||||
|
||||
// 업로드 관련 상태
|
||||
const [uploading, setUploading] = useState(false);
|
||||
@@ -20,6 +21,30 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
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;
|
||||
@@ -60,6 +85,11 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
|
||||
setFiles(fileList);
|
||||
|
||||
// 파일을 그룹화
|
||||
const grouped = groupFilesByBOM(fileList);
|
||||
setGroupedFiles(grouped);
|
||||
console.log('📂 그룹화된 파일:', grouped);
|
||||
|
||||
// 기존 선택된 파일이 목록에 있는지 확인
|
||||
if (selectedFile && !fileList.find(f => f.id === selectedFile.id)) {
|
||||
setSelectedFile(null);
|
||||
@@ -239,11 +269,50 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
jobNo: file.job_no,
|
||||
bomName: file.bom_name || file.original_filename,
|
||||
revision: file.revision,
|
||||
filename: file.original_filename
|
||||
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) {
|
||||
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',
|
||||
@@ -375,9 +444,9 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<h3 style={{ margin: 0, fontSize: '16px', fontWeight: '600' }}>
|
||||
BOM 파일 목록 ({files.length})
|
||||
</h3>
|
||||
<h3 style={{ margin: 0, fontSize: '16px', fontWeight: '600' }}>
|
||||
BOM 파일 목록 ({Object.keys(groupedFiles).length})
|
||||
</h3>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={loadFiles}
|
||||
@@ -421,34 +490,38 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
<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>
|
||||
{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 === file.id && editingField === 'bom_name' ? (
|
||||
{editingFile === latestFile.id && editingField === 'bom_name' ? (
|
||||
<input
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
@@ -476,43 +549,73 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startEdit(file, 'bom_name');
|
||||
startEdit(latestFile, 'bom_name');
|
||||
}}
|
||||
>
|
||||
{file.bom_name || file.original_filename}
|
||||
{bomName}
|
||||
</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')}
|
||||
📄 {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();
|
||||
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('리비전 기능 준비 중');
|
||||
handleRevisionUpload(latestFile);
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
@@ -529,7 +632,10 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(file.id);
|
||||
if (window.confirm(`${bomName} BOM을 삭제하시겠습니까? (모든 리비전이 삭제됩니다)`)) {
|
||||
// 모든 리비전 파일 삭제
|
||||
bomFiles.forEach(file => handleDelete(file.id));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
@@ -545,8 +651,9 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -648,7 +755,8 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => alert('리비전 업로드 기능 준비 중')}
|
||||
onClick={() => handleRevisionUpload(selectedFile)}
|
||||
disabled={uploading}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
|
||||
Reference in New Issue
Block a user