feat: BOM 관리 시스템 대폭 개선 및 Docker 배포 가이드 추가
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 🎨 UI/UX 개선: 데본씽크 스타일 모던 디자인 적용 - 📁 컴포넌트 구조 개선: 폴더별 체계적 관리 (common/, bom/, materials/) - 🔧 BOM 관리 페이지 리팩토링: NewMaterialsPage → BOMManagementPage + 카테고리별 컴포넌트 분리 - 💾 구매신청 기능 개선: 선택된 자재 비활성화, 제목 편집, 엑셀 다운로드 - 📊 자재 표시 개선: 타입/서브타입 컬럼 정리, 상세 정보 복원 - 🐛 CSS 빌드 오류 수정: NewMaterialsPage.css 문법 오류 해결 - 📚 문서화: PAGES_GUIDE.md 추가, README에 Docker 캐시 문제 해결 가이드 추가 - 🔄 API 개선: 구매신청 자재 조회, 제목 수정 엔드포인트 추가
This commit is contained in:
398
frontend/src/pages/InactiveProjectsPage.jsx
Normal file
398
frontend/src/pages/InactiveProjectsPage.jsx
Normal file
@@ -0,0 +1,398 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const InactiveProjectsPage = ({
|
||||
onNavigate,
|
||||
user,
|
||||
projects,
|
||||
inactiveProjects,
|
||||
onActivateProject,
|
||||
onDeleteProject
|
||||
}) => {
|
||||
const [selectedProjects, setSelectedProjects] = useState(new Set());
|
||||
|
||||
// 비활성 프로젝트 목록 필터링
|
||||
const inactiveProjectList = projects.filter(project =>
|
||||
inactiveProjects.has(project.job_no)
|
||||
);
|
||||
|
||||
// 프로젝트 선택/해제
|
||||
const handleProjectSelect = (projectNo) => {
|
||||
setSelectedProjects(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(projectNo)) {
|
||||
newSet.delete(projectNo);
|
||||
} else {
|
||||
newSet.add(projectNo);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// 전체 선택/해제
|
||||
const handleSelectAll = () => {
|
||||
if (selectedProjects.size === inactiveProjectList.length) {
|
||||
setSelectedProjects(new Set());
|
||||
} else {
|
||||
setSelectedProjects(new Set(inactiveProjectList.map(p => p.job_no)));
|
||||
}
|
||||
};
|
||||
|
||||
// 선택된 프로젝트들 활성화
|
||||
const handleBulkActivate = () => {
|
||||
if (selectedProjects.size === 0) {
|
||||
alert('활성화할 프로젝트를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.confirm(`선택된 ${selectedProjects.size}개 프로젝트를 활성화하시겠습니까?`)) {
|
||||
selectedProjects.forEach(projectNo => {
|
||||
const project = projects.find(p => p.job_no === projectNo);
|
||||
if (project) {
|
||||
onActivateProject(project);
|
||||
}
|
||||
});
|
||||
setSelectedProjects(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
// 선택된 프로젝트들 삭제
|
||||
const handleBulkDelete = () => {
|
||||
if (selectedProjects.size === 0) {
|
||||
alert('삭제할 프로젝트를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.confirm(`선택된 ${selectedProjects.size}개 프로젝트를 완전히 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.`)) {
|
||||
selectedProjects.forEach(projectNo => {
|
||||
onDeleteProject(projectNo);
|
||||
});
|
||||
setSelectedProjects(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '40px',
|
||||
background: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)',
|
||||
minHeight: '100vh'
|
||||
}}>
|
||||
{/* 헤더 */}
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderRadius: '20px',
|
||||
padding: '32px',
|
||||
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
marginBottom: '40px'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}>
|
||||
<div>
|
||||
<h2 style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: '700',
|
||||
color: '#0f172a',
|
||||
margin: '0 0 4px 0',
|
||||
letterSpacing: '-0.025em'
|
||||
}}>
|
||||
Inactive Projects Management
|
||||
</h2>
|
||||
<p style={{
|
||||
fontSize: '14px',
|
||||
color: '#64748b',
|
||||
margin: 0,
|
||||
fontWeight: '400'
|
||||
}}>
|
||||
Manage deactivated projects - activate or permanently delete
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onNavigate('dashboard')}
|
||||
style={{
|
||||
background: 'white',
|
||||
color: '#6b7280',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '12px',
|
||||
padding: '12px 20px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
transition: 'all 0.2s ease',
|
||||
letterSpacing: '0.025em'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = '#f9fafb';
|
||||
e.target.style.borderColor = '#9ca3af';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'white';
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
}}
|
||||
>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 통계 정보 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '20px',
|
||||
marginBottom: '32px'
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
|
||||
padding: '20px',
|
||||
borderRadius: '12px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ fontSize: '28px', fontWeight: '700', color: '#92400e', marginBottom: '4px' }}>
|
||||
{inactiveProjectList.length}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#92400e', fontWeight: '500' }}>
|
||||
Inactive Projects
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)',
|
||||
padding: '20px',
|
||||
borderRadius: '12px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ fontSize: '28px', fontWeight: '700', color: '#1d4ed8', marginBottom: '4px' }}>
|
||||
{selectedProjects.size}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1d4ed8', fontWeight: '500' }}>
|
||||
Selected
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 일괄 작업 버튼들 */}
|
||||
{inactiveProjectList.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: '12px', marginBottom: '24px' }}>
|
||||
<button
|
||||
onClick={handleSelectAll}
|
||||
style={{
|
||||
background: 'white',
|
||||
color: '#6b7280',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '8px',
|
||||
padding: '10px 16px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = '#f9fafb';
|
||||
e.target.style.borderColor = '#9ca3af';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'white';
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
}}
|
||||
>
|
||||
{selectedProjects.size === inactiveProjectList.length ? 'Deselect All' : 'Select All'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleBulkActivate}
|
||||
disabled={selectedProjects.size === 0}
|
||||
style={{
|
||||
background: selectedProjects.size > 0 ? 'linear-gradient(135deg, #10b981 0%, #059669 100%)' : '#e5e7eb',
|
||||
color: selectedProjects.size > 0 ? 'white' : '#9ca3af',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '10px 16px',
|
||||
cursor: selectedProjects.size > 0 ? 'pointer' : 'not-allowed',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
>
|
||||
Activate Selected ({selectedProjects.size})
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleBulkDelete}
|
||||
disabled={selectedProjects.size === 0}
|
||||
style={{
|
||||
background: selectedProjects.size > 0 ? 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)' : '#e5e7eb',
|
||||
color: selectedProjects.size > 0 ? 'white' : '#9ca3af',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '10px 16px',
|
||||
cursor: selectedProjects.size > 0 ? 'pointer' : 'not-allowed',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
>
|
||||
Delete Selected ({selectedProjects.size})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 비활성 프로젝트 목록 */}
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderRadius: '20px',
|
||||
padding: '32px',
|
||||
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)'
|
||||
}}>
|
||||
<h3 style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: '700',
|
||||
color: '#0f172a',
|
||||
margin: '0 0 24px 0',
|
||||
letterSpacing: '-0.025em'
|
||||
}}>
|
||||
Inactive Projects List
|
||||
</h3>
|
||||
|
||||
{inactiveProjectList.length === 0 ? (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '60px 20px',
|
||||
color: '#64748b'
|
||||
}}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📂</div>
|
||||
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
|
||||
No Inactive Projects
|
||||
</div>
|
||||
<div style={{ fontSize: '14px' }}>
|
||||
All projects are currently active
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gap: '16px'
|
||||
}}>
|
||||
{inactiveProjectList.map((project) => (
|
||||
<div
|
||||
key={project.job_no}
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.05)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.borderColor = '#cbd5e1';
|
||||
e.target.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.borderColor = '#e2e8f0';
|
||||
e.target.style.boxShadow = '0 2px 8px rgba(0,0,0,0.05)';
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', flex: 1 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedProjects.has(project.job_no)}
|
||||
onChange={() => handleProjectSelect(project.job_no)}
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
color: '#1a202c',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{project.job_name || project.job_no}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#64748b'
|
||||
}}>
|
||||
Code: {project.job_no} | Client: {project.client_name || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (window.confirm(`"${project.job_name || project.job_no}" 프로젝트를 활성화하시겠습니까?`)) {
|
||||
onActivateProject(project);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 16px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.transform = 'translateY(-1px)';
|
||||
e.target.style.boxShadow = '0 4px 12px rgba(16, 185, 129, 0.4)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.transform = 'translateY(0)';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
Activate
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (window.confirm(`"${project.job_name || project.job_no}" 프로젝트를 완전히 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.`)) {
|
||||
onDeleteProject(project.job_no);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 16px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.transform = 'translateY(-1px)';
|
||||
e.target.style.boxShadow = '0 4px 12px rgba(239, 68, 68, 0.4)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.transform = 'translateY(0)';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InactiveProjectsPage;
|
||||
Reference in New Issue
Block a user