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:
358
frontend/src/pages/ProjectWorkspacePage.jsx
Normal file
358
frontend/src/pages/ProjectWorkspacePage.jsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { api } from '../api';
|
||||
|
||||
const ProjectWorkspacePage = ({ project, user, onNavigate, onBackToDashboard }) => {
|
||||
const [projectStats, setProjectStats] = useState(null);
|
||||
const [recentFiles, setRecentFiles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (project) {
|
||||
loadProjectData();
|
||||
}
|
||||
}, [project]);
|
||||
|
||||
const loadProjectData = async () => {
|
||||
try {
|
||||
// 실제 파일 데이터만 로드
|
||||
const filesResponse = await api.get(`/files?job_no=${project.job_no}&limit=5`);
|
||||
|
||||
if (filesResponse.data && Array.isArray(filesResponse.data)) {
|
||||
setRecentFiles(filesResponse.data);
|
||||
|
||||
// 파일 데이터를 기반으로 통계 계산
|
||||
const stats = {
|
||||
totalFiles: filesResponse.data.length,
|
||||
totalMaterials: filesResponse.data.reduce((sum, file) => sum + (file.parsed_count || 0), 0),
|
||||
classifiedMaterials: 0, // API에서 분류 정보를 가져와야 함
|
||||
pendingVerification: 0, // API에서 검증 정보를 가져와야 함
|
||||
};
|
||||
setProjectStats(stats);
|
||||
} else {
|
||||
setRecentFiles([]);
|
||||
setProjectStats({
|
||||
totalFiles: 0,
|
||||
totalMaterials: 0,
|
||||
classifiedMaterials: 0,
|
||||
pendingVerification: 0
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('프로젝트 데이터 로딩 실패:', error);
|
||||
setRecentFiles([]);
|
||||
setProjectStats({
|
||||
totalFiles: 0,
|
||||
totalMaterials: 0,
|
||||
classifiedMaterials: 0,
|
||||
pendingVerification: 0
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getAvailableActions = () => {
|
||||
const userRole = user?.role || 'user';
|
||||
|
||||
const allActions = {
|
||||
// BOM 관리 (통합)
|
||||
'bom-management': {
|
||||
title: 'BOM 관리',
|
||||
description: 'BOM 파일 업로드, 관리 및 리비전 추적을 수행합니다',
|
||||
icon: '📋',
|
||||
color: '#667eea',
|
||||
roles: ['designer', 'manager', 'admin'],
|
||||
path: 'bom-status'
|
||||
},
|
||||
// 자재 관리
|
||||
'material-management': {
|
||||
title: '자재 관리',
|
||||
description: '자재 분류, 검증 및 구매 관리를 수행합니다',
|
||||
icon: '🔧',
|
||||
color: '#48bb78',
|
||||
roles: ['designer', 'purchaser', 'manager', 'admin'],
|
||||
path: 'materials'
|
||||
}
|
||||
};
|
||||
|
||||
// 사용자 권한에 따라 필터링
|
||||
return Object.entries(allActions).filter(([key, action]) =>
|
||||
action.roles.includes(userRole)
|
||||
);
|
||||
};
|
||||
|
||||
const handleActionClick = (actionPath) => {
|
||||
switch (actionPath) {
|
||||
case 'bom-management':
|
||||
onNavigate('bom-status', {
|
||||
job_no: project.job_no,
|
||||
job_name: project.project_name
|
||||
});
|
||||
break;
|
||||
case 'material-management':
|
||||
onNavigate('materials', {
|
||||
job_no: project.job_no,
|
||||
job_name: project.project_name
|
||||
});
|
||||
break;
|
||||
default:
|
||||
alert(`${actionPath} 기능은 곧 구현될 예정입니다.`);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '200px'
|
||||
}}>
|
||||
<div>프로젝트 데이터를 불러오는 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const availableActions = getAvailableActions();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '32px',
|
||||
background: '#f7fafc',
|
||||
minHeight: '100vh'
|
||||
}}>
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
{/* 헤더 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '32px'
|
||||
}}>
|
||||
<button
|
||||
onClick={onBackToDashboard}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '16px',
|
||||
padding: '8px'
|
||||
}}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<div>
|
||||
<h1 style={{
|
||||
margin: '0 0 8px 0',
|
||||
fontSize: '28px',
|
||||
fontWeight: 'bold',
|
||||
color: '#2d3748'
|
||||
}}>
|
||||
{project.project_name}
|
||||
</h1>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
color: '#718096'
|
||||
}}>
|
||||
{project.job_no} • 진행률: {project.progress || 0}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 프로젝트 통계 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '20px',
|
||||
marginBottom: '32px'
|
||||
}}>
|
||||
{[
|
||||
{ label: 'BOM 파일', value: projectStats.totalFiles, icon: '📄', color: '#667eea' },
|
||||
{ label: '전체 자재', value: projectStats.totalMaterials, icon: '📦', color: '#48bb78' },
|
||||
{ label: '분류 완료', value: projectStats.classifiedMaterials, icon: '✅', color: '#38b2ac' },
|
||||
{ label: '검증 대기', value: projectStats.pendingVerification, icon: '⏳', color: '#ed8936' }
|
||||
].map((stat, index) => (
|
||||
<div key={index} style={{
|
||||
background: 'white',
|
||||
padding: '20px',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#718096',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{stat.label}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: stat.color
|
||||
}}>
|
||||
{stat.value}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '24px' }}>
|
||||
{stat.icon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 업무 메뉴 */}
|
||||
<div style={{
|
||||
background: 'white',
|
||||
borderRadius: '16px',
|
||||
padding: '32px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
|
||||
marginBottom: '32px'
|
||||
}}>
|
||||
<h2 style={{
|
||||
margin: '0 0 24px 0',
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
color: '#2d3748'
|
||||
}}>
|
||||
🚀 사용 가능한 업무
|
||||
</h2>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gap: '20px'
|
||||
}}>
|
||||
{availableActions.map(([key, action]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => handleActionClick(key)}
|
||||
style={{
|
||||
padding: '20px',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '12px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
background: 'white'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.borderColor = action.color;
|
||||
e.target.style.boxShadow = `0 4px 12px ${action.color}20`;
|
||||
e.target.style.transform = 'translateY(-2px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.borderColor = '#e2e8f0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
e.target.style.transform = 'translateY(0)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '16px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '32px',
|
||||
lineHeight: 1
|
||||
}}>
|
||||
{action.icon}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{
|
||||
margin: '0 0 8px 0',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: action.color
|
||||
}}>
|
||||
{action.title}
|
||||
</h3>
|
||||
<p style={{
|
||||
margin: 0,
|
||||
fontSize: '14px',
|
||||
color: '#718096',
|
||||
lineHeight: 1.5
|
||||
}}>
|
||||
{action.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 최근 활동 (옵션) */}
|
||||
{recentFiles.length > 0 && (
|
||||
<div style={{
|
||||
background: 'white',
|
||||
borderRadius: '16px',
|
||||
padding: '32px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
||||
}}>
|
||||
<h2 style={{
|
||||
margin: '0 0 24px 0',
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
color: '#2d3748'
|
||||
}}>
|
||||
📁 최근 BOM 파일
|
||||
</h2>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{recentFiles.map((file, index) => (
|
||||
<div key={index} style={{
|
||||
padding: '16px',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
color: '#2d3748',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{file.original_filename || file.filename}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#718096'
|
||||
}}>
|
||||
{file.revision} • {file.uploaded_by || '시스템'} • {file.parsed_count || 0}개 자재
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleActionClick('materials')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#667eea',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
자재 보기
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectWorkspacePage;
|
||||
Reference in New Issue
Block a user