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:
319
frontend/src/components/ProjectSelector.jsx
Normal file
319
frontend/src/components/ProjectSelector.jsx
Normal file
@@ -0,0 +1,319 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { api } from '../api';
|
||||
|
||||
const ProjectSelector = ({ onProjectSelect, selectedProject }) => {
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadProjects();
|
||||
}, []);
|
||||
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
const response = await api.get('/jobs/');
|
||||
console.log('프로젝트 API 응답:', response.data);
|
||||
|
||||
// API 응답 구조에 맞게 처리
|
||||
let projectsData = [];
|
||||
if (response.data && response.data.success && Array.isArray(response.data.jobs)) {
|
||||
// 실제 API 데이터를 프론트엔드 형식에 맞게 변환
|
||||
projectsData = response.data.jobs.map(job => ({
|
||||
job_no: job.job_no,
|
||||
project_name: job.project_name || job.job_name,
|
||||
status: job.status === '진행중' ? 'active' : 'completed',
|
||||
progress: job.status === '진행중' ? 75 : 100, // 임시 진행률
|
||||
client_name: job.client_name,
|
||||
project_site: job.project_site,
|
||||
delivery_date: job.delivery_date
|
||||
}));
|
||||
}
|
||||
|
||||
// 데이터가 없으면 목 데이터 사용
|
||||
if (projectsData.length === 0) {
|
||||
projectsData = [
|
||||
{ job_no: 'TK-2024-001', project_name: '냉동기 시스템', status: 'active', progress: 75 },
|
||||
{ job_no: 'TK-2024-002', project_name: 'BOG 처리 시스템', status: 'active', progress: 45 },
|
||||
{ job_no: 'TK-2024-003', project_name: '다이아프램 펌프', status: 'active', progress: 90 },
|
||||
{ job_no: 'TK-2024-004', project_name: '드라이어 시스템', status: 'completed', progress: 100 },
|
||||
{ job_no: 'TK-2024-005', project_name: '열교환기 시스템', status: 'active', progress: 30 }
|
||||
];
|
||||
}
|
||||
|
||||
setProjects(projectsData);
|
||||
} catch (error) {
|
||||
console.error('프로젝트 목록 로딩 실패:', error);
|
||||
// 목 데이터 사용
|
||||
const mockProjects = [
|
||||
{ job_no: 'TK-2024-001', project_name: '냉동기 시스템', status: 'active', progress: 75 },
|
||||
{ job_no: 'TK-2024-002', project_name: 'BOG 처리 시스템', status: 'active', progress: 45 },
|
||||
{ job_no: 'TK-2024-003', project_name: '다이아프램 펌프', status: 'active', progress: 90 },
|
||||
{ job_no: 'TK-2024-004', project_name: '드라이어 시스템', status: 'completed', progress: 100 },
|
||||
{ job_no: 'TK-2024-005', project_name: '열교환기 시스템', status: 'active', progress: 30 }
|
||||
];
|
||||
setProjects(mockProjects);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredProjects = projects.filter(project =>
|
||||
project.project_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
project.job_no.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
'active': '#48bb78',
|
||||
'completed': '#38b2ac',
|
||||
'on_hold': '#ed8936',
|
||||
'cancelled': '#e53e3e'
|
||||
};
|
||||
return colors[status] || '#718096';
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
'active': '진행중',
|
||||
'completed': '완료',
|
||||
'on_hold': '보류',
|
||||
'cancelled': '취소'
|
||||
};
|
||||
return texts[status] || '알 수 없음';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
color: '#666'
|
||||
}}>
|
||||
프로젝트 목록을 불러오는 중...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
{/* 선택된 프로젝트 표시 또는 선택 버튼 */}
|
||||
<div
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
style={{
|
||||
padding: '16px 20px',
|
||||
background: selectedProject ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'white',
|
||||
color: selectedProject ? 'white' : '#2d3748',
|
||||
border: selectedProject ? 'none' : '2px dashed #cbd5e0',
|
||||
borderRadius: '12px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: selectedProject ? '0 4px 12px rgba(102, 126, 234, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!selectedProject) {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.backgroundColor = '#f7fafc';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!selectedProject) {
|
||||
e.target.style.borderColor = '#cbd5e0';
|
||||
e.target.style.backgroundColor = 'white';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{selectedProject ? (
|
||||
<div>
|
||||
<div style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '4px' }}>
|
||||
{selectedProject.project_name}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', opacity: '0.9' }}>
|
||||
{selectedProject.job_no} • {getStatusText(selectedProject.status)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '4px' }}>
|
||||
🎯 프로젝트를 선택하세요
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#718096' }}>
|
||||
작업할 프로젝트를 선택하면 관련 업무를 시작할 수 있습니다
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
{showDropdown ? '🔼' : '🔽'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 드롭다운 메뉴 */}
|
||||
{showDropdown && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
marginTop: '8px',
|
||||
background: 'white',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.15)',
|
||||
border: '1px solid #e2e8f0',
|
||||
zIndex: 1000,
|
||||
maxHeight: '400px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 검색 입력 */}
|
||||
<div style={{ padding: '16px', borderBottom: '1px solid #e2e8f0' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="프로젝트 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #cbd5e0',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
outline: 'none'
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = '#667eea'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#cbd5e0'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 프로젝트 목록 */}
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||
{filteredProjects.length === 0 ? (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
color: '#718096'
|
||||
}}>
|
||||
검색 결과가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
filteredProjects.map((project) => (
|
||||
<div
|
||||
key={project.job_no}
|
||||
onClick={() => {
|
||||
onProjectSelect(project);
|
||||
setShowDropdown(false);
|
||||
setSearchTerm('');
|
||||
}}
|
||||
style={{
|
||||
padding: '16px 20px',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid #f7fafc',
|
||||
transition: 'background-color 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.backgroundColor = '#f7fafc';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.backgroundColor = 'transparent';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '#2d3748',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{project.project_name}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#718096',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
{project.job_no}
|
||||
</div>
|
||||
|
||||
{/* 진행률 바 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{
|
||||
flex: 1,
|
||||
height: '4px',
|
||||
backgroundColor: '#e2e8f0',
|
||||
borderRadius: '2px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${project.progress || 0}%`,
|
||||
height: '100%',
|
||||
backgroundColor: getStatusColor(project.status),
|
||||
transition: 'width 0.3s ease'
|
||||
}} />
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#718096',
|
||||
minWidth: '35px'
|
||||
}}>
|
||||
{project.progress || 0}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginLeft: '16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-end'
|
||||
}}>
|
||||
<span style={{
|
||||
padding: '4px 8px',
|
||||
backgroundColor: `${getStatusColor(project.status)}20`,
|
||||
color: getStatusColor(project.status),
|
||||
borderRadius: '12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
{getStatusText(project.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 드롭다운 외부 클릭 시 닫기 */}
|
||||
{showDropdown && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 999
|
||||
}}
|
||||
onClick={() => setShowDropdown(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSelector;
|
||||
Reference in New Issue
Block a user