프론트엔드 작성중
This commit is contained in:
@@ -11,67 +11,228 @@ import {
|
||||
DialogActions,
|
||||
TextField,
|
||||
Alert,
|
||||
CircularProgress
|
||||
CircularProgress,
|
||||
Snackbar,
|
||||
IconButton,
|
||||
Chip,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
Divider,
|
||||
Menu,
|
||||
MenuItem
|
||||
} from '@mui/material';
|
||||
import { Add, Assignment } from '@mui/icons-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 [error, setError] = useState('');
|
||||
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()) {
|
||||
setError('프로젝트 코드와 이름을 모두 입력해주세요.');
|
||||
setToast({
|
||||
open: true,
|
||||
message: '프로젝트 코드와 이름을 모두 입력해주세요.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8000/api/projects', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
official_project_code: projectCode.trim(),
|
||||
project_name: projectName.trim(),
|
||||
design_project_code: projectCode.trim(),
|
||||
is_code_matched: true,
|
||||
status: 'active'
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const newProject = await response.json();
|
||||
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(newProject);
|
||||
setSelectedProject(result.job);
|
||||
setDialogOpen(false);
|
||||
setProjectCode('');
|
||||
setProjectName('');
|
||||
setError('');
|
||||
setToast({
|
||||
open: true,
|
||||
message: '프로젝트가 성공적으로 생성되었습니다.',
|
||||
type: 'success'
|
||||
});
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.detail || '프로젝트 생성에 실패했습니다.');
|
||||
setToast({
|
||||
open: true,
|
||||
message: result.message || '프로젝트 생성에 실패했습니다.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('프로젝트 생성 실패:', error);
|
||||
setError('네트워크 오류가 발생했습니다. 백엔드 서버가 실행 중인지 확인해주세요.');
|
||||
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('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
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 (
|
||||
@@ -89,6 +250,14 @@ function ProjectManager({ projects, selectedProject, setSelectedProject, onProje
|
||||
</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 }}>
|
||||
@@ -109,42 +278,89 @@ function ProjectManager({ projects, selectedProject, setSelectedProject, onProje
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
{projects.map((project) => (
|
||||
<Card
|
||||
key={project.id}
|
||||
sx={{
|
||||
mb: 2,
|
||||
cursor: 'pointer',
|
||||
border: selectedProject?.id === project.id ? 2 : 1,
|
||||
borderColor: selectedProject?.id === project.id ? 'primary.main' : 'divider'
|
||||
}}
|
||||
onClick={() => setSelectedProject(project)}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="h6">
|
||||
{project.project_name || project.official_project_code}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="primary" sx={{ mb: 1 }}>
|
||||
코드: {project.official_project_code}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
상태: {project.status} | 생성일: {new Date(project.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
))}
|
||||
</Box>
|
||||
</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>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
@@ -180,6 +396,121 @@ function ProjectManager({ projects, selectedProject, setSelectedProject, onProje
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user