feat: 자재 분류 시스템 개선 및 상세 테이블 추가
- 모든 자재 카테고리별 상세 테이블 생성 (fitting, valve, flange, bolt, gasket, instrument) - PIPE, FITTING, VALVE 분류 결과를 각 상세 테이블에 저장하는 로직 구현 - 프론트엔드 라우팅 정리 및 BOM 현황 페이지 기능 개선 - 자재확인 페이지 에러 처리 개선 TODO: FLANGE, BOLT, GASKET, INSTRUMENT 저장 로직 추가 필요
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import JobSelectionPage from './pages/JobSelectionPage';
|
||||
import BOMManagerPage from './pages/BOMManagerPage';
|
||||
import BOMStatusPage from './pages/BOMStatusPage';
|
||||
import MaterialsPage from './pages/MaterialsPage';
|
||||
|
||||
function App() {
|
||||
@@ -9,7 +9,7 @@ function App() {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<JobSelectionPage />} />
|
||||
<Route path="/bom-manager" element={<BOMManagerPage />} />
|
||||
<Route path="/bom-status" element={<BOMStatusPage />} />
|
||||
<Route path="/materials" element={<MaterialsPage />} />
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
|
||||
@@ -94,6 +94,18 @@ export function createJob(data) {
|
||||
return api.post('/jobs', data);
|
||||
}
|
||||
|
||||
// 리비전 비교
|
||||
export function compareRevisions(jobNo, filename, oldRevision, newRevision) {
|
||||
return api.get('/files/materials/compare-revisions', {
|
||||
params: {
|
||||
job_no: jobNo,
|
||||
filename: filename,
|
||||
old_revision: oldRevision,
|
||||
new_revision: newRevision
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 프로젝트 수정
|
||||
export function updateProject(projectId, data) {
|
||||
return api.put(`/projects/${projectId}`, data);
|
||||
|
||||
@@ -388,20 +388,20 @@ function FileUpload({ selectedProject, onUploadSuccess }) {
|
||||
📊 업로드 결과
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Description />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="파일명"
|
||||
secondary={uploadResult.original_filename}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
<ListItemText
|
||||
primary="파싱된 자재 수"
|
||||
secondary={`${uploadResult.parsed_materials_count}개`}
|
||||
/>
|
||||
@@ -413,9 +413,9 @@ function FileUpload({ selectedProject, onUploadSuccess }) {
|
||||
<ListItemText
|
||||
primary="저장된 자재 수"
|
||||
secondary={`${uploadResult.saved_materials_count}개`}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
@@ -434,11 +434,11 @@ function FileUpload({ selectedProject, onUploadSuccess }) {
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
{stat.count}개 ({stat.percentage}%)
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -470,67 +470,67 @@ function FileUpload({ selectedProject, onUploadSuccess }) {
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<Paper
|
||||
{...getRootProps()}
|
||||
) : (
|
||||
<>
|
||||
<Paper
|
||||
{...getRootProps()}
|
||||
onClick={() => console.log('드래그 앤 드롭 영역 클릭됨')}
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: 'center',
|
||||
border: 2,
|
||||
borderStyle: 'dashed',
|
||||
borderColor: isDragActive ? 'primary.main' : 'grey.300',
|
||||
bgcolor: isDragActive ? 'primary.50' : 'grey.50',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: 'primary.50'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<CloudUpload sx={{
|
||||
fontSize: 64,
|
||||
color: isDragActive ? 'primary.main' : 'grey.400',
|
||||
mb: 2
|
||||
}} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{isDragActive
|
||||
? "파일을 여기에 놓으세요!"
|
||||
: "Excel 파일을 드래그하거나 클릭하여 선택"
|
||||
}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
|
||||
지원 형식: .xlsx, .xls, .csv (최대 10MB)
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AttachFile />}
|
||||
component="span"
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: 'center',
|
||||
border: 2,
|
||||
borderStyle: 'dashed',
|
||||
borderColor: isDragActive ? 'primary.main' : 'grey.300',
|
||||
bgcolor: isDragActive ? 'primary.50' : 'grey.50',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: 'primary.50'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<CloudUpload sx={{
|
||||
fontSize: 64,
|
||||
color: isDragActive ? 'primary.main' : 'grey.400',
|
||||
mb: 2
|
||||
}} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{isDragActive
|
||||
? "파일을 여기에 놓으세요!"
|
||||
: "Excel 파일을 드래그하거나 클릭하여 선택"
|
||||
}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
|
||||
지원 형식: .xlsx, .xls, .csv (최대 10MB)
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AttachFile />}
|
||||
component="span"
|
||||
disabled={uploading}
|
||||
onClick={() => console.log('파일 선택 버튼 클릭됨')}
|
||||
>
|
||||
>
|
||||
{uploading ? '업로드 중...' : '파일 선택'}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
💡 <strong>업로드 및 분류 프로세스:</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
• BOM(Bill of Materials) 파일을 업로드하면 자동으로 자재 정보를 추출합니다
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
• BOM(Bill of Materials) 파일을 업로드하면 자동으로 자재 정보를 추출합니다
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
• 각 자재는 8가지 분류기(볼트, 플랜지, 피팅, 가스켓, 계기, 파이프, 밸브, 재질)로 자동 분류됩니다
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
• 분류 결과는 신뢰도 점수와 함께 저장되며, 필요시 수동 검증이 가능합니다
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
31
frontend/src/components/PipeDetailsCard.jsx
Normal file
31
frontend/src/components/PipeDetailsCard.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, Typography, Box } from '@mui/material';
|
||||
|
||||
const PipeDetailsCard = ({ material, fileId }) => {
|
||||
// 간단한 테스트 버전
|
||||
return (
|
||||
<Card sx={{ mt: 2, backgroundColor: '#f5f5f5' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
PIPE 상세 정보 (테스트)
|
||||
</Typography>
|
||||
<Box>
|
||||
<Typography variant="body2">
|
||||
자재명: {material.original_description}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
분류: {material.classified_category}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
사이즈: {material.size_spec || '정보 없음'}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
수량: {material.quantity} {material.unit}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PipeDetailsCard;
|
||||
@@ -17,7 +17,7 @@ const BOMStatusPage = () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
let url = '/files';
|
||||
let url = 'http://localhost:8000/files';
|
||||
if (jobNo) {
|
||||
url += `?job_no=${jobNo}`;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ const BOMStatusPage = () => {
|
||||
else setFiles([]);
|
||||
} catch (e) {
|
||||
setError('파일 목록을 불러오지 못했습니다.');
|
||||
console.error('파일 목록 로드 에러:', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -45,10 +46,10 @@ const BOMStatusPage = () => {
|
||||
setUploading(true);
|
||||
setError('');
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('project_id', 1); // 예시: 실제 프로젝트 ID로 대체 필요
|
||||
const res = await fetch('/files/upload', {
|
||||
const res = await fetch('http://localhost:8000/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
@@ -74,49 +75,62 @@ const BOMStatusPage = () => {
|
||||
accept=".csv,.xlsx,.xls"
|
||||
onChange={e => setFile(e.target.files[0])}
|
||||
disabled={uploading}
|
||||
/>
|
||||
/>
|
||||
<Button type="submit" variant="contained" disabled={!file || uploading} sx={{ ml: 2 }}>
|
||||
업로드
|
||||
</Button>
|
||||
</Button>
|
||||
</form>
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{loading && <CircularProgress sx={{ mt: 4 }} />}
|
||||
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>파일명</TableCell>
|
||||
<TableCell>리비전</TableCell>
|
||||
<TableCell>세부내역</TableCell>
|
||||
<TableCell>리비전</TableCell>
|
||||
<TableCell>삭제</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{files.map(file => (
|
||||
<TableRow key={file.id}>
|
||||
<TableCell>{file.original_filename || file.filename}</TableCell>
|
||||
<TableCell>{file.revision}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined" onClick={() => alert(`자재확인: ${file.original_filename}`)}>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined" onClick={() => navigate(`/materials?fileId=${file.id}`)}>
|
||||
자재확인
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined" color="info" onClick={() => alert(`리비전 관리: ${file.original_filename}`)}>
|
||||
리비전
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined" color="error" onClick={() => alert(`삭제: ${file.original_filename}`)}>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined" color="error" onClick={async () => {
|
||||
if (window.confirm(`정말로 ${file.original_filename}을 삭제하시겠습니까?`)) {
|
||||
try {
|
||||
const res = await fetch(`http://localhost:8000/files/${file.id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
fetchFiles();
|
||||
} else {
|
||||
alert('삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
}}>
|
||||
삭제
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ const JobSelectionPage = () => {
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selectedJobNo && selectedJobName) {
|
||||
navigate(`/bom-manager?job_no=${selectedJobNo}&job_name=${encodeURIComponent(selectedJobName)}`);
|
||||
navigate(`/bom-status?job_no=${selectedJobNo}&job_name=${encodeURIComponent(selectedJobName)}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
|
||||
// API 함수는 기존 api.js의 fetchJobs, fetchFiles, fetchMaterials를 활용한다고 가정
|
||||
import { fetchJobs, fetchMaterials } from '../api';
|
||||
|
||||
const MaterialLookupPage = () => {
|
||||
const [jobs, setJobs] = useState([]);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [revisions, setRevisions] = useState([]);
|
||||
const [selectedJobNo, setSelectedJobNo] = useState('');
|
||||
const [selectedFilename, setSelectedFilename] = useState('');
|
||||
const [selectedRevision, setSelectedRevision] = useState('');
|
||||
const [materials, setMaterials] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 1. Job 목록 불러오기 (최초 1회)
|
||||
useEffect(() => {
|
||||
async function loadJobs() {
|
||||
try {
|
||||
const res = await fetchJobs({});
|
||||
if (res.data && res.data.jobs) setJobs(res.data.jobs);
|
||||
} catch (e) {
|
||||
setError('Job 목록을 불러오지 못했습니다.');
|
||||
}
|
||||
}
|
||||
loadJobs();
|
||||
}, []);
|
||||
|
||||
// 2. Job 선택 시 해당 도면(파일) 목록 불러오기
|
||||
useEffect(() => {
|
||||
async function loadFiles() {
|
||||
if (!selectedJobNo) {
|
||||
setFiles([]);
|
||||
setRevisions([]);
|
||||
setSelectedFilename('');
|
||||
setSelectedRevision('');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/files?job_no=${selectedJobNo}`);
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) setFiles(data);
|
||||
else if (data && Array.isArray(data.files)) setFiles(data.files);
|
||||
else setFiles([]);
|
||||
setSelectedFilename('');
|
||||
setSelectedRevision('');
|
||||
setRevisions([]);
|
||||
} catch (e) {
|
||||
setFiles([]);
|
||||
setRevisions([]);
|
||||
setError('도면 목록을 불러오지 못했습니다.');
|
||||
}
|
||||
}
|
||||
loadFiles();
|
||||
}, [selectedJobNo]);
|
||||
|
||||
// 3. 도면 선택 시 해당 리비전 목록 추출
|
||||
useEffect(() => {
|
||||
if (!selectedFilename) {
|
||||
setRevisions([]);
|
||||
setSelectedRevision('');
|
||||
return;
|
||||
}
|
||||
const filtered = files.filter(f => f.original_filename === selectedFilename);
|
||||
setRevisions(filtered.map(f => f.revision));
|
||||
setSelectedRevision('');
|
||||
}, [selectedFilename, files]);
|
||||
|
||||
// 4. 조회 버튼 클릭 시 자재 목록 불러오기
|
||||
const handleLookup = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setMaterials([]);
|
||||
try {
|
||||
const params = {
|
||||
job_no: selectedJobNo,
|
||||
filename: selectedFilename,
|
||||
revision: selectedRevision
|
||||
};
|
||||
const res = await fetchMaterials(params);
|
||||
if (res.data && Array.isArray(res.data.materials)) {
|
||||
setMaterials(res.data.materials);
|
||||
} else {
|
||||
setMaterials([]);
|
||||
}
|
||||
} catch (e) {
|
||||
setError('자재 목록을 불러오지 못했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 3개 모두 선택 시 자동 조회 (원하면 주석 해제)
|
||||
// useEffect(() => {
|
||||
// if (selectedJobNo && selectedFilename && selectedRevision) {
|
||||
// handleLookup();
|
||||
// }
|
||||
// }, [selectedJobNo, selectedFilename, selectedRevision]);
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 900, mx: 'auto', mt: 4 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
자재 상세 조회 (Job No + 도면명 + 리비전)
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||
{/* Job No 드롭다운 */}
|
||||
<FormControl size="small" sx={{ minWidth: 160 }}>
|
||||
<InputLabel>Job No</InputLabel>
|
||||
<Select
|
||||
value={selectedJobNo}
|
||||
label="Job No"
|
||||
onChange={e => setSelectedJobNo(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value="">선택</MenuItem>
|
||||
{jobs.map(job => (
|
||||
<MenuItem key={job.job_number} value={job.job_number}>
|
||||
{job.job_number}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{/* 도면명 드롭다운 */}
|
||||
<FormControl size="small" sx={{ minWidth: 200 }} disabled={!selectedJobNo}>
|
||||
<InputLabel>도면명(파일명)</InputLabel>
|
||||
<Select
|
||||
value={selectedFilename}
|
||||
label="도면명(파일명)"
|
||||
onChange={e => setSelectedFilename(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value="">선택</MenuItem>
|
||||
{files.map(file => (
|
||||
<MenuItem key={file.id} value={file.original_filename}>
|
||||
{file.bom_name || file.original_filename || file.filename}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{/* 리비전 드롭다운 */}
|
||||
<FormControl size="small" sx={{ minWidth: 120 }} disabled={!selectedFilename}>
|
||||
<InputLabel>리비전</InputLabel>
|
||||
<Select
|
||||
value={selectedRevision}
|
||||
label="리비전"
|
||||
onChange={e => setSelectedRevision(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value="">선택</MenuItem>
|
||||
{revisions.map(rev => (
|
||||
<MenuItem key={rev} value={rev}>{rev}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleLookup}
|
||||
disabled={!(selectedJobNo && selectedFilename && selectedRevision) || loading}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
</Box>
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{loading && <CircularProgress sx={{ mt: 4 }} />}
|
||||
{!loading && materials.length > 0 && (
|
||||
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>품명</TableCell>
|
||||
<TableCell>수량</TableCell>
|
||||
<TableCell>단위</TableCell>
|
||||
<TableCell>사이즈</TableCell>
|
||||
<TableCell>재질</TableCell>
|
||||
<TableCell>라인번호</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{materials.map(mat => (
|
||||
<TableRow key={mat.id}>
|
||||
<TableCell>{mat.original_description}</TableCell>
|
||||
<TableCell>{mat.quantity}</TableCell>
|
||||
<TableCell>{mat.unit}</TableCell>
|
||||
<TableCell>{mat.size_spec}</TableCell>
|
||||
<TableCell>{mat.material_grade}</TableCell>
|
||||
<TableCell>{mat.line_number}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{!loading && materials.length === 0 && (selectedJobNo && selectedFilename && selectedRevision) && (
|
||||
<Alert severity="info" sx={{ mt: 4 }}>
|
||||
해당 조건에 맞는 자재가 없습니다.
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterialLookupPage;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,13 @@ const ProjectSelectionPage = () => {
|
||||
async function loadJobs() {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
try {
|
||||
const res = await fetchJobs({});
|
||||
if (res.data && Array.isArray(res.data.jobs)) {
|
||||
setJobs(res.data.jobs);
|
||||
} else {
|
||||
setJobs([]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError('프로젝트 목록을 불러오지 못했습니다.');
|
||||
} finally {
|
||||
@@ -37,8 +37,8 @@ const ProjectSelectionPage = () => {
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
<FormControl size="small" fullWidth sx={{ mt: 3 }}>
|
||||
<InputLabel>Job No</InputLabel>
|
||||
<Select
|
||||
value={selectedJob}
|
||||
<Select
|
||||
value={selectedJob}
|
||||
label="Job No"
|
||||
onChange={e => setSelectedJob(e.target.value)}
|
||||
displayEmpty
|
||||
@@ -47,23 +47,23 @@ const ProjectSelectionPage = () => {
|
||||
{jobs.map(job => (
|
||||
<MenuItem key={job.job_no} value={job.job_no}>
|
||||
{job.job_no} ({job.job_name})
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedJob && (
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedJob && (
|
||||
<Alert severity="info" sx={{ mt: 3 }}>
|
||||
선택된 Job No: <b>{selectedJob}</b>
|
||||
</Alert>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ mt: 4, minWidth: 120 }}
|
||||
disabled={!selectedJob}
|
||||
onClick={() => navigate(`/bom?job_no=${selectedJob}`)}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user