import React, { useState, useEffect } from 'react'; import { fetchMaterials } from '../api'; import { exportMaterialsToExcel } from '../utils/excelExport'; import * as XLSX from 'xlsx'; import { saveAs } from 'file-saver'; import api from '../api'; import './NewMaterialsPage.css'; const NewMaterialsPage = ({ onNavigate, selectedProject, fileId, jobNo, bomName, revision, filename }) => { const [materials, setMaterials] = useState([]); const [loading, setLoading] = useState(true); const [selectedCategory, setSelectedCategory] = useState('PIPE'); const [selectedMaterials, setSelectedMaterials] = useState(new Set()); const [viewMode, setViewMode] = useState('detailed'); // 'detailed' or 'simple' const [availableRevisions, setAvailableRevisions] = useState([]); const [currentRevision, setCurrentRevision] = useState(revision || 'Rev.0'); // 사용자 요구사항 상태 관리 const [userRequirements, setUserRequirements] = useState({}); // materialId: requirement 형태 const [savingRequirements, setSavingRequirements] = useState(false); // 정렬 및 필터링 상태 const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); const [columnFilters, setColumnFilters] = useState({}); const [showFilterDropdown, setShowFilterDropdown] = useState(null); // 같은 BOM의 다른 리비전들 조회 const loadAvailableRevisions = async () => { try { const response = await api.get('/files/', { params: { job_no: jobNo } }); const allFiles = Array.isArray(response.data) ? response.data : response.data?.files || []; const sameBomFiles = allFiles.filter(file => (file.bom_name || file.original_filename) === bomName ); // 리비전별로 정렬 (최신순) sameBomFiles.sort((a, b) => { const revA = parseInt(a.revision?.replace('Rev.', '') || '0'); const revB = parseInt(b.revision?.replace('Rev.', '') || '0'); return revB - revA; }); setAvailableRevisions(sameBomFiles); console.log('📋 사용 가능한 리비전:', sameBomFiles); } catch (error) { console.error('리비전 목록 조회 실패:', error); } }; // 자재 데이터 로드 useEffect(() => { if (fileId) { loadMaterials(fileId); loadAvailableRevisions(); loadUserRequirements(fileId); } }, [fileId]); // 외부 클릭 시 필터 드롭다운 닫기 useEffect(() => { const handleClickOutside = (event) => { if (!event.target.closest('.filterable-header')) { setShowFilterDropdown(null); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, []); const loadMaterials = async (id) => { try { setLoading(true); console.log('🔍 자재 데이터 로딩 중...', { file_id: id }); const response = await fetchMaterials({ file_id: parseInt(id), limit: 10000 }); if (response.data?.materials) { const materialsData = response.data.materials; console.log(`✅ ${materialsData.length}개 자재 로드 완료`); // 파이프 데이터 검증 const pipes = materialsData.filter(m => m.classified_category === 'PIPE'); if (pipes.length > 0) { console.log('📊 파이프 데이터 샘플:', pipes[0]); } setMaterials(materialsData); } } catch (error) { console.error('❌ 자재 로딩 실패:', error); setMaterials([]); } finally { setLoading(false); } }; // 사용자 요구사항 로드 const loadUserRequirements = async (id) => { try { console.log('🔍 사용자 요구사항 로딩 중...', { file_id: id }); const response = await api.get('/files/user-requirements', { params: { file_id: parseInt(id) } }); if (response.data && Array.isArray(response.data)) { const requirements = {}; console.log('📦 API 응답 데이터:', response.data); response.data.forEach(req => { // material_id를 키로 사용하여 요구사항 저장 if (req.material_id) { requirements[req.material_id] = req.requirement_description || req.requirement_title || ''; console.log(`📥 로드된 요구사항: 자재 ID ${req.material_id} = "${requirements[req.material_id]}"`); } else { console.warn('⚠️ material_id가 없는 요구사항:', req); } }); console.log('🔄 setUserRequirements 호출 전 상태:', userRequirements); setUserRequirements(requirements); console.log('🔄 setUserRequirements 호출 후 새로운 상태:', requirements); console.log('✅ 사용자 요구사항 로드 완료:', Object.keys(requirements).length, '개'); // 상태 업데이트 확인을 위한 지연된 로그 setTimeout(() => { console.log('⏰ 1초 후 실제 userRequirements 상태:', userRequirements); }, 1000); } } catch (error) { console.error('❌ 사용자 요구사항 로딩 실패:', error); setUserRequirements({}); } }; // 사용자 요구사항 저장 const saveUserRequirements = async () => { try { setSavingRequirements(true); // 강제 테스트: userRequirements가 비어있으면 첫 번째 자재에 테스트 데이터 추가 let currentRequirements = { ...userRequirements }; if (Object.keys(currentRequirements).length === 0 && materials.length > 0) { const firstMaterialId = materials[0].id; currentRequirements[firstMaterialId] = '강제 테스트 요구사항'; setUserRequirements(currentRequirements); console.log('⚠️ 테스트 데이터 강제 추가:', currentRequirements); } // 디버깅: 현재 userRequirements 상태 확인 console.log('💾 저장 시작 - 현재 userRequirements:', currentRequirements); console.log('💾 저장 시작 - userRequirements 키 개수:', Object.keys(currentRequirements).length); console.log('💾 사용자 요구사항 저장 중...', userRequirements); console.log('📋 전체 userRequirements 객체:', Object.keys(userRequirements).length, '개'); // 요구사항이 있는 자재들만 저장 const requirementsToSave = Object.entries(currentRequirements) .filter(([materialId, requirement]) => { const hasValue = requirement && requirement.trim() && requirement.trim().length > 0; console.log(`🔍 자재 ID ${materialId}: "${requirement}" (길이: ${requirement ? requirement.length : 0}) -> ${hasValue ? '저장' : '제외'}`); return hasValue; }) .map(([materialId, requirement]) => ({ material_id: parseInt(materialId), file_id: parseInt(fileId), requirement_type: 'CUSTOM_SPEC', requirement_title: '사용자 요구사항', requirement_description: requirement.trim(), priority: 'NORMAL' })); console.log('📝 저장할 요구사항 개수:', requirementsToSave.length); if (requirementsToSave.length === 0) { alert('저장할 요구사항이 없습니다.'); return; } // 기존 요구사항 삭제 후 새로 저장 console.log('🗑️ 기존 요구사항 삭제 중...', { file_id: parseInt(fileId) }); console.log('🌐 API Base URL:', api.defaults.baseURL); console.log('🔑 Authorization Header:', api.defaults.headers.Authorization); try { const deleteResponse = await api.delete(`/files/user-requirements`, { params: { file_id: parseInt(fileId) } }); console.log('✅ 기존 요구사항 삭제 완료:', deleteResponse.data); } catch (deleteError) { console.error('❌ 기존 요구사항 삭제 실패:', deleteError); console.error('❌ 삭제 에러 상세:', deleteError.response?.data); console.error('❌ 삭제 에러 전체:', deleteError); // 삭제 실패해도 계속 진행 } // 새 요구사항들 저장 console.log('🚀 API 호출 시작 - 저장할 데이터:', requirementsToSave); for (const req of requirementsToSave) { console.log('🚀 개별 API 호출:', req); try { const response = await api.post('/files/user-requirements', req); console.log('✅ API 응답:', response.data); } catch (apiError) { console.error('❌ API 호출 실패:', apiError); console.error('❌ API 에러 상세:', apiError.response?.data); throw apiError; } } alert(`${requirementsToSave.length}개의 사용자 요구사항이 저장되었습니다.`); console.log('✅ 사용자 요구사항 저장 완료'); // 저장 후 다시 로드하여 최신 상태 반영 console.log('🔄 저장 완료 후 다시 로드 시작...'); await loadUserRequirements(fileId); console.log('🔄 저장 완료 후 다시 로드 완료!'); } catch (error) { console.error('❌ 사용자 요구사항 저장 실패:', error); alert('사용자 요구사항 저장에 실패했습니다: ' + (error.response?.data?.detail || error.message)); } finally { setSavingRequirements(false); } }; // 사용자 요구사항 입력 핸들러 const handleUserRequirementChange = (materialId, value) => { console.log(`📝 사용자 요구사항 입력: 자재 ID ${materialId} = "${value}"`); setUserRequirements(prev => { const updated = { ...prev, [materialId]: value }; console.log('🔄 업데이트된 userRequirements:', updated); return updated; }); }; // 카테고리별 자재 수 계산 const getCategoryCounts = () => { const counts = {}; materials.forEach(material => { const category = material.classified_category || 'UNKNOWN'; counts[category] = (counts[category] || 0) + 1; }); return counts; }; // 파이프 구매 수량 계산 함수 const calculatePipePurchase = (material) => { // 백엔드에서 이미 그룹핑된 데이터 사용 const totalLength = material.pipe_details?.total_length_mm || 0; const pipeCount = material.pipe_details?.pipe_count || material.quantity || 0; // 절단 손실: 각 단관마다 2mm const cuttingLoss = pipeCount * 2; // 총 필요 길이 const requiredLength = totalLength + cuttingLoss; // 6M(6000mm) 단위로 구매 본수 계산 const purchaseCount = Math.ceil(requiredLength / 6000); return { pipeCount, // 단관 개수 totalLength, // 총 BOM 길이 cuttingLoss, // 절단 손실 requiredLength, // 필요 길이 purchaseCount // 구매 본수 }; }; // 카테고리 표시명 매핑 const getCategoryDisplayName = (category) => { const categoryMap = { 'SPECIAL': 'SPECIAL', 'U_BOLT': 'U-BOLT', 'SUPPORT': 'SUPPORT', 'PIPE': 'PIPE', 'FITTING': 'FITTING', 'FLANGE': 'FLANGE', 'VALVE': 'VALVE', 'BOLT': 'BOLT', 'GASKET': 'GASKET', 'INSTRUMENT': 'INSTRUMENT', 'UNKNOWN': 'UNKNOWN' }; return categoryMap[category] || category; }; // 니플 끝단 정보 추출 const extractNippleEndInfo = (description) => { const descUpper = description.toUpperCase(); // 니플 끝단 패턴들 const endPatterns = { 'PBE': 'PBE', // Plain Both End 'BBE': 'BBE', // Bevel Both End 'POE': 'POE', // Plain One End 'BOE': 'BOE', // Bevel One End 'TOE': 'TOE', // Thread One End 'SW X NPT': 'SW×NPT', // Socket Weld x NPT 'SW X SW': 'SW×SW', // Socket Weld x Socket Weld 'NPT X NPT': 'NPT×NPT', // NPT x NPT }; for (const [pattern, display] of Object.entries(endPatterns)) { if (descUpper.includes(pattern)) { return display; } } return ''; }; // 볼트 추가요구사항 추출 const extractBoltAdditionalRequirements = (description) => { const descUpper = description.toUpperCase(); const additionalReqs = []; // 표면처리 패턴들 (원본 영어 약어 사용) const surfaceTreatments = { 'ELEC.GALV': 'ELEC.GALV', 'ELEC GALV': 'ELEC.GALV', 'GALVANIZED': 'GALVANIZED', 'GALV': 'GALV', 'HOT DIP GALV': 'HDG', 'HDG': 'HDG', 'ZINC PLATED': 'ZINC PLATED', 'ZINC': 'ZINC', 'STAINLESS': 'STAINLESS', 'SS': 'SS' }; // 표면처리 확인 for (const [pattern, treatment] of Object.entries(surfaceTreatments)) { if (descUpper.includes(pattern)) { additionalReqs.push(treatment); } } // 중복 제거 const uniqueReqs = [...new Set(additionalReqs)]; return uniqueReqs.join(', '); }; // 자재 정보 파싱 const parseMaterialInfo = (material) => { const category = material.classified_category; if (category === 'PIPE') { const calc = calculatePipePurchase(material); return { type: 'PIPE', subtype: material.pipe_details?.manufacturing_method || 'SMLS', size: material.size_spec || '-', schedule: material.pipe_details?.schedule || '-', grade: material.full_material_grade || material.material_grade || '-', // 전체 재질명 우선 사용 quantity: calc.purchaseCount, unit: '본', details: calc }; } else if (category === 'FITTING') { const fittingDetails = material.fitting_details || {}; const classificationDetails = material.classification_details || {}; // 개선된 분류기 결과 우선 사용 const fittingTypeInfo = classificationDetails.fitting_type || {}; const scheduleInfo = classificationDetails.schedule_info || {}; // 기존 필드와 새 필드 통합 const fittingType = fittingTypeInfo.type || fittingDetails.fitting_type || ''; const fittingSubtype = fittingTypeInfo.subtype || fittingDetails.fitting_subtype || ''; const mainSchedule = scheduleInfo.main_schedule || fittingDetails.schedule || ''; const redSchedule = scheduleInfo.red_schedule || ''; const hasDifferentSchedules = scheduleInfo.has_different_schedules || false; const description = material.original_description || ''; // 피팅 타입별 상세 표시 let displayType = ''; // 개선된 분류기 결과 우선 표시 if (fittingType === 'TEE' && fittingSubtype === 'REDUCING') { displayType = 'TEE REDUCING'; } else if (fittingType === 'REDUCER' && fittingSubtype === 'CONCENTRIC') { displayType = 'REDUCER CONC'; } else if (fittingType === 'REDUCER' && fittingSubtype === 'ECCENTRIC') { displayType = 'REDUCER ECC'; } else if (description.toUpperCase().includes('TEE RED')) { // 기존 데이터의 TEE RED 패턴 displayType = 'TEE REDUCING'; } else if (description.toUpperCase().includes('RED CONC')) { // 기존 데이터의 RED CONC 패턴 displayType = 'REDUCER CONC'; } else if (description.toUpperCase().includes('RED ECC')) { // 기존 데이터의 RED ECC 패턴 displayType = 'REDUCER ECC'; } else if (description.toUpperCase().includes('CAP')) { // CAP: 연결 방식 표시 (예: CAP, NPT(F), 3000LB, ASTM A105) if (description.includes('NPT(F)')) { displayType = 'CAP NPT(F)'; } else if (description.includes('SW')) { displayType = 'CAP SW'; } else if (description.includes('BW')) { displayType = 'CAP BW'; } else { displayType = 'CAP'; } } else if (description.toUpperCase().includes('PLUG')) { // PLUG: 타입과 연결 방식 표시 (예: HEX.PLUG, NPT(M), 6000LB, ASTM A105) if (description.toUpperCase().includes('HEX')) { if (description.includes('NPT(M)')) { displayType = 'HEX PLUG NPT(M)'; } else { displayType = 'HEX PLUG'; } } else if (description.includes('NPT(M)')) { displayType = 'PLUG NPT(M)'; } else if (description.includes('NPT')) { displayType = 'PLUG NPT'; } else { displayType = 'PLUG'; } } else if (fittingType === 'NIPPLE') { // 니플: 길이와 끝단 가공 정보 const length = fittingDetails.length_mm || fittingDetails.avg_length_mm; const endInfo = extractNippleEndInfo(description); let nippleType = 'NIPPLE'; if (length) nippleType += ` ${length}mm`; if (endInfo) nippleType += ` ${endInfo}`; displayType = nippleType; } else if (fittingType === 'ELBOW') { // 엘보: 각도와 연결 방식 const angle = fittingSubtype === '90DEG' ? '90°' : fittingSubtype === '45DEG' ? '45°' : ''; const connection = description.includes('SW') ? 'SW' : description.includes('BW') ? 'BW' : ''; displayType = `ELBOW ${angle} ${connection}`.trim(); } else if (fittingType === 'TEE') { // 티: 타입과 연결 방식 const teeType = fittingSubtype === 'EQUAL' ? 'EQ' : fittingSubtype === 'REDUCING' ? 'RED' : ''; const connection = description.includes('SW') ? 'SW' : description.includes('BW') ? 'BW' : ''; displayType = `TEE ${teeType} ${connection}`.trim(); } else if (fittingType === 'REDUCER') { // 레듀서: 콘센트릭/에센트릭 const reducerType = fittingSubtype === 'CONCENTRIC' ? 'CONC' : fittingSubtype === 'ECCENTRIC' ? 'ECC' : ''; const sizes = fittingDetails.reduced_size ? `${material.size_spec}×${fittingDetails.reduced_size}` : material.size_spec; displayType = `RED ${reducerType} ${sizes}`.trim(); } else if (fittingType === 'SWAGE') { // 스웨이지: 타입 명시 const swageType = fittingSubtype || ''; displayType = `SWAGE ${swageType}`.trim(); } else if (!displayType) { // 기타 피팅 타입 displayType = fittingType || 'FITTING'; } // 압력 등급과 스케줄 추출 let pressure = '-'; let schedule = '-'; // 압력 등급 찾기 (3000LB, 6000LB 등) const pressureMatch = description.match(/(\d+)LB/i); if (pressureMatch) { pressure = `${pressureMatch[1]}LB`; } // 스케줄 표시 (분리 스케줄 지원) if (hasDifferentSchedules && mainSchedule && redSchedule) { // 분리 스케줄: "SCH 40 x SCH 80" schedule = `${mainSchedule} x ${redSchedule}`; } else if (mainSchedule && mainSchedule !== 'UNKNOWN') { // 단일 스케줄: "SCH 40" schedule = mainSchedule; } else if (description.includes('SCH')) { // 기존 데이터에서 분리 스케줄 패턴 확인 const separatedSchMatch = description.match(/SCH\s*(\d+[A-Z]*)\s*[xX×]\s*SCH\s*(\d+[A-Z]*)/i); if (separatedSchMatch) { // 분리 스케줄 발견: "SCH 40 x SCH 80" schedule = `SCH ${separatedSchMatch[1]} x SCH ${separatedSchMatch[2]}`; } else { // 단일 스케줄 const schMatch = description.match(/SCH\s*(\d+[A-Z]*)/i); if (schMatch) { schedule = `SCH ${schMatch[1]}`; } } } return { type: 'FITTING', subtype: displayType, size: material.size_spec || '-', pressure: pressure, schedule: schedule, grade: material.full_material_grade || material.material_grade || '-', // 전체 재질명 우선 사용 quantity: Math.round(material.quantity || 0), unit: '개', isFitting: true }; } else if (category === 'VALVE') { const valveDetails = material.valve_details || {}; const description = material.original_description || ''; // 밸브 타입 파싱 (GATE, BALL, CHECK, GLOBE 등) let valveType = valveDetails.valve_type || ''; if (!valveType && description) { if (description.includes('GATE')) valveType = 'GATE'; else if (description.includes('BALL')) valveType = 'BALL'; else if (description.includes('CHECK')) valveType = 'CHECK'; else if (description.includes('GLOBE')) valveType = 'GLOBE'; else if (description.includes('BUTTERFLY')) valveType = 'BUTTERFLY'; } // 연결 방식 파싱 (FLG, SW, THRD 등) let connectionType = ''; if (description.includes('FLG')) { connectionType = 'FLG'; } else if (description.includes('SW X THRD')) { connectionType = 'SW×THRD'; } else if (description.includes('SW')) { connectionType = 'SW'; } else if (description.includes('THRD')) { connectionType = 'THRD'; } else if (description.includes('BW')) { connectionType = 'BW'; } // 압력 등급 파싱 let pressure = '-'; const pressureMatch = description.match(/(\d+)LB/i); if (pressureMatch) { pressure = `${pressureMatch[1]}LB`; } // 스케줄은 밸브에는 일반적으로 없음 let schedule = '-'; return { type: 'VALVE', valveType: valveType, connectionType: connectionType, size: material.size_spec || '-', pressure: pressure, schedule: schedule, grade: material.material_grade || '-', quantity: Math.round(material.quantity || 0), unit: '개', isValve: true }; } else if (category === 'FLANGE') { const description = material.original_description || ''; // 백엔드에서 개선된 플랜지 타입 제공 (WN RF, SO FF 등) const displayType = material.flange_details?.flange_type || '-'; // 원본 설명에서 스케줄 추출 let schedule = '-'; const upperDesc = description.toUpperCase(); // SCH 40, SCH 80 등의 패턴 찾기 if (upperDesc.includes('SCH')) { const schMatch = description.match(/SCH\s*(\d+[A-Z]*)/i); if (schMatch && schMatch[1]) { schedule = `SCH ${schMatch[1]}`; } } return { type: 'FLANGE', subtype: displayType, // 백엔드에서 개선된 타입 정보 제공 (WN RF, SO FF 등) size: material.size_spec || '-', pressure: material.flange_details?.pressure_rating || '-', schedule: schedule, grade: material.full_material_grade || material.material_grade || '-', // 전체 재질명 우선 사용 quantity: Math.round(material.quantity || 0), unit: '개', isFlange: true // 플랜지 구분용 플래그 }; } else if (category === 'BOLT') { const qty = Math.round(material.quantity || 0); const safetyQty = Math.ceil(qty * 1.05); // 5% 여유율 const purchaseQty = Math.ceil(safetyQty / 4) * 4; // 4의 배수 // 볼트 상세 정보 우선 사용 const boltDetails = material.bolt_details || {}; // 길이 정보 (bolt_details 우선, 없으면 원본 설명에서 추출) let boltLength = '-'; if (boltDetails.length && boltDetails.length !== '-') { boltLength = boltDetails.length; } else { // 원본 설명에서 길이 추출 const description = material.original_description || ''; const lengthPatterns = [ /(\d+(?:\.\d+)?)\s*LG/i, // 75 LG, 90.0000 LG /(\d+(?:\.\d+)?)\s*mm/i, // 50mm /(\d+(?:\.\d+)?)\s*MM/i, // 50MM /LG[,\s]*(\d+(?:\.\d+)?)/i // LG, 75 형태 ]; for (const pattern of lengthPatterns) { const match = description.match(pattern); if (match) { let lengthValue = match[1]; // 소수점 제거 (145.0000 → 145) if (lengthValue.includes('.') && lengthValue.endsWith('.0000')) { lengthValue = lengthValue.split('.')[0]; } else if (lengthValue.includes('.') && /\.0+$/.test(lengthValue)) { lengthValue = lengthValue.split('.')[0]; } boltLength = `${lengthValue}mm`; break; } } } // 재질 정보 (bolt_details 우선, 없으면 기본 필드 사용) let boltGrade = '-'; if (boltDetails.material_standard && boltDetails.material_grade) { // bolt_details에서 완전한 재질 정보 구성 if (boltDetails.material_grade !== 'UNKNOWN' && boltDetails.material_grade !== boltDetails.material_standard) { boltGrade = `${boltDetails.material_standard} ${boltDetails.material_grade}`; } else { boltGrade = boltDetails.material_standard; } } else if (material.full_material_grade && material.full_material_grade !== '-') { boltGrade = material.full_material_grade; } else if (material.material_grade && material.material_grade !== '-') { boltGrade = material.material_grade; } // 볼트 타입 (PSV_BOLT, LT_BOLT 등) let boltSubtype = 'BOLT_GENERAL'; if (boltDetails.bolt_type && boltDetails.bolt_type !== 'UNKNOWN') { boltSubtype = boltDetails.bolt_type; } else { // 원본 설명에서 특수 볼트 타입 추출 const description = material.original_description || ''; const upperDesc = description.toUpperCase(); if (upperDesc.includes('PSV')) { boltSubtype = 'PSV_BOLT'; } else if (upperDesc.includes('LT')) { boltSubtype = 'LT_BOLT'; } else if (upperDesc.includes('CK')) { boltSubtype = 'CK_BOLT'; } } // 추가요구사항 추출 (ELEC.GALV 등) const additionalReq = extractBoltAdditionalRequirements(material.original_description || ''); return { type: 'BOLT', subtype: boltSubtype, size: material.size_spec || material.main_nom || '-', schedule: boltLength, // 길이 정보 grade: boltGrade, additionalReq: additionalReq, // 추가요구사항 quantity: purchaseQty, unit: 'SETS' }; } else if (category === 'GASKET') { const qty = Math.round(material.quantity || 0); const purchaseQty = Math.ceil(qty / 5) * 5; // 5의 배수 // original_description에서 재질 정보 파싱 const description = material.original_description || ''; let materialStructure = '-'; // H/F/I/O 부분 let materialDetail = '-'; // SS304/GRAPHITE/CS/CS 부분 // H/F/I/O와 재질 상세 정보 추출 const materialMatch = description.match(/H\/F\/I\/O\s+(.+?)(?:,|$)/); if (materialMatch) { materialStructure = 'H/F/I/O'; materialDetail = materialMatch[1].trim(); // 두께 정보 제거 (별도 추출) materialDetail = materialDetail.replace(/,?\s*\d+(?:\.\d+)?mm$/, '').trim(); } // 압력 정보 추출 let pressure = '-'; const pressureMatch = description.match(/(\d+LB)/); if (pressureMatch) { pressure = pressureMatch[1]; } // 두께 정보 추출 let thickness = '-'; const thicknessMatch = description.match(/(\d+(?:\.\d+)?)\s*mm/i); if (thicknessMatch) { thickness = thicknessMatch[1] + 'mm'; } return { type: 'GASKET', subtype: 'SWG', // 항상 SWG로 표시 size: material.size_spec || '-', pressure: pressure, materialStructure: materialStructure, materialDetail: materialDetail, thickness: thickness, quantity: purchaseQty, unit: '개', isGasket: true }; } else if (category === 'UNKNOWN') { return { type: 'UNKNOWN', description: material.original_description || 'Unknown Item', quantity: Math.round(material.quantity || 0), unit: '개', isUnknown: true }; } else { return { type: category || 'UNKNOWN', subtype: '-', size: material.size_spec || '-', schedule: '-', grade: material.material_grade || '-', quantity: Math.round(material.quantity || 0), unit: '개' }; } }; // 정렬 함수 const handleSort = (key) => { let direction = 'asc'; if (sortConfig.key === key && sortConfig.direction === 'asc') { direction = 'desc'; } setSortConfig({ key, direction }); }; // 필터 함수 const handleFilter = (column, value) => { setColumnFilters(prev => ({ ...prev, [column]: value })); }; // 필터 초기화 const clearFilter = (column) => { setColumnFilters(prev => { const newFilters = { ...prev }; delete newFilters[column]; return newFilters; }); }; // 모든 필터 초기화 const clearAllFilters = () => { setColumnFilters({}); setSortConfig({ key: null, direction: 'asc' }); }; // 필터링된 자재 목록 const filteredMaterials = materials .filter(material => { // 카테고리 필터 if (material.classified_category !== selectedCategory) { return false; } // 컬럼 필터 적용 for (const [column, filterValue] of Object.entries(columnFilters)) { if (!filterValue) continue; const info = parseMaterialInfo(material); let materialValue = ''; switch (column) { case 'type': materialValue = info.type || ''; break; case 'subtype': materialValue = info.subtype || ''; break; case 'size': materialValue = info.size || ''; break; case 'schedule': materialValue = info.schedule || ''; break; case 'grade': materialValue = info.grade || ''; break; case 'quantity': materialValue = info.quantity?.toString() || ''; break; default: materialValue = material[column]?.toString() || ''; } if (!materialValue.toLowerCase().includes(filterValue.toLowerCase())) { return false; } } return true; }) .sort((a, b) => { if (!sortConfig.key) return 0; const aInfo = parseMaterialInfo(a); const bInfo = parseMaterialInfo(b); let aValue, bValue; switch (sortConfig.key) { case 'type': aValue = aInfo.type || ''; bValue = bInfo.type || ''; break; case 'subtype': aValue = aInfo.subtype || ''; bValue = bInfo.subtype || ''; break; case 'size': aValue = aInfo.size || ''; bValue = bInfo.size || ''; break; case 'schedule': aValue = aInfo.schedule || ''; bValue = bInfo.schedule || ''; break; case 'grade': aValue = aInfo.grade || ''; bValue = bInfo.grade || ''; break; case 'quantity': aValue = aInfo.quantity || 0; bValue = bInfo.quantity || 0; break; default: aValue = a[sortConfig.key] || ''; bValue = b[sortConfig.key] || ''; } // 숫자 비교 if (typeof aValue === 'number' && typeof bValue === 'number') { return sortConfig.direction === 'asc' ? aValue - bValue : bValue - aValue; } // 문자열 비교 const comparison = aValue.toString().localeCompare(bValue.toString()); return sortConfig.direction === 'asc' ? comparison : -comparison; }); // 카테고리 색상 (제거 - CSS에서 처리) // 전체 선택/해제 const toggleAllSelection = () => { if (selectedMaterials.size === filteredMaterials.length) { setSelectedMaterials(new Set()); } else { setSelectedMaterials(new Set(filteredMaterials.map(m => m.id))); } }; // 개별 선택 const toggleMaterialSelection = (id) => { const newSelection = new Set(selectedMaterials); if (newSelection.has(id)) { newSelection.delete(id); } else { newSelection.add(id); } setSelectedMaterials(newSelection); }; // 필터 헤더 컴포넌트 const FilterableHeader = ({ children, sortKey, filterKey, className = "" }) => { const uniqueValues = React.useMemo(() => { const values = new Set(); // 현재 선택된 카테고리의 자재들만 필터링 const categoryMaterials = materials.filter(material => { return material.classified_category === selectedCategory; }); categoryMaterials.forEach(material => { const info = parseMaterialInfo(material); let value = ''; switch (filterKey) { case 'type': value = info.type || ''; break; case 'subtype': value = info.subtype || ''; break; case 'size': value = info.size || ''; break; case 'schedule': value = info.schedule || ''; break; case 'grade': value = info.grade || ''; break; case 'quantity': value = info.quantity?.toString() || ''; break; default: value = material[filterKey]?.toString() || ''; } if (value) values.add(value); }); return Array.from(values).sort(); }, [materials, filterKey, selectedCategory]); return (
{children}
{/* 정렬 버튼 */} {/* 필터 버튼 */}
{/* 필터 드롭다운 */} {showFilterDropdown === filterKey && (
handleFilter(filterKey, e.target.value)} autoFocus /> {columnFilters[filterKey] && ( )}
값 목록:
{uniqueValues.slice(0, 20).map(value => (
{ handleFilter(filterKey, value); setShowFilterDropdown(null); }} > {value}
))} {uniqueValues.length > 20 && (
+{uniqueValues.length - 20}개 더...
)}
)}
); }; // 엑셀 내보내기 - 개선된 버전 사용 const exportToExcel = () => { try { // 내보낼 데이터 결정 (선택 항목 또는 현재 카테고리 전체) const dataToExport = selectedMaterials.size > 0 ? filteredMaterials.filter(m => selectedMaterials.has(m.id)) : filteredMaterials; console.log('📊 엑셀 내보내기:', dataToExport.length, '개 항목'); // 사용자 요구사항을 자재에 추가 const dataWithRequirements = dataToExport.map(material => ({ ...material, user_requirement: userRequirements[material.id] || '' })); // 개선된 엑셀 내보내기 함수 사용 const additionalInfo = { filename: filename || bomName, jobNo: jobNo, revision: currentRevision, uploadDate: new Date().toLocaleDateString() }; const fileName = `${selectedCategory}_${jobNo || 'export'}_${new Date().toISOString().split('T')[0]}.xlsx`; exportMaterialsToExcel(dataWithRequirements, fileName, additionalInfo); console.log('✅ 엑셀 내보내기 성공'); } catch (error) { console.error('❌ 엑셀 내보내기 실패:', error); alert('엑셀 내보내기에 실패했습니다.'); } }; if (loading) { return (

