프론트엔드 작성중
This commit is contained in:
426
frontend/src/components/SpoolManager.jsx
Normal file
426
frontend/src/components/SpoolManager.jsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Button,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add,
|
||||
Build,
|
||||
CheckCircle,
|
||||
Error,
|
||||
Visibility,
|
||||
Edit,
|
||||
Delete,
|
||||
MoreVert
|
||||
} from '@mui/icons-material';
|
||||
import { fetchProjectSpools, validateSpoolIdentifier, generateSpoolIdentifier } from '../api';
|
||||
import Toast from './Toast';
|
||||
|
||||
function SpoolManager({ selectedProject }) {
|
||||
const [spools, setSpools] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [validationDialogOpen, setValidationDialogOpen] = useState(false);
|
||||
const [toast, setToast] = useState({ open: false, message: '', type: 'info' });
|
||||
|
||||
// 스풀 생성 폼 상태
|
||||
const [newSpool, setNewSpool] = useState({
|
||||
dwg_name: '',
|
||||
area_number: '',
|
||||
spool_number: ''
|
||||
});
|
||||
|
||||
// 유효성 검증 상태
|
||||
const [validationResult, setValidationResult] = useState(null);
|
||||
const [validating, setValidating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProject) {
|
||||
fetchSpools();
|
||||
} else {
|
||||
setSpools([]);
|
||||
}
|
||||
}, [selectedProject]);
|
||||
|
||||
const fetchSpools = async () => {
|
||||
if (!selectedProject) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetchProjectSpools(selectedProject.id);
|
||||
if (response.data && response.data.spools) {
|
||||
setSpools(response.data.spools);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('스풀 조회 실패:', error);
|
||||
setToast({
|
||||
open: true,
|
||||
message: '스풀 데이터를 불러오는데 실패했습니다.',
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateSpool = async () => {
|
||||
if (!newSpool.dwg_name || !newSpool.area_number || !newSpool.spool_number) {
|
||||
setToast({
|
||||
open: true,
|
||||
message: '도면명, 에리어 번호, 스풀 번호를 모두 입력해주세요.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await generateSpoolIdentifier(
|
||||
newSpool.dwg_name,
|
||||
newSpool.area_number,
|
||||
newSpool.spool_number
|
||||
);
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
setToast({
|
||||
open: true,
|
||||
message: '스풀이 성공적으로 생성되었습니다.',
|
||||
type: 'success'
|
||||
});
|
||||
setDialogOpen(false);
|
||||
setNewSpool({ dwg_name: '', area_number: '', spool_number: '' });
|
||||
fetchSpools(); // 목록 새로고침
|
||||
} 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 handleValidateSpool = async (identifier) => {
|
||||
setValidating(true);
|
||||
try {
|
||||
const response = await validateSpoolIdentifier(identifier);
|
||||
setValidationResult(response.data);
|
||||
setValidationDialogOpen(true);
|
||||
} catch (error) {
|
||||
console.error('스풀 유효성 검증 실패:', error);
|
||||
setToast({
|
||||
open: true,
|
||||
message: '스풀 유효성 검증에 실패했습니다.',
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return 'success';
|
||||
case 'inactive': return 'warning';
|
||||
case 'completed': return 'info';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedProject) {
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
🔧 스풀 관리
|
||||
</Typography>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Build sx={{ fontSize: 64, color: 'primary.main', mb: 2 }} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
프로젝트를 선택해주세요
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
프로젝트 관리 탭에서 프로젝트를 선택하면 스풀을 관리할 수 있습니다.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<Typography variant="h6" color="primary" sx={{ mb: 2 }}>
|
||||
{selectedProject.project_name} ({selectedProject.official_project_code})
|
||||
</Typography>
|
||||
|
||||
{/* 전역 Toast */}
|
||||
<Toast
|
||||
open={toast.open}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast({ open: false, message: '', type: 'info' })}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<CircularProgress size={60} />
|
||||
<Typography variant="h6" sx={{ mt: 2 }}>
|
||||
스풀 데이터 로딩 중...
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : spools.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Build 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>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Typography variant="h6">
|
||||
총 {spools.length}개 스풀
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${spools.length}개 표시 중`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow sx={{ bgcolor: 'grey.50' }}>
|
||||
<TableCell><strong>스풀 식별자</strong></TableCell>
|
||||
<TableCell><strong>도면명</strong></TableCell>
|
||||
<TableCell><strong>에리어</strong></TableCell>
|
||||
<TableCell><strong>스풀 번호</strong></TableCell>
|
||||
<TableCell align="center"><strong>자재 수</strong></TableCell>
|
||||
<TableCell align="center"><strong>총 수량</strong></TableCell>
|
||||
<TableCell><strong>상태</strong></TableCell>
|
||||
<TableCell align="center"><strong>작업</strong></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{spools.map((spool) => (
|
||||
<TableRow
|
||||
key={spool.id}
|
||||
sx={{ '&:hover': { bgcolor: 'grey.50' } }}
|
||||
>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
|
||||
{spool.spool_identifier}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{spool.dwg_name}</TableCell>
|
||||
<TableCell>{spool.area_number}</TableCell>
|
||||
<TableCell>{spool.spool_number}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
label={spool.material_count || 0}
|
||||
size="small"
|
||||
color="primary"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
label={(spool.total_quantity || 0).toLocaleString()}
|
||||
size="small"
|
||||
color="success"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={spool.status || 'active'}
|
||||
size="small"
|
||||
color={getStatusColor(spool.status)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleValidateSpool(spool.spool_identifier)}
|
||||
disabled={validating}
|
||||
>
|
||||
<Visibility />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 새 스풀 생성 다이얼로그 */}
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>새 스풀 생성</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label="도면명"
|
||||
placeholder="예: MP7-PIPING-001"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={newSpool.dwg_name}
|
||||
onChange={(e) => setNewSpool({ ...newSpool, dwg_name: e.target.value })}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label="에리어 번호"
|
||||
placeholder="예: A1"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={newSpool.area_number}
|
||||
onChange={(e) => setNewSpool({ ...newSpool, area_number: e.target.value })}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label="스풀 번호"
|
||||
placeholder="예: 001"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={newSpool.spool_number}
|
||||
onChange={(e) => setNewSpool({ ...newSpool, spool_number: e.target.value })}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(false)} disabled={loading}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateSpool}
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
{loading ? '생성 중...' : '생성'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* 스풀 유효성 검증 결과 다이얼로그 */}
|
||||
<Dialog open={validationDialogOpen} onClose={() => setValidationDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle>스풀 유효성 검증 결과</DialogTitle>
|
||||
<DialogContent>
|
||||
{validationResult && (
|
||||
<Box>
|
||||
<Alert
|
||||
severity={validationResult.validation.is_valid ? 'success' : 'error'}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{validationResult.validation.is_valid ? '유효한 스풀 식별자입니다.' : '유효하지 않은 스풀 식별자입니다.'}
|
||||
</Alert>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="body2" color="textSecondary">스풀 식별자</Typography>
|
||||
<Typography variant="body1" sx={{ fontFamily: 'monospace', mb: 2 }}>
|
||||
{validationResult.spool_identifier}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="body2" color="textSecondary">검증 시간</Typography>
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
{new Date(validationResult.timestamp).toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body2" color="textSecondary">검증 세부사항</Typography>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
{validationResult.validation.details &&
|
||||
Object.entries(validationResult.validation.details).map(([key, value]) => (
|
||||
<Chip
|
||||
key={key}
|
||||
label={`${key}: ${value}`}
|
||||
size="small"
|
||||
color={value ? 'success' : 'error'}
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setValidationDialogOpen(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpoolManager;
|
||||
Reference in New Issue
Block a user