Files
TK-BOM-Project/frontend/src/components/ProjectManager.jsx
2025-07-16 15:44:50 +09:00

519 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import {
Typography,
Box,
Card,
CardContent,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Alert,
CircularProgress,
Snackbar,
IconButton,
Chip,
Grid,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
Divider,
Menu,
MenuItem
} from '@mui/material';
import {
Add,
Assignment,
Edit,
Delete,
MoreVert,
Visibility,
CheckCircle,
Warning
} from '@mui/icons-material';
import { createJob, updateProject, deleteProject } from '../api';
import Toast from './Toast';
function ProjectManager({ projects, selectedProject, setSelectedProject, onProjectsChange }) {
const [dialogOpen, setDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
const [editingProject, setEditingProject] = useState(null);
const [projectCode, setProjectCode] = useState('');
const [projectName, setProjectName] = useState('');
const [loading, setLoading] = useState(false);
const [toast, setToast] = useState({ open: false, message: '', type: 'info' });
const [menuAnchor, setMenuAnchor] = useState(null);
const [selectedProjectForMenu, setSelectedProjectForMenu] = useState(null);
const handleCreateProject = async () => {
if (!projectCode.trim() || !projectName.trim()) {
setToast({
open: true,
message: '프로젝트 코드와 이름을 모두 입력해주세요.',
type: 'warning'
});
return;
}
setLoading(true);
try {
const data = {
official_project_code: projectCode.trim(),
project_name: projectName.trim(),
design_project_code: projectCode.trim(),
is_code_matched: true,
status: 'active'
};
const response = await createJob(data);
const result = response.data;
if (result && result.job) {
onProjectsChange();
setSelectedProject(result.job);
setDialogOpen(false);
setProjectCode('');
setProjectName('');
setToast({
open: true,
message: '프로젝트가 성공적으로 생성되었습니다.',
type: 'success'
});
} else {
setToast({
open: true,
message: result.message || '프로젝트 생성에 실패했습니다.',
type: 'error'
});
}
} catch (error) {
console.error('프로젝트 생성 실패:', error);
setToast({
open: true,
message: '네트워크 오류가 발생했습니다. 백엔드 서버가 실행 중인지 확인해주세요.',
type: 'error'
});
} finally {
setLoading(false);
}
};
const handleEditProject = async () => {
if (!editingProject || !editingProject.project_name.trim()) {
setToast({
open: true,
message: '프로젝트명을 입력해주세요.',
type: 'warning'
});
return;
}
setLoading(true);
try {
const response = await updateProject(editingProject.id, {
project_name: editingProject.project_name.trim(),
status: editingProject.status
});
if (response.data && response.data.success) {
onProjectsChange();
setEditDialogOpen(false);
setEditingProject(null);
setToast({
open: true,
message: '프로젝트가 성공적으로 수정되었습니다.',
type: 'success'
});
} else {
setToast({
open: true,
message: response.data?.message || '프로젝트 수정에 실패했습니다.',
type: 'error'
});
}
} catch (error) {
console.error('프로젝트 수정 실패:', error);
setToast({
open: true,
message: '프로젝트 수정 중 오류가 발생했습니다.',
type: 'error'
});
} finally {
setLoading(false);
}
};
const handleDeleteProject = async (project) => {
if (!window.confirm(`정말로 프로젝트 "${project.project_name}"을 삭제하시겠습니까?`)) {
return;
}
setLoading(true);
try {
const response = await deleteProject(project.id);
if (response.data && response.data.success) {
onProjectsChange();
if (selectedProject?.id === project.id) {
setSelectedProject(null);
}
setToast({
open: true,
message: '프로젝트가 성공적으로 삭제되었습니다.',
type: 'success'
});
} else {
setToast({
open: true,
message: response.data?.message || '프로젝트 삭제에 실패했습니다.',
type: 'error'
});
}
} catch (error) {
console.error('프로젝트 삭제 실패:', error);
setToast({
open: true,
message: '프로젝트 삭제 중 오류가 발생했습니다.',
type: 'error'
});
} finally {
setLoading(false);
}
};
const handleOpenMenu = (event, project) => {
setMenuAnchor(event.currentTarget);
setSelectedProjectForMenu(project);
};
const handleCloseMenu = () => {
setMenuAnchor(null);
setSelectedProjectForMenu(null);
};
const handleEditClick = () => {
setEditingProject({ ...selectedProjectForMenu });
setEditDialogOpen(true);
handleCloseMenu();
};
const handleDetailClick = () => {
setDetailDialogOpen(true);
handleCloseMenu();
};
const handleCloseDialog = () => {
setDialogOpen(false);
setProjectCode('');
setProjectName('');
};
const handleCloseEditDialog = () => {
setEditDialogOpen(false);
setEditingProject(null);
};
const getStatusColor = (status) => {
switch (status) {
case 'active': return 'success';
case 'inactive': return 'warning';
case 'completed': return 'info';
default: return 'default';
}
};
const getStatusIcon = (status) => {
switch (status) {
case 'active': return <CheckCircle />;
case 'inactive': return <Warning />;
default: return <Assignment />;
}
};
return (
<Box>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h4">
🗂 프로젝트 관리
</Typography>
<Button
variant="contained"
startIcon={<Add />}
onClick={() => setDialogOpen(true)}
>
프로젝트
</Button>
</Box>
{/* 전역 Toast */}
<Toast
open={toast.open}
message={toast.message}
type={toast.type}
onClose={() => setToast({ open: false, message: '', type: 'info' })}
/>
{projects.length === 0 ? (
<Card>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Assignment sx={{ fontSize: 64, color: 'primary.main', mb: 2 }} />
<Typography variant="h6" gutterBottom>
프로젝트가 없습니다
</Typography>
<Typography variant="body2" color="textSecondary" sx={{ mb: 3 }}>
프로젝트를 생성하여 시작하세요!
</Typography>
<Button
variant="contained"
startIcon={<Add />}
onClick={() => setDialogOpen(true)}
>
번째 프로젝트 생성
</Button>
</CardContent>
</Card>
) : (
<Grid container spacing={2}>
{projects.map((project) => (
<Grid item xs={12} md={6} lg={4} key={project.id}>
<Card
sx={{
cursor: 'pointer',
border: selectedProject?.id === project.id ? 2 : 1,
borderColor: selectedProject?.id === project.id ? 'primary.main' : 'divider',
'&:hover': {
boxShadow: 3,
borderColor: 'primary.main'
}
}}
onClick={() => setSelectedProject(project)}
>
<CardContent>
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ mb: 1 }}>
{project.project_name || project.official_project_code}
</Typography>
<Typography variant="body2" color="primary" sx={{ mb: 1 }}>
코드: {project.official_project_code}
</Typography>
<Chip
label={project.status}
size="small"
color={getStatusColor(project.status)}
icon={getStatusIcon(project.status)}
sx={{ mb: 1 }}
/>
<Typography variant="body2" color="textSecondary">
생성일: {new Date(project.created_at).toLocaleDateString()}
</Typography>
</Box>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
handleOpenMenu(e, project);
}}
>
<MoreVert />
</IconButton>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
{/* 프로젝트 메뉴 */}
<Menu
anchorEl={menuAnchor}
open={Boolean(menuAnchor)}
onClose={handleCloseMenu}
>
<MenuItem onClick={handleDetailClick}>
<Visibility sx={{ mr: 1 }} />
상세 보기
</MenuItem>
<MenuItem onClick={handleEditClick}>
<Edit sx={{ mr: 1 }} />
수정
</MenuItem>
<Divider />
<MenuItem
onClick={() => {
handleDeleteProject(selectedProjectForMenu);
handleCloseMenu();
}}
sx={{ color: 'error.main' }}
>
<Delete sx={{ mr: 1 }} />
삭제
</MenuItem>
</Menu>
{/* 새 프로젝트 생성 다이얼로그 */}
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle> 프로젝트 생성</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="프로젝트 코드"
placeholder="예: MP7-PIPING-R3"
fullWidth
variant="outlined"
value={projectCode}
onChange={(e) => setProjectCode(e.target.value)}
sx={{ mb: 2 }}
/>
<TextField
margin="dense"
label="프로젝트명"
placeholder="예: MP7 PIPING PROJECT Rev.3"
fullWidth
variant="outlined"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog} disabled={loading}>
취소
</Button>
<Button
onClick={handleCreateProject}
variant="contained"
disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null}
>
{loading ? '생성 중...' : '생성'}
</Button>
</DialogActions>
</Dialog>
{/* 프로젝트 수정 다이얼로그 */}
<Dialog open={editDialogOpen} onClose={handleCloseEditDialog} maxWidth="sm" fullWidth>
<DialogTitle>프로젝트 수정</DialogTitle>
<DialogContent>
<TextField
margin="dense"
label="프로젝트 코드"
fullWidth
variant="outlined"
value={editingProject?.official_project_code || ''}
disabled
sx={{ mb: 2 }}
/>
<TextField
autoFocus
margin="dense"
label="프로젝트명"
fullWidth
variant="outlined"
value={editingProject?.project_name || ''}
onChange={(e) => setEditingProject({
...editingProject,
project_name: e.target.value
})}
sx={{ mb: 2 }}
/>
<TextField
select
margin="dense"
label="상태"
fullWidth
variant="outlined"
value={editingProject?.status || 'active'}
onChange={(e) => setEditingProject({
...editingProject,
status: e.target.value
})}
>
<MenuItem key="active" value="active">활성</MenuItem>
<MenuItem key="inactive" value="inactive">비활성</MenuItem>
<MenuItem key="completed" value="completed">완료</MenuItem>
</TextField>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseEditDialog} disabled={loading}>
취소
</Button>
<Button
onClick={handleEditProject}
variant="contained"
disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null}
>
{loading ? '수정 중...' : '수정'}
</Button>
</DialogActions>
</Dialog>
{/* 프로젝트 상세 보기 다이얼로그 */}
<Dialog open={detailDialogOpen} onClose={() => setDetailDialogOpen(false)} maxWidth="md" fullWidth>
<DialogTitle>프로젝트 상세 정보</DialogTitle>
<DialogContent>
{selectedProjectForMenu && (
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<Typography variant="body2" color="textSecondary">프로젝트 코드</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
{selectedProjectForMenu.official_project_code}
</Typography>
</Grid>
<Grid item xs={12} sm={6}>
<Typography variant="body2" color="textSecondary">프로젝트명</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
{selectedProjectForMenu.project_name}
</Typography>
</Grid>
<Grid item xs={12} sm={6}>
<Typography variant="body2" color="textSecondary">상태</Typography>
<Chip
label={selectedProjectForMenu.status}
color={getStatusColor(selectedProjectForMenu.status)}
icon={getStatusIcon(selectedProjectForMenu.status)}
sx={{ mb: 2 }}
/>
</Grid>
<Grid item xs={12} sm={6}>
<Typography variant="body2" color="textSecondary">생성일</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
{new Date(selectedProjectForMenu.created_at).toLocaleString()}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body2" color="textSecondary">설계 프로젝트 코드</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
{selectedProjectForMenu.design_project_code || '-'}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body2" color="textSecondary">코드 매칭</Typography>
<Chip
label={selectedProjectForMenu.is_code_matched ? '매칭됨' : '매칭 안됨'}
color={selectedProjectForMenu.is_code_matched ? 'success' : 'warning'}
sx={{ mb: 2 }}
/>
</Grid>
</Grid>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setDetailDialogOpen(false)}>
닫기
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
export default ProjectManager;