import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Card, CardContent, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button, Alert, CircularProgress, Chip, Divider, FormControl, InputLabel, Select, MenuItem, Grid } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ShoppingCart from '@mui/icons-material/ShoppingCart'; import { Compare as CompareIcon, Download } from '@mui/icons-material'; import { api, fetchFiles } from '../api'; import { exportMaterialsToExcel } from '../utils/excelExport'; const MaterialsPage = () => { const [materials, setMaterials] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [fileId, setFileId] = useState(null); const [fileName, setFileName] = useState(''); const [jobNo, setJobNo] = useState(''); const [bomName, setBomName] = useState(''); const [currentRevision, setCurrentRevision] = useState(''); const [availableRevisions, setAvailableRevisions] = useState([]); const navigate = useNavigate(); useEffect(() => { const urlParams = new URLSearchParams(window.location.search); const id = urlParams.get('file_id'); const name = urlParams.get('filename') || ''; const job_no = urlParams.get('job_no') || ''; if (id && job_no) { setFileId(id); setFileName(decodeURIComponent(name)); setJobNo(job_no); loadMaterials(id); loadAvailableRevisions(job_no, name); } else { setLoading(false); setError('파일 ID 또는 Job No가 지정되지 않았습니다.'); } }, []); // 같은 BOM의 다른 리비전들 로드 const loadAvailableRevisions = async (job_no, filename) => { try { const response = await fetchFiles({ job_no }); if (Array.isArray(response.data)) { // 같은 BOM 이름의 파일들만 필터링 const sameNameFiles = response.data.filter(file => file.original_filename === filename || file.bom_name === filename || file.filename === filename ); // 리비전 순으로 정렬 (최신부터) const sortedFiles = sameNameFiles.sort((a, b) => { const revA = parseInt(a.revision?.replace('Rev.', '') || '0'); const revB = parseInt(b.revision?.replace('Rev.', '') || '0'); return revB - revA; }); setAvailableRevisions(sortedFiles); // 현재 파일 정보 설정 const currentFile = sortedFiles.find(file => file.id === parseInt(fileId)); if (currentFile) { setCurrentRevision(currentFile.revision || 'Rev.0'); setBomName(currentFile.bom_name || currentFile.original_filename); } } } catch (err) { console.error('리비전 목록 로드 실패:', err); } }; const loadMaterials = async (id) => { try { setLoading(true); const response = await api.get('/files/materials', { params: { file_id: parseInt(id), limit: 10000 } }); if (response.data && response.data.materials) { setMaterials(response.data.materials); } else { setMaterials([]); } setError(null); } catch (err) { setError('자재 정보를 불러오는데 실패했습니다.'); console.error('자재 로딩 에러:', err); } finally { setLoading(false); } }; // 자재 사양서 생성 로직 const generateMaterialSpecs = (materials) => { const specs = {}; materials.forEach(material => { const category = material.classified_category || 'UNKNOWN'; let specKey = ''; let specData = {}; if (category === 'PIPE') { // PIPE: 재질 + 외경 + 스케줄 + 제작방식 // 재질 정보 - pipe_details에서 이미 정제된 것만 사용 const material_spec = material.pipe_details?.material_spec || material.material_grade || ''; const outer_diameter = material.main_nom || material.pipe_details?.outer_diameter || ''; const schedule = material.pipe_details?.schedule || ''; const manufacturing = material.pipe_details?.manufacturing_method || ''; specKey = `${category}|${material_spec}|${outer_diameter}|${schedule}|${manufacturing}`; specData = { category: 'PIPE', material_spec, outer_diameter, schedule, manufacturing_method: manufacturing, unit: 'mm', isLength: true }; } else if (category === 'FITTING') { // FITTING: 타입 + 서브타입 + 연결방식 + 압력등급 + 사이즈 + 재질 const material_spec = material.fitting_details?.material_spec || material.material_grade || ''; const main_nom = material.main_nom || ''; const red_nom = material.red_nom || ''; const size_display = red_nom ? `${main_nom} x ${red_nom}` : main_nom; const fitting_type = material.fitting_details?.fitting_type || 'UNKNOWN'; const fitting_subtype = material.fitting_details?.fitting_subtype || ''; const connection_method = material.fitting_details?.connection_method || ''; const pressure_rating = material.fitting_details?.pressure_rating || ''; // 전체 피팅 스펙 생성 - 중복 제거 (OLET 특별 처리) const spec_parts = []; // OLET 계열 특별 처리 if (fitting_type === 'OLET' && fitting_subtype && fitting_subtype !== 'UNKNOWN') { // SOCKOLET, WELDOLET 등 서브타입만 표시 (OLET 생략) spec_parts.push(fitting_subtype); // OLET 계열은 연결방식이 서브타입에 이미 내포됨 // SOCKOLET = SOCKET_WELD, WELDOLET = BUTT_WELD 등 // 따라서 connection_method 생략 } else if (fitting_type === 'NIPPLE') { // NIPPLE 특별 처리 - 스케줄 + 길이 정보 포함 spec_parts.push(fitting_type); // 서브타입 (CLOSE, SHORT, LONG 등) if (fitting_subtype && fitting_subtype !== 'UNKNOWN' && fitting_subtype !== fitting_type) { spec_parts.push(fitting_subtype); } // NIPPLE 스케줄 정보 추가 (fitting_details에서 가져옴) const nipple_schedule = material.fitting_details?.schedule; if (nipple_schedule && nipple_schedule !== 'UNKNOWN') { spec_parts.push(nipple_schedule); } // 연결방식 if (connection_method && connection_method !== 'UNKNOWN' && !spec_parts.some(part => part.includes(connection_method))) { spec_parts.push(connection_method); } // NIPPLE 길이 정보 추가 (fitting_details에서 가져옴) const length_mm = material.fitting_details?.length_mm; if (length_mm && length_mm > 0) { if (length_mm >= 1000) { spec_parts.push(`${(length_mm / 1000).toFixed(2)}m`); } else { spec_parts.push(`${length_mm}mm`); } } } else { // 일반 피팅 처리 // 기본 타입 (CAP, TEE, ELBOW 등) if (fitting_type && fitting_type !== 'UNKNOWN') { spec_parts.push(fitting_type); } // 서브타입 (CONCENTRIC, HEXAGON 등) - 단, 타입과 중복되지 않을 때만 if (fitting_subtype && fitting_subtype !== 'UNKNOWN' && fitting_subtype !== fitting_type) { spec_parts.push(fitting_subtype); } // 연결방식 (THREADED, NPT 등) - 단, 이미 포함되지 않았을 때만 if (connection_method && connection_method !== 'UNKNOWN' && !spec_parts.some(part => part.includes(connection_method))) { spec_parts.push(connection_method); } } // 압력등급 (3000LB, 6000LB 등) - 모든 경우에 표시 if (pressure_rating && pressure_rating !== 'UNKNOWN' && !spec_parts.some(part => part.includes(pressure_rating))) { spec_parts.push(pressure_rating); } const full_fitting_spec = spec_parts.join(', '); specKey = `${category}|${full_fitting_spec}|${material_spec}|${size_display}`; specData = { category: 'FITTING', fitting_type, fitting_subtype, connection_method, pressure_rating, full_fitting_spec, material_spec, size_display, main_nom, red_nom, unit: 'EA', isLength: false }; } else if (category === 'FLANGE') { // FLANGE: 타입 + 압력등급 + 면가공 + 재질 const material_spec = material.flange_details?.material_spec || material.material_grade || ''; const main_nom = material.main_nom || ''; const flange_type = material.flange_details?.flange_type || 'UNKNOWN'; const pressure_rating = material.flange_details?.pressure_rating || ''; const facing_type = material.flange_details?.facing_type || ''; // 플랜지 스펙 생성 const flange_spec_parts = []; // 플랜지 타입 (WN, BL, SO 등) if (flange_type && flange_type !== 'UNKNOWN') { flange_spec_parts.push(flange_type); } // 면 가공 (RF, FF, RTJ 등) if (facing_type && facing_type !== 'UNKNOWN') { flange_spec_parts.push(facing_type); } // 압력등급 (150LB, 300LB 등) if (pressure_rating && pressure_rating !== 'UNKNOWN') { flange_spec_parts.push(pressure_rating); } const full_flange_spec = flange_spec_parts.join(', '); specKey = `${category}|${full_flange_spec}|${material_spec}|${main_nom}`; specData = { category: 'FLANGE', flange_type, pressure_rating, facing_type, full_flange_spec, material_spec, size_display: main_nom, main_nom, unit: 'EA', isLength: false }; } else if (category === 'GASKET') { // GASKET: 타입 + 소재 + 압력등급 + 사이즈 const main_nom = material.main_nom || ''; const gasket_type = material.gasket_details?.gasket_type || 'UNKNOWN'; const material_type = material.gasket_details?.material_type || 'UNKNOWN'; const pressure_rating = material.gasket_details?.pressure_rating || ''; // 가스켓 재질은 gasket_details에서 가져옴 const material_spec = material_type !== 'UNKNOWN' ? material_type : (material.material_grade || 'Unknown'); // SWG 상세 정보 파싱 (additional_info에서) let detailed_construction = 'N/A'; let face_type = ''; let thickness = material.gasket_details?.thickness || null; // API에서 gasket_details의 추가 정보를 확인 (브라우저 콘솔에서 확인용) if (material.gasket_details && Object.keys(material.gasket_details).length > 0) { console.log('Gasket details:', material.gasket_details); } // 상세 구성 정보 생성 (Face Type + Construction) // H/F/I/O SS304/GRAPHITE/CS/CS 형태로 표시 if (material.original_description) { const desc = material.original_description.toUpperCase(); // H/F/I/O 다음에 오는 재질 구성만 찾기 (H/F/I/O는 제외) const fullMatch = desc.match(/H\/F\/I\/O\s+([A-Z0-9]+\/[A-Z]+\/[A-Z0-9]+\/[A-Z0-9]+)/); if (fullMatch) { // H/F/I/O와 재질 구성 둘 다 있는 경우 face_type = 'H/F/I/O'; const construction = fullMatch[1]; detailed_construction = `${face_type} ${construction}`; } else { // H/F/I/O만 있는 경우 const faceMatch = desc.match(/H\/F\/I\/O/); if (faceMatch) { detailed_construction = 'H/F/I/O'; } else { // 재질 구성만 있는 경우 (H/F/I/O 없이) const constructionOnlyMatch = desc.match(/([A-Z0-9]+\/[A-Z]+\/[A-Z0-9]+\/[A-Z0-9]+)/); if (constructionOnlyMatch) { detailed_construction = constructionOnlyMatch[1]; } } } } // 가스켓 스펙 생성 const gasket_spec_parts = []; // 가스켓 타입 (SPIRAL_WOUND, O_RING 등) if (gasket_type && gasket_type !== 'UNKNOWN') { gasket_spec_parts.push(gasket_type.replace('_', ' ')); } // 소재 (GRAPHITE, PTFE 등) if (material_type && material_type !== 'UNKNOWN') { gasket_spec_parts.push(material_type); } // 압력등급 (150LB, 300LB 등) if (pressure_rating && pressure_rating !== 'UNKNOWN') { gasket_spec_parts.push(pressure_rating); } const full_gasket_spec = gasket_spec_parts.join(', '); specKey = `${category}|${full_gasket_spec}|${material_spec}|${main_nom}|${detailed_construction}`; specData = { category: 'GASKET', gasket_type, material_type, pressure_rating, full_gasket_spec, material_spec, size_display: main_nom, main_nom, detailed_construction, thickness, unit: 'EA', isLength: false }; } else if (category === 'BOLT') { // BOLT: 타입 + 재질 + 사이즈 + 길이 const material_spec = material.material_grade || ''; const main_nom = material.main_nom || ''; const bolt_type = material.bolt_details?.bolt_type || 'BOLT'; const material_standard = material.bolt_details?.material_standard || ''; const material_grade = material.bolt_details?.material_grade || ''; const thread_type = material.bolt_details?.thread_type || ''; const diameter = material.bolt_details?.diameter || main_nom; const length = material.bolt_details?.length || ''; const pressure_rating = material.bolt_details?.pressure_rating || ''; const coating_type = material.bolt_details?.coating_type || ''; // 볼트 스펙 생성 const bolt_spec_parts = []; // 볼트 타입 (HEX_BOLT, STUD_BOLT 등) if (bolt_type && bolt_type !== 'UNKNOWN') { bolt_spec_parts.push(bolt_type.replace('_', ' ')); } // 재질 (ASTM A193, ASTM A194 등) if (material_standard) { bolt_spec_parts.push(material_standard); if (material_grade && material_grade !== material_standard) { bolt_spec_parts.push(material_grade); } } else if (material_spec) { bolt_spec_parts.push(material_spec); } // 나사 규격 (M12, 1/2" 등) if (diameter) { bolt_spec_parts.push(diameter); } // 코팅 타입 (ELECTRO_GALVANIZED 등) if (coating_type && coating_type !== 'PLAIN') { bolt_spec_parts.push(coating_type.replace('_', ' ')); } const full_bolt_spec = bolt_spec_parts.join(', '); specKey = `${category}|${full_bolt_spec}|${diameter}|${length}|${coating_type}|${pressure_rating}`; specData = { category: 'BOLT', bolt_type, thread_type, full_bolt_spec, material_standard, material_grade, diameter, length, coating_type, pressure_rating, size_display: diameter, main_nom: diameter, unit: 'EA', isLength: false }; } else if (category === 'INSTRUMENT') { // INSTRUMENT: 타입 + 연결사이즈 + 측정범위 const main_nom = material.main_nom || ''; const instrument_type = material.instrument_details?.instrument_type || 'INSTRUMENT'; const measurement_range = material.instrument_details?.measurement_range || ''; const signal_type = material.instrument_details?.signal_type || ''; // 계기 스펙 생성 const instrument_spec_parts = []; // 계기 타입 (PRESSURE_GAUGE, TEMPERATURE_TRANSMITTER 등) if (instrument_type && instrument_type !== 'UNKNOWN') { instrument_spec_parts.push(instrument_type.replace('_', ' ')); } // 측정 범위 (0-100 PSI, 4-20mA 등) if (measurement_range) { instrument_spec_parts.push(measurement_range); } // 연결 사이즈 (1/4", 1/2" 등) if (main_nom) { instrument_spec_parts.push(`${main_nom} CONNECTION`); } const full_instrument_spec = instrument_spec_parts.join(', '); specKey = `${category}|${full_instrument_spec}|${main_nom}`; specData = { category: 'INSTRUMENT', instrument_type, measurement_range, signal_type, full_instrument_spec, size_display: main_nom, main_nom, unit: 'EA', isLength: false }; } else if (category === 'VALVE') { // VALVE: 타입 + 연결방식 + 압력등급 + 재질 + 사이즈 const main_nom = material.main_nom || ''; const valve_type = material.valve_details?.valve_type || 'VALVE'; const valve_subtype = material.valve_details?.valve_subtype || ''; const connection_method = material.valve_details?.connection_method || ''; const pressure_rating = material.valve_details?.pressure_rating || ''; const body_material = material.valve_details?.body_material || material.material_grade || ''; const actuator_type = material.valve_details?.actuator_type || 'MANUAL'; const fire_safe = material.valve_details?.fire_safe || false; // 밸브 스펙 생성 const valve_spec_parts = []; // 밸브 타입 (GATE_VALVE, BALL_VALVE 등) if (valve_type && valve_type !== 'UNKNOWN') { valve_spec_parts.push(valve_type.replace('_', ' ')); } // 연결 방식 (FLANGED, THREADED, SOCKET_WELD 등) if (connection_method && connection_method !== 'UNKNOWN') { valve_spec_parts.push(connection_method.replace('_', ' ')); } // 압력 등급 (150LB, 300LB 등) if (pressure_rating && pressure_rating !== 'UNKNOWN') { valve_spec_parts.push(pressure_rating); } // 작동 방식 (수동이 아닌 경우만 표시) if (actuator_type && actuator_type !== 'MANUAL' && actuator_type !== 'UNKNOWN') { valve_spec_parts.push(actuator_type.replace('_', ' ')); } // 특수 기능 (Fire Safe 등) if (fire_safe) { valve_spec_parts.push('FIRE SAFE'); } if (valve_subtype && valve_subtype !== 'UNKNOWN') { valve_spec_parts.push(valve_subtype); } const full_valve_spec = valve_spec_parts.join(', '); specKey = `${category}|${full_valve_spec}|${body_material}|${main_nom}`; specData = { category: 'VALVE', valve_type, valve_subtype, connection_method, pressure_rating, body_material, actuator_type, fire_safe, full_valve_spec, size_display: main_nom, main_nom, unit: 'EA', isLength: false }; } else { // 기타 자재: 기본 분류 const material_spec = material.material_grade || ''; const size_display = material.main_nom || material.size_spec || ''; specKey = `${category}|${material_spec}|${size_display}`; specData = { category, material_spec, size_display, unit: 'EA', isLength: false }; } if (!specs[specKey]) { specs[specKey] = { ...specData, totalQuantity: 0, totalLength: 0, count: 0, items: [] }; } specs[specKey].totalQuantity += material.quantity || 0; specs[specKey].count += 1; specs[specKey].items.push(material); // PIPE의 경우 길이 합산 if (category === 'PIPE' && material.pipe_details?.length_mm) { specs[specKey].totalLength += material.pipe_details.length_mm; } }); return Object.values(specs); }; const formatLength = (lengthMm) => { if (!lengthMm || lengthMm === 0) return '0mm'; if (lengthMm >= 1000) { return `${(lengthMm / 1000).toFixed(2)}m (${lengthMm.toLocaleString()}mm)`; } return `${lengthMm.toLocaleString()}mm`; }; const getCategoryColor = (category) => { const colors = { 'PIPE': 'primary', 'FITTING': 'secondary', 'VALVE': 'success', 'BOLT': 'warning', 'GASKET': 'info', 'INSTRUMENT': 'error', 'UNKNOWN': 'default' }; return colors[category] || 'default'; }; // 엑셀 내보내기 함수 const handleExportToExcel = () => { if (materials.length === 0) { alert('내보낼 자재 데이터가 없습니다.'); return; } const additionalInfo = { filename: fileName, jobNo: jobNo, revision: currentRevision, uploadDate: new Date().toLocaleDateString() }; const baseFilename = `자재목록_${jobNo}_${currentRevision}`; exportMaterialsToExcel(materials, baseFilename, additionalInfo); }; if (loading) { return ( 자재 정보를 불러오는 중... ); } if (error) { return ( ⚠️ {error} 💡 해당 파일에 자재 정보가 없습니다. ); } const materialSpecs = generateMaterialSpecs(materials); const totalSpecs = materialSpecs.length; const categoryStats = materialSpecs.reduce((acc, spec) => { acc[spec.category] = (acc[spec.category] || 0) + 1; return acc; }, {}); return ( {/* 헤더 */} {/* 리비전 비교 버튼 */} {availableRevisions.length > 1 && currentRevision !== 'Rev.0' && ( )} {/* 리비전 선택 */} {availableRevisions.length > 1 && ( 📋 {bomName} Job No: {jobNo} | 현재 리비전: {currentRevision} 리비전 선택 )} 📋 자재 사양서 업체 견적 요청용 자재 사양 목록 {fileName && ( 파일명: {fileName} )} {/* 통계 요약 */} 📊 사양 요약 {Object.entries(categoryStats).map(([category, count]) => ( ))} {/* 카테고리별 자재 사양서 */} {Object.entries(categoryStats).map(([category, count]) => { const categorySpecs = materialSpecs.filter(spec => spec.category === category); return ( 🔧 {category} 사양 ({count}개) {category === 'PIPE' ? '동일한 재질·외경·스케줄·제작방식의 파이프들을 그룹화하여 표시합니다.' : '동일한 사양의 자재들을 그룹화하여 수량을 합산합니다.' } {category === 'PIPE' && ( <> 사양 외경 스케줄 제작방식 총 길이 )} {category === 'FITTING' && ( <> 품목 재질 사이즈 수량 )} {category === 'FLANGE' && ( <> 플랜지 타입 재질 사이즈 수량 )} {category === 'GASKET' && ( <> 가스켓 타입 상세 구성 재질 두께 사이즈 수량 )} {category === 'BOLT' && ( <> 볼트 타입 재질 사이즈 길이 코팅 압력등급 수량 )} {category === 'INSTRUMENT' && ( <> 계기 타입 측정범위 연결사이즈 수량 )} {category === 'VALVE' && ( <> 밸브 타입 연결방식 압력등급 재질 사이즈 작동방식 수량 )} {!['PIPE', 'FITTING', 'FLANGE', 'GASKET', 'BOLT', 'INSTRUMENT', 'VALVE'].includes(category) && ( <> 재질 사이즈 수량 )} 개수 {categorySpecs.map((spec, index) => ( {category === 'PIPE' && ( <> {spec.material_spec || 'Unknown'} {spec.outer_diameter || 'Unknown'} {spec.schedule || 'Unknown'} {spec.manufacturing_method || 'Unknown'} {formatLength(spec.totalLength)} )} {category === 'FITTING' && ( <> {spec.full_fitting_spec || spec.fitting_type || 'UNKNOWN'} {spec.material_spec || 'Unknown'} {spec.size_display || 'Unknown'} {spec.totalQuantity} {spec.unit} )} {category === 'FLANGE' && ( <> {spec.full_flange_spec || spec.flange_type || 'UNKNOWN'} {spec.material_spec || 'Unknown'} {spec.size_display || 'Unknown'} {spec.totalQuantity} {spec.unit} )} {category === 'GASKET' && ( <> {spec.gasket_type?.replace('_', ' ') || 'UNKNOWN'} {spec.detailed_construction || 'N/A'} {spec.material_spec || 'Unknown'} {spec.thickness ? `${spec.thickness}mm` : 'N/A'} {spec.size_display || 'Unknown'} {spec.totalQuantity} {spec.unit} )} {category === 'BOLT' && ( <> {spec.bolt_type?.replace('_', ' ') || 'UNKNOWN'} {spec.material_standard || 'Unknown'} {spec.material_grade || ''} {spec.diameter ? (spec.diameter.includes('"') ? spec.diameter : spec.diameter.replace('0.5', '1/2"').replace('0.75', '3/4"').replace('1.0', '1"').replace('1.5', '1 1/2"')) : 'Unknown'} {spec.length || 'N/A'} {spec.coating_type ? spec.coating_type.replace('_', ' ') : 'N/A'} {spec.pressure_rating || 'N/A'} {spec.totalQuantity} {spec.unit} )} {category === 'INSTRUMENT' && ( <> {spec.instrument_type?.replace('_', ' ') || 'UNKNOWN'} {spec.measurement_range || 'Unknown'} {spec.size_display || 'Unknown'} {spec.totalQuantity} {spec.unit} )} {category === 'VALVE' && ( <> {spec.valve_type?.replace('_', ' ') || 'VALVE'} {spec.connection_method?.replace('_', ' ') || 'Unknown'} {spec.pressure_rating || 'Unknown'} {spec.body_material || 'Unknown'} {spec.size_display || 'Unknown'} {spec.actuator_type?.replace('_', ' ') || 'MANUAL'} {spec.fire_safe && ' + FIRE SAFE'} {spec.totalQuantity} {spec.unit} )} {(!['PIPE', 'FITTING', 'FLANGE', 'GASKET', 'BOLT', 'INSTRUMENT', 'VALVE'].includes(category)) && ( <> {spec.material_spec || 'Unknown'} {spec.size_display || 'Unknown'} {spec.totalQuantity} {spec.unit} )} ))}
); })} {materialSpecs.length === 0 && ( 💡 해당 파일에 자재 정보가 없습니다. )}
); }; export default MaterialsPage;