519 lines
16 KiB
JavaScript
519 lines
16 KiB
JavaScript
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;
|