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

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;