Files
TK-BOM-Project/frontend/src/pages/NewMaterialsPage.jsx
Hyungi Ahn f09e494bd4
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
🗑️ 전체(ALL) 카테고리 제거 - 헤더/본문 정렬 문제 해결
- 전체 카테고리 버튼 제거
- 기본 선택 카테고리를 PIPE로 변경
- 문제 원인: 전체 카테고리에서 서로 다른 컬럼 수를 가진 자재들이 섞여서 표시됨
  (PIPE 9개, FLANGE 10개, GASKET 11개 등)
- 이제 각 카테고리별로만 표시되어 헤더와 본문이 완벽히 정렬됨
- quantity-info wrapper 제거로 셀 구조 단순화
2025-10-13 15:30:27 +09:00

2091 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div className={`filterable-header ${className}`}>
<span className="header-text">{children}</span>
<div className="header-controls">
{/* 정렬 버튼 */}
<button
className={`sort-btn ${sortConfig.key === sortKey ? 'active' : ''}`}
onClick={() => handleSort(sortKey)}
title="정렬"
>
{sortConfig.key === sortKey ? (
sortConfig.direction === 'asc' ? '↑' : '↓'
) : '↕'}
</button>
{/* 필터 버튼 */}
<button
className={`filter-btn ${columnFilters[filterKey] ? 'active' : ''}`}
onClick={() => setShowFilterDropdown(showFilterDropdown === filterKey ? null : filterKey)}
title="필터"
>
</button>
</div>
{/* 필터 드롭다운 */}
{showFilterDropdown === filterKey && (
<div className="filter-dropdown">
<div className="filter-search">
<input
type="text"
placeholder="검색..."
value={columnFilters[filterKey] || ''}
onChange={(e) => handleFilter(filterKey, e.target.value)}
autoFocus
/>
{columnFilters[filterKey] && (
<button
className="clear-filter-btn"
onClick={() => clearFilter(filterKey)}
title="필터 초기화"
>
</button>
)}
</div>
<div className="filter-options">
<div className="filter-option-header"> 목록:</div>
{uniqueValues.slice(0, 20).map(value => (
<div
key={value}
className="filter-option"
onClick={() => {
handleFilter(filterKey, value);
setShowFilterDropdown(null);
}}
>
{value}
</div>
))}
{uniqueValues.length > 20 && (
<div className="filter-option-more">
+{uniqueValues.length - 20} ...
</div>
)}
</div>
</div>
)}
</div>
);
};
// 엑셀 내보내기 - 개선된 버전 사용
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 (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>자재 목록을 불러오는 ...</p>
</div>
);
}
const categoryCounts = getCategoryCounts();
return (
<div className="materials-page">
{/* 헤더 */}
<div className="materials-header">
<div className="header-left">
<button
onClick={() => {
// 프로젝트 정보를 포함하여 BOM 페이지로 돌아가기
const projectInfo = selectedProject || {
job_no: jobNo,
official_project_code: jobNo,
job_name: bomName || filename
};
onNavigate('bom', {
selectedProject: projectInfo
});
}}
className="back-button-simple"
title="BOM 업로드로 돌아가기"
>
</button>
<div className="header-info">
<h1>자재 목록</h1>
{jobNo && (
<span className="job-info">
{jobNo} - {bomName}
</span>
)}
<span className="material-count-inline">
{materials.length} 자재 ({currentRevision})
</span>
</div>
</div>
<div className="header-center">
{availableRevisions.length > 1 && (
<div className="revision-selector">
<label>리비전: </label>
<select
value={currentRevision}
onChange={(e) => {
const selectedRev = e.target.value;
const selectedFile = availableRevisions.find(f => f.revision === selectedRev);
if (selectedFile) {
setCurrentRevision(selectedRev);
loadMaterials(selectedFile.id);
}
}}
className="revision-dropdown"
>
{availableRevisions.map(file => (
<option key={file.id} value={file.revision}>
{file.revision || 'Rev.0'} ({file.parsed_count || 0} 자재)
</option>
))}
</select>
</div>
)}
</div>
<div className="header-right">
<button
onClick={() => {
const currentUrl = window.location.href;
const pageInfo = `페이지: NewMaterialsPage\nURL: ${currentUrl}\nJob: ${jobNo}\nRevision: ${currentRevision}`;
alert(pageInfo);
}}
style={{
padding: '4px 8px',
background: '#667eea',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '11px',
cursor: 'pointer'
}}
>
🔗 URL
</button>
</div>
</div>
{/* 카테고리 필터 */}
<div className="category-filters">
{/* SPECIAL 카테고리 우선 표시 */}
<button
key="SPECIAL"
className={`category-btn ${selectedCategory === 'SPECIAL' ? 'active' : ''}`}
onClick={() => setSelectedCategory('SPECIAL')}
>
SPECIAL <span className="count">{categoryCounts.SPECIAL || 0}</span>
</button>
{Object.entries(categoryCounts).filter(([category]) => category !== 'SPECIAL').map(([category, count]) => (
<button
key={category}
className={`category-btn ${selectedCategory === category ? 'active' : ''}`}
onClick={() => setSelectedCategory(category)}
>
{getCategoryDisplayName(category)} <span className="count">{count}</span>
</button>
))}
</div>
{/* 액션 바 */}
<div className="action-bar">
<div className="selection-info">
{selectedMaterials.size} {filteredMaterials.length} 선택
{Object.keys(columnFilters).length > 0 && (
<span className="filter-info">
(필터 {Object.keys(columnFilters).length} 적용됨)
</span>
)}
</div>
<div className="action-buttons">
{(Object.keys(columnFilters).length > 0 || sortConfig.key) && (
<button
onClick={clearAllFilters}
className="clear-filters-btn"
title="모든 필터 및 정렬 초기화"
>
필터 초기화
</button>
)}
<button
onClick={toggleAllSelection}
className="select-all-btn"
>
{selectedMaterials.size === filteredMaterials.length ? '전체 해제' : '전체 선택'}
</button>
<button
onClick={saveUserRequirements}
className="save-requirements-btn"
disabled={savingRequirements}
style={{
backgroundColor: savingRequirements ? '#ccc' : '#10b981',
color: 'white',
border: 'none',
padding: '10px 20px',
borderRadius: '6px',
cursor: savingRequirements ? 'not-allowed' : 'pointer',
marginRight: '10px',
fontSize: '14px',
fontWeight: '500'
}}
>
{savingRequirements ? '저장 중...' : '사용자 요구사항 저장'}
</button>
<button
onClick={exportToExcel}
className="export-btn"
>
{selectedMaterials.size > 0
? `선택 항목 엑셀 내보내기 (${selectedMaterials.size})`
: '전체 엑셀 내보내기'}
</button>
</div>
</div>
{/* 자재 테이블 */}
<div className="materials-grid">
{/* SPECIAL 전용 헤더 */}
{selectedCategory === 'SPECIAL' ? (
<div className="detailed-grid-header special-header">
<div>선택</div>
<FilterableHeader sortKey="type" filterKey="type">종류</FilterableHeader>
<FilterableHeader sortKey="subtype" filterKey="subtype">타입</FilterableHeader>
<FilterableHeader sortKey="size" filterKey="size">크기</FilterableHeader>
<FilterableHeader sortKey="schedule" filterKey="schedule">스케줄</FilterableHeader>
<FilterableHeader sortKey="grade" filterKey="grade">재질</FilterableHeader>
<div>도면번호</div>
<div>추가요구</div>
<div>사용자요구</div>
<FilterableHeader sortKey="quantity" filterKey="quantity">수량</FilterableHeader>
</div>
) : selectedCategory === 'FLANGE' ? (
<div className="detailed-grid-header flange-header">
<div>선택</div>
<FilterableHeader sortKey="type" filterKey="type">종류</FilterableHeader>
<FilterableHeader sortKey="subtype" filterKey="subtype">타입</FilterableHeader>
<FilterableHeader sortKey="size" filterKey="size">크기</FilterableHeader>
<div>압력(파운드)</div>
<FilterableHeader sortKey="schedule" filterKey="schedule">스케줄</FilterableHeader>
<FilterableHeader sortKey="grade" filterKey="grade">재질</FilterableHeader>
<div>추가요구</div>
<div>사용자요구</div>
<FilterableHeader sortKey="quantity" filterKey="quantity">수량</FilterableHeader>
</div>
) : selectedCategory === 'FITTING' ? (
<div className="detailed-grid-header fitting-header">
<div>선택</div>
<FilterableHeader sortKey="type" filterKey="type">종류</FilterableHeader>
<FilterableHeader sortKey="subtype" filterKey="subtype">타입/상세</FilterableHeader>
<FilterableHeader sortKey="size" filterKey="size">크기</FilterableHeader>
<div>압력</div>
<FilterableHeader sortKey="schedule" filterKey="schedule">스케줄</FilterableHeader>
<FilterableHeader sortKey="grade" filterKey="grade">재질</FilterableHeader>
<div>추가요구</div>
<div>사용자요구</div>
<FilterableHeader sortKey="quantity" filterKey="quantity">수량</FilterableHeader>
</div>
) : selectedCategory === 'GASKET' ? (
<div className="detailed-grid-header gasket-header">
<div>선택</div>
<div>종류</div>
<div>타입</div>
<div>크기</div>
<div>압력</div>
<div>재질</div>
<div>상세내역</div>
<div>두께</div>
<div>추가요구</div>
<div>사용자요구</div>
<div>수량</div>
</div>
) : selectedCategory === 'VALVE' ? (
<div className="detailed-grid-header valve-header">
<div>선택</div>
<div>타입</div>
<div>연결방식</div>
<div>크기</div>
<div>압력</div>
<div>재질</div>
<div>추가요구</div>
<div>사용자요구</div>
<div>수량</div>
</div>
) : selectedCategory === 'BOLT' ? (
<div className="detailed-grid-header bolt-header">
<div>선택</div>
<FilterableHeader sortKey="type" filterKey="type">종류</FilterableHeader>
<FilterableHeader sortKey="subtype" filterKey="subtype">타입</FilterableHeader>
<FilterableHeader sortKey="size" filterKey="size">크기</FilterableHeader>
<FilterableHeader sortKey="schedule" filterKey="schedule">길이</FilterableHeader>
<FilterableHeader sortKey="grade" filterKey="grade">재질</FilterableHeader>
<div>추가요구</div>
<div>사용자요구</div>
<FilterableHeader sortKey="quantity" filterKey="quantity">수량</FilterableHeader>
</div>
) : selectedCategory === 'U_BOLT' ? (
<div className="detailed-grid-header ubolt-header">
<div>선택</div>
<FilterableHeader sortKey="type" filterKey="type">종류</FilterableHeader>
<FilterableHeader sortKey="subtype" filterKey="subtype">타입</FilterableHeader>
<FilterableHeader sortKey="size" filterKey="size">크기</FilterableHeader>
<div>재질</div>
<div>추가요구</div>
<div>사용자요구</div>
<FilterableHeader sortKey="quantity" filterKey="quantity">수량</FilterableHeader>
</div>
) : selectedCategory === 'SUPPORT' ? (
<div className="detailed-grid-header support-header">
<div>선택</div>
<FilterableHeader sortKey="type" filterKey="type">종류</FilterableHeader>
<FilterableHeader sortKey="subtype" filterKey="subtype">타입</FilterableHeader>
<FilterableHeader sortKey="size" filterKey="size">크기</FilterableHeader>
<div>스케줄</div>
<div>재질</div>
<div>추가요구</div>
<div>사용자요구</div>
<FilterableHeader sortKey="quantity" filterKey="quantity">수량</FilterableHeader>
</div>
) : selectedCategory === 'UNKNOWN' ? (
<div className="detailed-grid-header unknown-header">
<div>선택</div>
<div>종류</div>
<div>설명</div>
<div>사용자요구</div>
<div>수량</div>
</div>
) : (
<div className="detailed-grid-header pipe-header">
<div>선택</div>
<FilterableHeader sortKey="type" filterKey="type">종류</FilterableHeader>
<FilterableHeader sortKey="subtype" filterKey="subtype">타입</FilterableHeader>
<FilterableHeader sortKey="size" filterKey="size">크기</FilterableHeader>
<FilterableHeader sortKey="schedule" filterKey="schedule">스케줄</FilterableHeader>
<FilterableHeader sortKey="grade" filterKey="grade">재질</FilterableHeader>
<div>추가요구</div>
<div>사용자요구</div>
<FilterableHeader sortKey="quantity" filterKey="quantity">수량</FilterableHeader>
</div>
)}
{filteredMaterials.map((material) => {
const info = parseMaterialInfo(material);
if (material.classified_category === 'SPECIAL') {
// SPECIAL 카테고리 (10개 컬럼)
return (
<div
key={material.id}
className={`detailed-material-row special-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 종류 */}
<div className="material-cell">
<span className={`type-badge special`}>
SPECIAL
</span>
</div>
{/* 타입 */}
<div className="material-cell">
<span className="subtype-text">{info.subtype || material.description}</span>
</div>
{/* 크기 */}
<div className="material-cell">
<span className="size-text">{info.size || material.main_nom}</span>
</div>
{/* 스케줄 */}
<div className="material-cell">
<span className="schedule-text">{info.schedule}</span>
</div>
{/* 재질 */}
<div className="material-cell">
<span className="grade-text">{info.grade || material.full_material_grade}</span>
</div>
{/* 도면번호 */}
<div className="material-cell">
<span className="drawing-text">{material.dat_file || 'N/A'}</span>
</div>
{/* 추가요구 */}
<div className="material-cell">
<span className="additional-req-text">{material.additional_requirements || '-'}</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<span className="quantity-text">{info.quantity || material.quantity || 1}</span>
</div>
</div>
);
}
if (material.classified_category === 'PIPE') {
// PIPE 또는 카테고리 없는 경우 (기본 9개 컬럼)
return (
<div
key={material.id}
className={`detailed-material-row pipe-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 종류 */}
<div className="material-cell">
<span className={`type-badge ${info.type ? info.type.toLowerCase() : 'unknown'}`}>
{info.type || 'N/A'}
</span>
</div>
{/* 타입 */}
<div className="material-cell">
<span className="subtype-text">{info.subtype}</span>
</div>
{/* 크기 */}
<div className="material-cell">
<span className="size-text">{info.size}</span>
</div>
{/* 스케줄 */}
<div className="material-cell">
<span>{info.schedule}</span>
</div>
{/* 재질 */}
<div className="material-cell">
<span className="material-grade">{info.grade}</span>
</div>
{/* 추가요구 */}
<div className="material-cell">
<span>{info.additionalReq || '-'}</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<div className="quantity-info">
<span className="quantity-value">
{info.quantity} {info.unit}
</span>
{info.type === 'PIPE' && info.details && (
<div className="quantity-details">
<small>
단관 {info.details.pipeCount} {Math.round(info.details.totalLength)}mm
</small>
</div>
)}
</div>
</div>
</div>
);
}
if (material.classified_category === 'FITTING') {
return (
<div
key={material.id}
className={`detailed-material-row fitting-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 종류 */}
<div className="material-cell">
<span className={`type-badge ${info.type.toLowerCase()}`}>
{info.type}
</span>
</div>
{/* 타입/상세 */}
<div className="material-cell">
<span className="subtype-text">{info.subtype}</span>
</div>
{/* 크기 */}
<div className="material-cell">
<span className="size-text">{info.size}</span>
</div>
{/* 압력 */}
<div className="material-cell">
<span className="pressure-info">{info.pressure}</span>
</div>
{/* 스케줄 */}
<div className="material-cell">
<span>{info.schedule}</span>
</div>
{/* 재질 */}
<div className="material-cell">
<span className="material-grade">{info.grade}</span>
</div>
{/* 추가요구 */}
<div className="material-cell">
<span>{info.additionalReq || '-'}</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => {
console.log('🎯 입력 이벤트 발생!', material.id, e.target.value);
handleUserRequirementChange(material.id, e.target.value);
}}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<span className="quantity-value">
{info.quantity} {info.unit}
</span>
</div>
</div>
);
}
if (material.classified_category === 'VALVE') {
return (
<div
key={material.id}
className={`detailed-material-row valve-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 타입 */}
<div className="material-cell">
<span className="valve-type">{info.valveType}</span>
</div>
{/* 연결방식 */}
<div className="material-cell">
<span className="connection-type">{info.connectionType}</span>
</div>
{/* 크기 */}
<div className="material-cell">
<span className="size-text">{info.size}</span>
</div>
{/* 압력 */}
<div className="material-cell">
<span className="pressure-info">{info.pressure}</span>
</div>
{/* 재질 */}
<div className="material-cell">
<span className="material-grade">{info.grade}</span>
</div>
{/* 추가요구 */}
<div className="material-cell">
<span>{info.additionalReq || '-'}</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => {
console.log('🎯 입력 이벤트 발생!', material.id, e.target.value);
handleUserRequirementChange(material.id, e.target.value);
}}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<span className="quantity-value">
{info.quantity} {info.unit}
</span>
</div>
</div>
);
}
if (material.classified_category === 'FLANGE') {
return (
<div
key={material.id}
className={`detailed-material-row flange-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 종류 */}
<div className="material-cell">
<span className={`type-badge ${info.type.toLowerCase()}`}>
{info.type}
</span>
</div>
{/* 타입 */}
<div className="material-cell">
<span className="subtype-text">{info.subtype}</span>
</div>
{/* 크기 */}
<div className="material-cell">
<span className="size-text">{info.size}</span>
</div>
{/* 압력(파운드) */}
<div className="material-cell">
<span className="pressure-info">{info.pressure}</span>
</div>
{/* 스케줄 */}
<div className="material-cell">
<span>{info.schedule}</span>
</div>
{/* 재질 */}
<div className="material-cell">
<span className="material-grade">{info.grade}</span>
</div>
{/* 추가요구 */}
<div className="material-cell">
<span>{info.additionalReq || '-'}</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => {
console.log('🎯 입력 이벤트 발생!', material.id, e.target.value);
handleUserRequirementChange(material.id, e.target.value);
}}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<span className="quantity-value">
{info.quantity} {info.unit}
</span>
</div>
</div>
);
}
if (material.classified_category === 'UNKNOWN') {
return (
<div
key={material.id}
className={`detailed-material-row unknown-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 종류 */}
<div className="material-cell">
<span className={`type-badge unknown`}>
{info.type}
</span>
</div>
{/* 설명 */}
<div className="material-cell description-cell">
<span className="description-text" title={info.description}>
{info.description}
</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => {
console.log('🎯 입력 이벤트 발생!', material.id, e.target.value);
handleUserRequirementChange(material.id, e.target.value);
}}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<div className="quantity-info">
<span className="quantity-value">
{info.quantity} {info.unit}
</span>
</div>
</div>
</div>
);
}
if (material.classified_category === 'GASKET') {
return (
<div
key={material.id}
className={`detailed-material-row gasket-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 종류 */}
<div className="material-cell">
<span className={`type-badge ${info.type.toLowerCase()}`}>
{info.type}
</span>
</div>
{/* 타입 */}
<div className="material-cell">
<span className="subtype-text">{info.subtype}</span>
</div>
{/* 크기 */}
<div className="material-cell">
<span className="size-text">{info.size}</span>
</div>
{/* 압력 */}
<div className="material-cell">
<span className="pressure-info">{info.pressure}</span>
</div>
{/* 재질 */}
<div className="material-cell">
<span className="material-structure">{info.materialStructure}</span>
</div>
{/* 상세내역 */}
<div className="material-cell">
<span className="material-detail">{info.materialDetail}</span>
</div>
{/* 두께 */}
<div className="material-cell">
<span className="thickness-info">{info.thickness}</span>
</div>
{/* 추가요구 */}
<div className="material-cell">
<span>{info.additionalReq || '-'}</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => {
console.log('🎯 입력 이벤트 발생!', material.id, e.target.value);
handleUserRequirementChange(material.id, e.target.value);
}}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<div className="quantity-info">
<span className="quantity-value">
{info.quantity} {info.unit}
</span>
</div>
</div>
</div>
);
}
if (material.classified_category === 'BOLT') {
// BOLT 카테고리 (9개 컬럼, 길이 표시)
return (
<div
key={material.id}
className={`detailed-material-row bolt-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 종류 */}
<div className="material-cell">
<span className={`type-badge bolt`}>
BOLT
</span>
</div>
{/* 타입 */}
<div className="material-cell">
<span className="subtype-text">{info.subtype || 'BOLT_GENERAL'}</span>
</div>
{/* 크기 */}
<div className="material-cell">
<span className="size-text">{info.size || material.main_nom}</span>
</div>
{/* 길이 (스케줄 대신) */}
<div className="material-cell">
<span className="length-text">{info.schedule || '-'}</span>
</div>
{/* 재질 */}
<div className="material-cell">
<span className="grade-text">{info.grade || material.full_material_grade}</span>
</div>
{/* 추가요구 */}
<div className="material-cell">
<span className="additional-req-text">{info.additionalReq || '-'}</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<span className="quantity-text">{info.quantity || material.quantity || 1} {info.unit || 'SETS'}</span>
</div>
</div>
);
}
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 (
<div
key={material.id}
className={`detailed-material-row ubolt-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 종류 */}
<div className="material-cell">
<span className={`type-badge ${isUrethaneBlock ? 'urethane' : 'ubolt'}`}>
{isUrethaneBlock ? 'URETHANE' : 'U-BOLT'}
</span>
</div>
{/* 타입 */}
<div className="material-cell">
<span className="subtype-text">
{isUrethaneBlock ? 'BLOCK SHOE' : (info.subtype || 'U_BOLT')}
</span>
</div>
{/* 크기 */}
<div className="material-cell">
<span className="size-text">
{isUrethaneBlock ?
(material.original_description?.match(/\d+t/i)?.[0] || material.main_nom) :
(info.size || material.main_nom)
}
</span>
</div>
{/* 재질 */}
<div className="material-cell">
<span className="grade-text">
{isUrethaneBlock ?
'URETHANE' :
(info.grade || material.full_material_grade || '재질 확인 필요')
}
</span>
</div>
{/* 추가요구 */}
<div className="material-cell">
<span className="additional-req-text">{info.additionalReq || '-'}</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<span className="quantity-text">{info.quantity || material.quantity || 1} {info.unit || 'SETS'}</span>
</div>
</div>
);
}
if (material.classified_category === 'SUPPORT') {
// SUPPORT 카테고리 (9개 컬럼)
return (
<div
key={material.id}
className={`detailed-material-row support-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* 선택 */}
<div className="material-cell">
<input
type="checkbox"
checked={selectedMaterials.has(material.id)}
onChange={() => toggleMaterialSelection(material.id)}
/>
</div>
{/* 종류 */}
<div className="material-cell">
<span className={`type-badge support`}>
SUPPORT
</span>
</div>
{/* 타입 */}
<div className="material-cell">
<span className="subtype-text">{info.subtype || material.original_description}</span>
</div>
{/* 크기 */}
<div className="material-cell">
<span className="size-text">{info.size || material.main_nom}</span>
</div>
{/* 스케줄 */}
<div className="material-cell">
<span className="schedule-text">{info.schedule || '-'}</span>
</div>
{/* 재질 */}
<div className="material-cell">
<span className="grade-text">{info.grade || material.full_material_grade || '-'}</span>
</div>
{/* 추가요구 */}
<div className="material-cell">
<span className="additional-req-text">{info.additionalReq || '-'}</span>
</div>
{/* 사용자요구 */}
<div className="material-cell">
<input
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>
{/* 수량 */}
<div className="material-cell">
<span className="quantity-text">{info.quantity || material.quantity || 1} {info.unit || 'EA'}</span>
</div>
</div>
);
}
// 위에서 처리되지 않은 모든 자재는 기본 9개 컬럼으로 렌더링
// (예: 아직 전용 뷰가 없는 자재)
return (
<div
key={material.id + '-default'}
className={`detailed-material-row pipe-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
>
{/* This is a fallback view, adjust columns as needed */}
<div className="material-cell"><input type="checkbox" checked={selectedMaterials.has(material.id)} onChange={() => toggleMaterialSelection(material.id)} /></div>
<div className="material-cell"><span className={`type-badge ${info.type ? info.type.toLowerCase() : 'unknown'}`}>{info.type || 'N/A'}</span></div>
<div className="material-cell">{info.subtype || material.original_description}</div>
<div className="material-cell">{info.size}</div>
<div className="material-cell">{info.schedule}</div>
<div className="material-cell">{info.grade}</div>
<div className="material-cell">-</div>
<div className="material-cell"><input type="text" className="user-req-input" placeholder="요구사항" value={userRequirements[material.id] || ''} onChange={(e) => handleUserRequirementChange(material.id, e.target.value)} /></div>
<div className="material-cell">{info.quantity} {info.unit}</div>
</div>
);
})}
</div>
</div>
);
};
export default NewMaterialsPage;