자재 목록을 불러오는 중...

); } const categoryCounts = getCategoryCounts(); return (
{/* 헤더 */}

자재 목록

{jobNo && ( {jobNo} - {bomName} )} 총 {materials.length}개 자재 ({currentRevision})
{availableRevisions.length > 1 && (
)}
{/* 카테고리 필터 */}
{/* SPECIAL 카테고리 우선 표시 */} {Object.entries(categoryCounts).filter(([category]) => category !== 'SPECIAL').map(([category, count]) => ( ))}
{/* 액션 바 */}
{selectedMaterials.size}개 중 {filteredMaterials.length}개 선택 {Object.keys(columnFilters).length > 0 && ( (필터 {Object.keys(columnFilters).length}개 적용됨) )}
{(Object.keys(columnFilters).length > 0 || sortConfig.key) && ( )}
{/* 자재 테이블 */}
{/* SPECIAL 전용 헤더 */} {selectedCategory === 'SPECIAL' ? (
선택
종류 타입 크기 스케줄 재질
도면번호
추가요구
사용자요구
수량
) : selectedCategory === 'FLANGE' ? (
선택
종류 타입 크기
압력(파운드)
스케줄 재질
추가요구
사용자요구
수량
) : selectedCategory === 'FITTING' ? (
선택
종류 타입/상세 크기
압력
스케줄 재질
추가요구
사용자요구
수량
) : selectedCategory === 'GASKET' ? (
선택
종류
타입
크기
압력
재질
상세내역
두께
추가요구
사용자요구
수량
) : selectedCategory === 'VALVE' ? (
선택
타입
연결방식
크기
압력
재질
추가요구
사용자요구
수량
) : selectedCategory === 'BOLT' ? (
선택
종류 타입 크기 길이 재질
추가요구
사용자요구
수량
) : selectedCategory === 'U_BOLT' ? (
선택
종류 타입 크기
재질
추가요구
사용자요구
수량
) : selectedCategory === 'SUPPORT' ? (
선택
종류 타입 크기
스케줄
재질
추가요구
사용자요구
수량
) : selectedCategory === 'UNKNOWN' ? (
선택
종류
설명
사용자요구
수량
) : (
선택
종류 타입 크기 스케줄 재질
추가요구
사용자요구
수량
)} {filteredMaterials.map((material) => { const info = parseMaterialInfo(material); if (material.classified_category === 'SPECIAL') { // SPECIAL 카테고리 (10개 컬럼) return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 종류 */}
SPECIAL
{/* 타입 */}
{info.subtype || material.description}
{/* 크기 */}
{info.size || material.main_nom}
{/* 스케줄 */}
{info.schedule}
{/* 재질 */}
{info.grade || material.full_material_grade}
{/* 도면번호 */}
{material.dat_file || 'N/A'}
{/* 추가요구 */}
{material.additional_requirements || '-'}
{/* 사용자요구 */}
handleUserRequirementChange(material.id, e.target.value)} />
{/* 수량 */}
{info.quantity || material.quantity || 1}
); } if (material.classified_category === 'PIPE') { // PIPE 또는 카테고리 없는 경우 (기본 9개 컬럼) return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 종류 */}
{info.type || 'N/A'}
{/* 타입 */}
{info.subtype}
{/* 크기 */}
{info.size}
{/* 스케줄 */}
{info.schedule}
{/* 재질 */}
{info.grade}
{/* 추가요구 */}
{info.additionalReq || '-'}
{/* 사용자요구 */}
handleUserRequirementChange(material.id, e.target.value)} />
{/* 수량 */}
{info.quantity} {info.unit}
); } if (material.classified_category === 'FITTING') { return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 종류 */}
{info.type}
{/* 타입/상세 */}
{info.subtype}
{/* 크기 */}
{info.size}
{/* 압력 */}
{info.pressure}
{/* 스케줄 */}
{info.schedule}
{/* 재질 */}
{info.grade}
{/* 추가요구 */}
{info.additionalReq || '-'}
{/* 사용자요구 */}
{ console.log('🎯 입력 이벤트 발생!', material.id, e.target.value); handleUserRequirementChange(material.id, e.target.value); }} />
{/* 수량 */}
{info.quantity} {info.unit}
); } if (material.classified_category === 'VALVE') { return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 타입 */}
{info.valveType}
{/* 연결방식 */}
{info.connectionType}
{/* 크기 */}
{info.size}
{/* 압력 */}
{info.pressure}
{/* 재질 */}
{info.grade}
{/* 추가요구 */}
{info.additionalReq || '-'}
{/* 사용자요구 */}
{ console.log('🎯 입력 이벤트 발생!', material.id, e.target.value); handleUserRequirementChange(material.id, e.target.value); }} />
{/* 수량 */}
{info.quantity} {info.unit}
); } if (material.classified_category === 'FLANGE') { return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 종류 */}
{info.type}
{/* 타입 */}
{info.subtype}
{/* 크기 */}
{info.size}
{/* 압력(파운드) */}
{info.pressure}
{/* 스케줄 */}
{info.schedule}
{/* 재질 */}
{info.grade}
{/* 추가요구 */}
{info.additionalReq || '-'}
{/* 사용자요구 */}
{ console.log('🎯 입력 이벤트 발생!', material.id, e.target.value); handleUserRequirementChange(material.id, e.target.value); }} />
{/* 수량 */}
{info.quantity} {info.unit}
); } if (material.classified_category === 'UNKNOWN') { return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 종류 */}
{info.type}
{/* 설명 */}
{info.description}
{/* 사용자요구 */}
{ console.log('🎯 입력 이벤트 발생!', material.id, e.target.value); handleUserRequirementChange(material.id, e.target.value); }} />
{/* 수량 */}
{info.quantity} {info.unit}
); } if (material.classified_category === 'GASKET') { return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 종류 */}
{info.type}
{/* 타입 */}
{info.subtype}
{/* 크기 */}
{info.size}
{/* 압력 */}
{info.pressure}
{/* 재질 */}
{info.materialStructure}
{/* 상세내역 */}
{info.materialDetail}
{/* 두께 */}
{info.thickness}
{/* 추가요구 */}
{info.additionalReq || '-'}
{/* 사용자요구 */}
{ console.log('🎯 입력 이벤트 발생!', material.id, e.target.value); handleUserRequirementChange(material.id, e.target.value); }} />
{/* 수량 */}
{info.quantity} {info.unit}
); } if (material.classified_category === 'BOLT') { // BOLT 카테고리 (9개 컬럼, 길이 표시) return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 종류 */}
BOLT
{/* 타입 */}
{info.subtype || 'BOLT_GENERAL'}
{/* 크기 */}
{info.size || material.main_nom}
{/* 길이 (스케줄 대신) */}
{info.schedule || '-'}
{/* 재질 */}
{info.grade || material.full_material_grade}
{/* 추가요구 */}
{info.additionalReq || '-'}
{/* 사용자요구 */}
handleUserRequirementChange(material.id, e.target.value)} />
{/* 수량 */}
{info.quantity || material.quantity || 1} {info.unit || 'SETS'}
); } if (material.classified_category === 'U_BOLT') { // U_BOLT 카테고리 - 자재 타입별 다른 표시 const isUrethaneBlock = material.original_description?.includes('URETHANE') || material.original_description?.includes('BLOCK SHOE') || material.original_description?.includes('우레탄'); return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 종류 */}
{isUrethaneBlock ? 'URETHANE' : 'U-BOLT'}
{/* 타입 */}
{isUrethaneBlock ? 'BLOCK SHOE' : (info.subtype || 'U_BOLT')}
{/* 크기 */}
{isUrethaneBlock ? (material.original_description?.match(/\d+t/i)?.[0] || material.main_nom) : (info.size || material.main_nom) }
{/* 재질 */}
{isUrethaneBlock ? 'URETHANE' : (info.grade || material.full_material_grade || '재질 확인 필요') }
{/* 추가요구 */}
{info.additionalReq || '-'}
{/* 사용자요구 */}
handleUserRequirementChange(material.id, e.target.value)} />
{/* 수량 */}
{info.quantity || material.quantity || 1} {info.unit || 'SETS'}
); } if (material.classified_category === 'SUPPORT') { // SUPPORT 카테고리 (9개 컬럼) return (
{/* 선택 */}
toggleMaterialSelection(material.id)} />
{/* 종류 */}
SUPPORT
{/* 타입 */}
{info.subtype || material.original_description}
{/* 크기 */}
{info.size || material.main_nom}
{/* 스케줄 */}
{info.schedule || '-'}
{/* 재질 */}
{info.grade || material.full_material_grade || '-'}
{/* 추가요구 */}
{info.additionalReq || '-'}
{/* 사용자요구 */}
handleUserRequirementChange(material.id, e.target.value)} />
{/* 수량 */}
{info.quantity || material.quantity || 1} {info.unit || 'EA'}
); } // 위에서 처리되지 않은 모든 자재는 기본 9개 컬럼으로 렌더링 // (예: 아직 전용 뷰가 없는 자재) return (
{/* This is a fallback view, adjust columns as needed */}
toggleMaterialSelection(material.id)} />
{info.type || 'N/A'}
{info.subtype || material.original_description}
{info.size}
{info.schedule}
{info.grade}
-
handleUserRequirementChange(material.id, e.target.value)} />
{info.quantity} {info.unit}
); })}
); }; export default NewMaterialsPage;