Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 전체 카테고리 버튼 제거 - 기본 선택 카테고리를 PIPE로 변경 - 문제 원인: 전체 카테고리에서 서로 다른 컬럼 수를 가진 자재들이 섞여서 표시됨 (PIPE 9개, FLANGE 10개, GASKET 11개 등) - 이제 각 카테고리별로만 표시되어 헤더와 본문이 완벽히 정렬됨 - quantity-info wrapper 제거로 셀 구조 단순화
2091 lines
78 KiB
JavaScript
2091 lines
78 KiB
JavaScript
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;
|
||
|