feat: 모든 카테고리에 추가요청사항 저장 기능 및 엑셀 내보내기 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- 모든 BOM 카테고리(Pipe, Fitting, Flange, Gasket, Bolt, Support)에 추가요청사항 저장/편집 기능 추가
- 저장된 데이터의 카테고리 변경 및 페이지 새로고침 시 지속성 보장
- 백엔드 materials 테이블에 brand, user_requirement 컬럼 추가
- 새로운 /materials/{id}/brand, /materials/{id}/user-requirement PATCH API 엔드포인트 추가
- 모든 카테고리에서 Additional Request 컬럼 너비 확장 (UI 겹침 방지)
- GASKET 카테고리 엑셀 내보내기에 누락된 '추가요청사항' 컬럼 추가
- 엑셀 내보내기 시 저장된 추가요청사항이 우선 반영되도록 개선
- P열 납기일 규칙 유지하며 관리항목 개수 조정
This commit is contained in:
hyungi
2025-10-17 12:54:17 +09:00
parent 6b6360ecd5
commit f336b5a4a8
12 changed files with 1667 additions and 333 deletions

View File

@@ -115,6 +115,56 @@ const consolidateMaterials = (materials, isComparison = false) => {
return Object.values(consolidated);
};
/**
* 벨브 연결방식 추출 함수
*/
const extractValveConnectionType = (description) => {
const descUpper = description.toUpperCase();
if (descUpper.includes('SW X THRD') || descUpper.includes('SW×THRD')) {
return 'SW×THRD';
} else if (descUpper.includes('FLG') || descUpper.includes('FLANGE')) {
return 'FLG';
} else if (descUpper.includes('SW') || descUpper.includes('SOCKET')) {
return 'SW';
} else if (descUpper.includes('THRD') || descUpper.includes('THREAD')) {
return 'THRD';
} else if (descUpper.includes('BW') || descUpper.includes('BUTT WELD')) {
return 'BW';
} else {
return '-';
}
};
/**
* 벨브 추가 정보 추출 함수
*/
const extractValveAdditionalInfo = (description) => {
const descUpper = description.toUpperCase();
let additionalInfo = '';
const additionalPatterns = [
'3-WAY', '3WAY', 'THREE WAY',
'DOUL PLATE', 'DOUBLE PLATE', 'DUAL PLATE',
'DOUBLE DISC', 'DUAL DISC',
'SWING', 'LIFT', 'TILTING',
'WAFER', 'LUG', 'FLANGED',
'FULL BORE', 'REDUCED BORE',
'FIRE SAFE', 'ANTI STATIC'
];
for (const pattern of additionalPatterns) {
if (descUpper.includes(pattern)) {
if (additionalInfo) {
additionalInfo += ', ';
}
additionalInfo += pattern;
}
}
return additionalInfo || '-';
};
/**
* 자재 데이터를 엑셀용 형태로 변환
*/
@@ -401,24 +451,28 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
} else if (valveType === 'RELIEF') {
itemName = '릴리프 밸브';
} else {
// description에서 추출
// description에서 추출 (BOM 페이지와 동일한 로직)
const desc = cleanDescription.toUpperCase();
if (desc.includes('GATE')) {
itemName = '게이트 밸브';
} else if (desc.includes('BALL')) {
itemName = '볼 밸브';
} else if (desc.includes('GLOBE')) {
itemName = '글로브 밸브';
} else if (desc.includes('CHECK')) {
itemName = '체크 밸브';
} else if (desc.includes('BUTTERFLY')) {
itemName = '버터플라이 밸브';
} else if (desc.includes('NEEDLE')) {
itemName = '니들 밸브';
} else if (desc.includes('RELIEF')) {
itemName = '릴리프 밸브';
if (desc.includes('SIGHT GLASS') || desc.includes('사이트글라스')) {
itemName = 'SIGHT GLASS';
} else if (desc.includes('STRAINER') || desc.includes('스트레이너')) {
itemName = 'STRAINER';
} else if (desc.includes('GATE') || desc.includes('게이트')) {
itemName = 'GATE VALVE';
} else if (desc.includes('BALL') || desc.includes('볼')) {
itemName = 'BALL VALVE';
} else if (desc.includes('CHECK') || desc.includes('체크')) {
itemName = 'CHECK VALVE';
} else if (desc.includes('GLOBE') || desc.includes('글로브')) {
itemName = 'GLOBE VALVE';
} else if (desc.includes('BUTTERFLY') || desc.includes('버터플라이')) {
itemName = 'BUTTERFLY VALVE';
} else if (desc.includes('NEEDLE') || desc.includes('니들')) {
itemName = 'NEEDLE VALVE';
} else if (desc.includes('RELIEF') || desc.includes('릴리프')) {
itemName = 'RELIEF VALVE';
} else {
itemName = '밸브';
itemName = 'VALVE';
}
}
} else if (category === 'GASKET') {
@@ -489,13 +543,15 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
const desc = cleanDescription.toUpperCase();
if (desc.includes('URETHANE') || desc.includes('BLOCK SHOE')) {
itemName = 'URETHANE BLOCK SHOE';
// 우레탄 블럭슈의 경우 두께 정보 추가
const thicknessMatch = desc.match(/(\d+)\s*[tT](?![oO])/);
if (thicknessMatch) {
itemName += ` ${thicknessMatch[1]}t`;
}
// 우레탄 블럭슈의 경우 두께 정보는 품목명에 포함하지 않음 (재질 열에서 처리)
} else if (desc.includes('CLAMP')) {
itemName = 'CLAMP';
// 클램프 타입 상세 분류 (CL-1, CL-2, CL-3 등)
const clampMatch = desc.match(/CL[-\s]*(\d+)/i);
if (clampMatch) {
itemName = `CLAMP CL-${clampMatch[1]}`;
} else {
itemName = 'CLAMP CL-1'; // 기본값
}
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
itemName = 'U-BOLT';
} else if (desc.includes('HANGER')) {
@@ -823,17 +879,17 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
base['관리항목3'] = ''; // N열
base['관리항목4'] = ''; // O열
} else if (category === 'VALVE') {
// 밸브 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
base['크기'] = material.size_spec || '-'; // F열
// 밸브 전용 컬럼 (F~O) - 새로운 구조
base['크기'] = material.size_spec || material.main_nom || '-'; // F열
base['압력등급'] = pressure; // G열
base['재질'] = grade; // H열
base['상세내역'] = detailInfo || '-'; // I열
base['사용자요구'] = material.user_requirement || ''; // J열
base['관리항목1'] = ''; // K열
base['관리항목2'] = ''; // L열
base['관리항목3'] = ''; // M열
base['관리항목4'] = ''; // N열
base['관리항목5'] = ''; // O열
base['브랜드'] = material.brand || '-'; // H열 (사용자 입력)
base['추가정보'] = extractValveAdditionalInfo(cleanDescription); // I열 (3-WAY, DOUL PLATE 등)
base['연결방식'] = material.connection_type || extractValveConnectionType(cleanDescription); // J열
base['추가요청사항'] = material.user_requirement || ''; // K열
base['관리항목1'] = ''; // L열
base['관리항목2'] = ''; // M열
base['관리항목3'] = ''; // N열
base['관리항목4'] = ''; // O열
} else if (category === 'GASKET') {
// 가스켓 전용 컬럼 (F~O) - 재질을 2개로 분리
// 재질 분리 로직: SS304/GRAPHITE/SS304/SS304 → 재질1: SS304/GRAPHITE, 재질2: /SS304/SS304
@@ -866,10 +922,10 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
base['재질1'] = material1; // I열: SS304/GRAPHITE
base['재질2'] = material2; // J열: SS304/SS304
base['두께'] = gasketThickness || '-'; // K열: 4.5mm
base['사용자요구'] = material.user_requirement || ''; // L열
base['관리항목1'] = ''; // M열
base['관리항목2'] = ''; // N열
base['관리항목3'] = ''; // O열
base['사용자요구'] = detailInfo; // L열: 분류기에서 추출된 요구사항
base['추가요청사항'] = material.user_requirement || ''; // M열: 사용자 입력 요구사항
base['관리항목1'] = ''; // N열
base['관리항목2'] = ''; // O열
} else if (category === 'BOLT') {
// 볼트 전용 컬럼 (F~O) - User Requirements와 Additional Request 분리
base['크기'] = material.size_spec || '-'; // F열
@@ -883,17 +939,17 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
base['관리항목3'] = ''; // N열
base['관리항목4'] = ''; // O열
} else if (category === 'SUPPORT') {
// 서포트 전용 컬럼 (F~O)
// 서포트 전용 컬럼 (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열
base['관리항목4'] = ''; // O열
base['재질'] = grade; // G열
base['사용자요구'] = material.user_requirements?.join(', ') || ''; // H열 (분류기에서 추출)
base['추가요청사항'] = material.user_requirement || ''; // I열 (사용자 입력)
base['관리항목1'] = ''; // J열
base['관리항목2'] = ''; // K열
base['관리항목3'] = ''; // L열
base['관리항목4'] = ''; // M열
base['관리항목5'] = ''; // N열
base['관리항목6'] = ''; // O열
} else {
// 기타 카테고리 기본 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
base['크기'] = material.size_spec || '-'; // F열
@@ -1153,10 +1209,10 @@ export const createExcelBlob = async (materials, filename, options = {}) => {
'PIPE': ['크기', '스케줄', '재질', '제조방법', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
'FITTING': ['크기', '압력등급', '스케줄', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
'FLANGE': ['크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
'VALVE': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
'GASKET': ['크기', '압력등급', '구조', '재질1', '재질2', '두께', '사용자요구', '관리항목1', '관리항목2', '관리항목3'],
'VALVE': ['크기', '압력등급', '브랜드', '추가정보', '연결방식', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
'GASKET': ['크기', '압력등급', '구조', '재질1', '재질2', '두께', '사용자요구', '추가요청사항', '관리항목1', '관리항목2'],
'BOLT': ['크기', '압력등급', '길이', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
'SUPPORT': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
'SUPPORT': ['크기', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '관리항목5', '관리항목6'],
};
const deliveryDateHeader = '납기일(YYYY-MM-DD)';