feat: 서포트 카테고리 전면 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 서포트 카테고리 UI 개선: 좌우 스크롤, 헤더/본문 동기화, 가운데 정렬 - 동일 항목 합산 기능 구현 (Type + Size + Grade 기준) - 헤더 구조 변경: 압력/스케줄 제거, 구매수량 단일화, User Requirements 추가 - 우레탄 블럭슈 두께 정보(40t, 27t) Material Grade에 포함 - 서포트 수량 계산 수정: 취합된 숫자 그대로 표시 (4의 배수 계산 제거) - 서포트 분류 로직 개선: CLAMP, U-BOLT, URETHANE BLOCK SHOE 등 정확한 분류 - 백엔드 서포트 분류기에 User Requirements 추출 기능 추가 - 엑셀 내보내기에 서포트 카테고리 처리 로직 추가
This commit is contained in:
@@ -457,60 +457,57 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
itemName = gasketTypeMap[gasketType];
|
||||
} else if (extractedType && gasketTypeMap[extractedType]) {
|
||||
itemName = gasketTypeMap[extractedType];
|
||||
} else {
|
||||
itemName = 'GASKET';
|
||||
}
|
||||
} else {
|
||||
itemName = 'GASKET';
|
||||
}
|
||||
} else if (category === 'BOLT') {
|
||||
// 볼트 상세 타입 표시
|
||||
const boltDetails = material.bolt_details || {};
|
||||
const boltType = boltDetails.bolt_type || '';
|
||||
|
||||
if (boltType === 'HEX_BOLT') {
|
||||
itemName = '육각 볼트';
|
||||
} else if (boltType === 'STUD_BOLT') {
|
||||
itemName = '스터드 볼트';
|
||||
} else if (boltType === 'U_BOLT') {
|
||||
itemName = '유볼트';
|
||||
} else if (boltType === 'FLANGE_BOLT') {
|
||||
itemName = '플랜지 볼트';
|
||||
} else if (boltType === 'PSV_BOLT') {
|
||||
itemName = 'PSV 볼트';
|
||||
} else if (boltType === 'LT_BOLT') {
|
||||
itemName = '저온용 볼트';
|
||||
} else if (boltType === 'CK_BOLT') {
|
||||
itemName = '체크밸브용 볼트';
|
||||
// BOM 페이지의 타입을 따라가도록 - 프론트엔드 parseBoltInfo와 동일한 로직
|
||||
let boltSubtype = 'BOLT_GENERAL';
|
||||
|
||||
if (boltType && boltType !== 'UNKNOWN') {
|
||||
boltSubtype = boltType;
|
||||
} else {
|
||||
// description에서 추출
|
||||
// 원본 설명에서 특수 볼트 타입 추출 (프론트엔드와 동일)
|
||||
const desc = cleanDescription.toUpperCase();
|
||||
if (desc.includes('PSV')) {
|
||||
itemName = 'PSV 볼트';
|
||||
boltSubtype = 'PSV_BOLT';
|
||||
} else if (desc.includes('LT')) {
|
||||
itemName = '저온용 볼트';
|
||||
boltSubtype = 'LT_BOLT';
|
||||
} else if (desc.includes('CK')) {
|
||||
itemName = '체크밸브용 볼트';
|
||||
} else if (desc.includes('STUD')) {
|
||||
itemName = '스터드 볼트';
|
||||
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
|
||||
itemName = '유볼트';
|
||||
} else {
|
||||
itemName = '볼트';
|
||||
boltSubtype = 'CK_BOLT';
|
||||
}
|
||||
}
|
||||
|
||||
// BOM 페이지와 동일한 타입명 사용
|
||||
itemName = boltSubtype;
|
||||
} else if (category === 'SUPPORT' || category === 'U_BOLT') {
|
||||
// 서포트 상세 타입 표시
|
||||
const desc = cleanDescription.toUpperCase();
|
||||
if (desc.includes('URETHANE') || desc.includes('BLOCK SHOE')) {
|
||||
itemName = '우레탄 블록 슈';
|
||||
itemName = 'URETHANE BLOCK SHOE';
|
||||
// 우레탄 블럭슈의 경우 두께 정보 추가
|
||||
const thicknessMatch = desc.match(/(\d+)\s*[tT](?![oO])/);
|
||||
if (thicknessMatch) {
|
||||
itemName += ` ${thicknessMatch[1]}t`;
|
||||
}
|
||||
} else if (desc.includes('CLAMP')) {
|
||||
itemName = '클램프';
|
||||
itemName = 'CLAMP';
|
||||
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
|
||||
itemName = '유볼트';
|
||||
itemName = 'U-BOLT';
|
||||
} else if (desc.includes('HANGER')) {
|
||||
itemName = '행거';
|
||||
itemName = 'HANGER';
|
||||
} else if (desc.includes('SPRING')) {
|
||||
itemName = '스프링 서포트';
|
||||
itemName = 'SPRING HANGER';
|
||||
} else if (desc.includes('GUIDE')) {
|
||||
itemName = 'GUIDE';
|
||||
} else if (desc.includes('ANCHOR')) {
|
||||
itemName = 'ANCHOR';
|
||||
} else {
|
||||
itemName = '서포트';
|
||||
itemName = 'SUPPORT';
|
||||
}
|
||||
} else {
|
||||
itemName = category || 'UNKNOWN';
|
||||
@@ -542,12 +539,22 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
let schedule = '-';
|
||||
|
||||
if (category === 'BOLT') {
|
||||
// 볼트의 경우 길이 정보 추출
|
||||
// 볼트의 경우 길이 정보 추출 (백엔드와 동일한 패턴 사용)
|
||||
const lengthPatterns = [
|
||||
/(\d+(?:\.\d+)?)\s*LG/i, // 145.0000 LG, 75 LG
|
||||
/(\d+(?:\.\d+)?)\s*LG/i, // 145.0000 LG, 75 LG (최우선)
|
||||
/(\d+(?:\.\d+)?)\s*MM\s*LG/i, // 70MM LG 형태
|
||||
/L\s*(\d+(?:\.\d+)?)\s*MM/i,
|
||||
/LENGTH\s*(\d+(?:\.\d+)?)\s*MM/i,
|
||||
/(\d+(?:\.\d+)?)\s*MM\s*LONG/i,
|
||||
/X\s*(\d+(?:\.\d+)?)\s*MM/i, // M8 X 20MM 형태
|
||||
/,\s*(\d+(?:\.\d+)?)\s*LG/i, // ", 145.0000 LG" 형태 (PSV, LT 볼트용)
|
||||
/,\s*(\d+(?:\.\d+)?)\s+(?:CK|PSV|LT)/i, // ", 140 CK" 형태 (PSV 볼트용)
|
||||
/PSV\s+(\d+(?:\.\d+)?)/i, // PSV 140 형태 (PSV 볼트 전용)
|
||||
/(\d+(?:\.\d+)?)\s+PSV/i, // 140 PSV 형태 (PSV 볼트 전용)
|
||||
/(\d+(?:\.\d+)?)\s*CK/i, // 140CK 형태 (체크밸브용 볼트)
|
||||
/(\d+(?:\.\d+)?)\s*LT/i, // 140LT 형태 (저온용 볼트)
|
||||
/(\d+(?:\.\d+)?)\s*mm/i, // 50mm
|
||||
/(\d+(?:\.\d+)?)\s*MM/i, // 50MM
|
||||
/,\s*(\d+(?:\.\d+)?)\s*LG/i, // ", 145.0000 LG" 형태
|
||||
/LG[,\s]*(\d+(?:\.\d+)?)/i // LG, 75 형태
|
||||
];
|
||||
|
||||
@@ -678,20 +685,20 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
gasketMaterial = `${fourMaterialMatch[1]}/${fourMaterialMatch[2]}/${fourMaterialMatch[3]}/${fourMaterialMatch[4]}`;
|
||||
} else {
|
||||
// DB에서 가져온 정보로 구성 (fallback)
|
||||
if (material.gasket_details) {
|
||||
const materialType = material.gasket_details.material_type || '';
|
||||
const fillerMaterial = material.gasket_details.filler_material || '';
|
||||
|
||||
if (materialType && fillerMaterial) {
|
||||
gasketMaterial = `${materialType}/${fillerMaterial}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (material.gasket_details) {
|
||||
const materialType = material.gasket_details.material_type || '';
|
||||
const fillerMaterial = material.gasket_details.filler_material || '';
|
||||
|
||||
if (materialType && fillerMaterial) {
|
||||
gasketMaterial = `${materialType}/${fillerMaterial}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막으로 간단한 패턴
|
||||
if (!gasketMaterial) {
|
||||
const simpleMaterialMatch = cleanDescription.match(/(SS\d+|304|316)\s*[\/+]\s*(GRAPHITE|PTFE|VITON|EPDM)/i);
|
||||
if (simpleMaterialMatch) {
|
||||
gasketMaterial = `${simpleMaterialMatch[1]}/${simpleMaterialMatch[2]}`;
|
||||
if (!gasketMaterial) {
|
||||
const simpleMaterialMatch = cleanDescription.match(/(SS\d+|304|316)\s*[\/+]\s*(GRAPHITE|PTFE|VITON|EPDM)/i);
|
||||
if (simpleMaterialMatch) {
|
||||
gasketMaterial = `${simpleMaterialMatch[1]}/${simpleMaterialMatch[2]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,6 +737,29 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
} else if (material.pipe_count) {
|
||||
quantity = material.pipe_count;
|
||||
}
|
||||
} else if (category === 'BOLT') {
|
||||
// BOLT의 경우 플랜지당 볼트 세트 수를 고려한 수량 계산 (BOM 페이지와 동일)
|
||||
const qty = Math.round(material.quantity || 0);
|
||||
|
||||
// 플랜지당 볼트 세트 수 추출 (예: (8), (4))
|
||||
const boltDetails = material.bolt_details || {};
|
||||
let boltsPerFlange = boltDetails.dimensions?.bolts_per_flange || 1;
|
||||
|
||||
// 백엔드에서 정보가 없으면 원본 설명에서 직접 추출
|
||||
if (boltsPerFlange === 1) {
|
||||
const description = material.original_description || '';
|
||||
const flangePattern = description.match(/\((\d+)\)/);
|
||||
if (flangePattern) {
|
||||
boltsPerFlange = parseInt(flangePattern[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// 실제 필요한 볼트 수 = 플랜지 수 × 플랜지당 볼트 세트 수
|
||||
const totalBoltsNeeded = qty * boltsPerFlange;
|
||||
const safetyQty = Math.ceil(totalBoltsNeeded * 1.05); // 5% 여유율
|
||||
const purchaseQty = Math.ceil(safetyQty / 4) * 4; // 4의 배수
|
||||
|
||||
quantity = purchaseQty;
|
||||
}
|
||||
|
||||
// 새로운 엑셀 양식: A~E 고정, F~O 카테고리별, P 납기일
|
||||
@@ -840,14 +870,26 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
base['관리항목1'] = ''; // M열
|
||||
base['관리항목2'] = ''; // N열
|
||||
base['관리항목3'] = ''; // O열
|
||||
} else if (category === 'BOLT') {
|
||||
// 볼트 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
||||
} else if (category === 'BOLT') {
|
||||
// 볼트 전용 컬럼 (F~O) - User Requirements와 Additional Request 분리
|
||||
base['크기'] = material.size_spec || '-'; // F열
|
||||
base['압력등급'] = pressure; // G열
|
||||
base['길이'] = schedule; // H열: 볼트는 길이 정보
|
||||
base['재질'] = grade; // I열
|
||||
base['추가요구'] = detailInfo || '-'; // J열: 표면처리 등
|
||||
base['사용자요구'] = material.user_requirement || ''; // K열
|
||||
base['사용자요구'] = detailInfo || '-'; // J열: ELEC.GALV 등 (분류기 추출)
|
||||
base['추가요청사항'] = material.user_requirement || ''; // K열: 사용자 입력
|
||||
base['관리항목1'] = ''; // L열
|
||||
base['관리항목2'] = ''; // M열
|
||||
base['관리항목3'] = ''; // N열
|
||||
base['관리항목4'] = ''; // O열
|
||||
} else if (category === 'SUPPORT') {
|
||||
// 서포트 전용 컬럼 (F~O)
|
||||
base['크기'] = material.size_spec || material.main_nom || '-'; // F열
|
||||
base['압력등급'] = pressure; // G열
|
||||
base['재질'] = grade; // H열
|
||||
base['상세내역'] = material.original_description || '-'; // I열
|
||||
base['사용자요구'] = material.user_requirements?.join(', ') || ''; // J열 (분류기에서 추출)
|
||||
base['추가요청사항'] = material.user_requirement || ''; // K열 (사용자 입력)
|
||||
base['관리항목1'] = ''; // L열
|
||||
base['관리항목2'] = ''; // M열
|
||||
base['관리항목3'] = ''; // N열
|
||||
@@ -1113,7 +1155,7 @@ export const createExcelBlob = async (materials, filename, options = {}) => {
|
||||
'FLANGE': ['크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||
'VALVE': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||
'GASKET': ['크기', '압력등급', '구조', '재질1', '재질2', '두께', '사용자요구', '관리항목1', '관리항목2', '관리항목3'],
|
||||
'BOLT': ['크기', '압력등급', '길이', '재질', '추가요구', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||
'BOLT': ['크기', '압력등급', '길이', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||
'SUPPORT': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||
};
|
||||
|
||||
|
||||
@@ -126,6 +126,15 @@ export const calculatePurchaseQuantity = (material) => {
|
||||
unit: 'EA'
|
||||
};
|
||||
|
||||
case 'SUPPORT':
|
||||
// 서포트는 취합된 숫자 그대로
|
||||
return {
|
||||
purchaseQuantity: bomQuantity,
|
||||
calculation: `${bomQuantity} EA (취합된 수량 그대로)`,
|
||||
category: 'SUPPORT',
|
||||
unit: 'EA'
|
||||
};
|
||||
|
||||
case 'FITTING':
|
||||
case 'INSTRUMENT':
|
||||
case 'VALVE':
|
||||
|
||||
Reference in New Issue
Block a user