Files
TK-BOM-Project/frontend/src/components/ProjectSelector.jsx
Hyungi Ahn 83b90ef05c
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: 자재 관리 페이지 대규모 개선
- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산)
- UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인)
- 자재별 그룹핑 로직 개선:
  * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시
  * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등)
  * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시
  * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계)
  * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시
  * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑
- 전체 카테고리 버튼 제거 (표시 복잡도 감소)
- 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
2025-09-09 09:24:45 +09:00

320 lines
15 KiB
JavaScript

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;