426 lines
14 KiB
JavaScript
426 lines
14 KiB
JavaScript
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;
|