feat: 엑셀 다운로드 방식 개선 - BOM에서 생성한 엑셀을 구매관리에서 다운로드
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- 파이프, 피팅, 플랜지, 밸브 카테고리에 새로운 엑셀 업로드 로직 적용
- createExcelBlob 함수로 클라이언트에서 엑셀 생성 후 서버 업로드
- /purchase-request/upload-excel API로 엑셀 파일 서버 저장
- 구매관리 페이지에서 원본 엑셀 파일 다운로드 가능
- 가스켓, 볼트, 서포트는 추후 개선 시 적용 예정

배포 버전: index-5e5aa4a4.js
This commit is contained in:
hyungi
2025-10-16 14:53:22 +09:00
parent c7297c6fb7
commit a5bfeec9aa
8 changed files with 425 additions and 200 deletions

View File

@@ -302,65 +302,85 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
itemName = displayType;
} else if (category === 'FLANGE') {
// 플랜지 상세 타입 표시
// 플랜지 상세 타입 표시 - 프론트엔드 표시와 동일한 로직 사용
const flangeDetails = material.flange_details || {};
const flangeType = flangeDetails.flange_type || '';
const facingType = flangeDetails.facing_type || '';
const rawFlangeType = flangeDetails.flange_type || '';
const rawFacingType = flangeDetails.facing_type || '';
// 플랜지 타입 풀네임 매핑 (한국어)
const flangeTypeKoreanMap = {
// 플랜지 타입 풀네임 매핑 (어)
const flangeTypeMap = {
'WN': 'WELD NECK FLANGE',
'WELD_NECK': 'WELD NECK FLANGE',
'SLIP_ON': 'SLIP ON FLANGE',
'SO': 'SLIP ON FLANGE',
'SLIP_ON': 'SLIP ON FLANGE',
'SW': 'SOCKET WELD FLANGE',
'SOCKET_WELD': 'SOCKET WELD FLANGE',
'THREADED': 'THREADED FLANGE',
'THD': 'THREADED FLANGE',
'BLIND': 'BLIND FLANGE',
'LAP_JOINT': 'LAP JOINT FLANGE',
'LJ': 'LAP JOINT FLANGE',
'REDUCING': 'REDUCING FLANGE',
'ORIFICE': 'ORIFICE FLANGE',
'SPECTACLE': 'SPECTACLE BLIND',
'SPECTACLE_BLIND': 'SPECTACLE BLIND',
'PADDLE': 'PADDLE BLIND',
'SPACER': 'SPACER'
'PADDLE_BLIND': 'PADDLE BLIND',
'SPACER': 'SPACER',
'SWIVEL': 'SWIVEL FLANGE',
'DRIP_RING': 'DRIP RING',
'NOZZLE': 'NOZZLE FLANGE'
};
// 끝단처리 정보 추가
const facingInfo = facingType ? ` ${facingType}` : '';
if (flangeType && flangeTypeKoreanMap[flangeType]) {
itemName = `${flangeTypeKoreanMap[flangeType]}${facingInfo}`;
} else {
// description에서 추출
// rawFlangeType에서 facing 정보 분리 (예: "WN RF" -> "WN")
let cleanFlangeType = rawFlangeType;
if (rawFlangeType.includes(' RF')) {
cleanFlangeType = rawFlangeType.replace(' RF', '').trim();
} else if (rawFlangeType.includes(' FF')) {
cleanFlangeType = rawFlangeType.replace(' FF', '').trim();
} else if (rawFlangeType.includes(' RTJ')) {
cleanFlangeType = rawFlangeType.replace(' RTJ', '').trim();
}
let displayType = flangeTypeMap[cleanFlangeType] || '';
// Description에서 추출 (매핑되지 않은 경우)
if (!displayType) {
const desc = cleanDescription.toUpperCase();
if (desc.includes('ORIFICE')) {
itemName = `ORIFICE FLANGE${facingInfo}`;
displayType = 'ORIFICE FLANGE';
} else if (desc.includes('SPECTACLE')) {
itemName = `SPECTACLE BLIND${facingInfo}`;
displayType = 'SPECTACLE BLIND';
} else if (desc.includes('PADDLE')) {
itemName = `PADDLE BLIND${facingInfo}`;
displayType = 'PADDLE BLIND';
} else if (desc.includes('SPACER')) {
itemName = `SPACER${facingInfo}`;
displayType = 'SPACER';
} else if (desc.includes('REDUCING') || desc.includes('RED')) {
itemName = `REDUCING FLANGE${facingInfo}`;
displayType = 'REDUCING FLANGE';
} else if (desc.includes('BLIND')) {
itemName = `BLIND FLANGE${facingInfo}`;
displayType = 'BLIND FLANGE';
} else if (desc.includes('WN RF') || desc.includes('WN-RF')) {
displayType = 'WELD NECK FLANGE';
} else if (desc.includes('WN FF') || desc.includes('WN-FF')) {
displayType = 'WELD NECK FLANGE';
} else if (desc.includes('WN RTJ') || desc.includes('WN-RTJ')) {
displayType = 'WELD NECK FLANGE';
} else if (desc.includes('WN')) {
itemName = `WELD NECK FLANGE${facingInfo}`;
displayType = 'WELD NECK FLANGE';
} else if (desc.includes('SO RF') || desc.includes('SO-RF')) {
displayType = 'SLIP ON FLANGE';
} else if (desc.includes('SO FF') || desc.includes('SO-FF')) {
displayType = 'SLIP ON FLANGE';
} else if (desc.includes('SO')) {
itemName = `SLIP ON FLANGE${facingInfo}`;
displayType = 'SLIP ON FLANGE';
} else if (desc.includes('SW')) {
itemName = `SOCKET WELD FLANGE${facingInfo}`;
displayType = 'SOCKET WELD FLANGE';
} else {
itemName = `FLANGE${facingInfo}`;
displayType = 'FLANGE';
}
}
// 상세내역에 플랜지 타입 정보 저장
if (flangeDetails.flange_type) {
detailInfo = `${flangeType} ${facingType}`.trim();
} else {
// description에서 추출
const flangeTypeMatch = cleanDescription.match(/FLG\s+([^,]+?)(?=\s*SCH|\s*,\s*\d+LB|$)/i);
if (flangeTypeMatch) {
detailInfo = flangeTypeMatch[1].trim();
}
}
itemName = displayType;
} else if (category === 'VALVE') {
// 밸브 상세 타입 표시
const valveDetails = material.valve_details || {};
@@ -736,50 +756,29 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
base['관리항목3'] = ''; // N열
base['관리항목4'] = ''; // O열
} else if (category === 'FLANGE') {
// 플랜지 타입 풀네임 매핑 (영어)
const flangeTypeMap = {
'WN': 'WELD NECK FLANGE',
'WELD_NECK': 'WELD NECK FLANGE',
'SO': 'SLIP ON FLANGE',
'SLIP_ON': 'SLIP ON FLANGE',
'SW': 'SOCKET WELD FLANGE',
'SOCKET_WELD': 'SOCKET WELD FLANGE',
'BL': 'BLIND FLANGE',
'BLIND': 'BLIND FLANGE',
'RED': 'REDUCING FLANGE',
'REDUCING': 'REDUCING FLANGE',
'ORIFICE': 'ORIFICE FLANGE',
'SPECTACLE': 'SPECTACLE BLIND',
'PADDLE': 'PADDLE BLIND',
'SPACER': 'SPACER'
};
// 플랜지 전용 컬럼 (F~O) - 크기 이후에 페이싱, 압력, 스케줄, Material Grade, 추가요구사항 순서
base['크기'] = material.size_spec || '-'; // F열
// 끝단처리 풀네임 매핑 (영어)
// 페이싱 정보 (G열)
const rawFacingType = material.flange_details?.facing_type || '';
const facingTypeMap = {
'RF': 'RAISED FACE',
'RAISED_FACE': 'RAISED FACE',
'FF': 'FULL FACE',
'FULL_FACE': 'FULL FACE',
'RTJ': 'RING JOINT',
'RING_JOINT': 'RING JOINT',
'MALE': 'MALE',
'FEMALE': 'FEMALE'
'FF': 'FLAT FACE',
'FLAT_FACE': 'FLAT FACE',
'RTJ': 'RING TYPE JOINT',
'RING_TYPE_JOINT': 'RING TYPE JOINT'
};
base['페이싱'] = facingTypeMap[rawFacingType] || rawFacingType || '-'; // G열
const rawFlangeType = material.flange_details?.flange_type || '';
const rawFacingType = material.flange_details?.facing_type || '';
// 플랜지 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
base['크기'] = material.size_spec || '-'; // F
base['압력등급'] = pressure; // G
base['재질'] = grade; // H
base['페이싱'] = facingTypeMap[rawFacingType] || rawFacingType || '-'; // I
base['사용자요구'] = material.user_requirement || ''; // J열
base['관리항목1'] = ''; // K열
base['관리항목2'] = ''; // L열
base['관리항목3'] = ''; // M열
base['관리항목4'] = ''; // N열
base['관리항목5'] = ''; // O열
base['압력등급'] = pressure; // H열
base['스케줄'] = schedule; // I열
base['재질'] = grade; // J열
base['추가요구사항'] = material.user_requirement || ''; // K열
base['관리항목1'] = ''; // L
base['관리항목2'] = ''; // M
base['관리항목3'] = ''; // N
base['관리항목4'] = ''; // O
} else if (category === 'VALVE') {
// 밸브 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
base['크기'] = material.size_spec || '-'; // F열
@@ -1059,4 +1058,77 @@ export const exportComparisonToExcel = (comparisonData, filename, additionalInfo
alert('엑셀 파일 생성에 실패했습니다: ' + error.message);
return false;
}
};
};
// 엑셀 Blob 생성 함수 (서버 업로드용)
export const createExcelBlob = async (materials, filename, options = {}) => {
try {
// 기존 exportMaterialsToExcel과 동일한 로직이지만 Blob만 반환
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Materials');
// 헤더 설정
const headers = [
'TAGNO', '품목명', '수량', '통화구분', '단가'
];
// 카테고리별 추가 헤더
if (options.category === 'PIPE') {
headers.push('크기', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)');
} else if (options.category === 'FITTING') {
headers.push('크기', '압력등급', '스케줄', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)');
} else if (options.category === 'FLANGE') {
headers.push('크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)');
} else {
// 기본 헤더
headers.push('크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)');
}
// 헤더 추가
worksheet.addRow(headers);
// 헤더 스타일링
const headerRow = worksheet.getRow(1);
headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
headerRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF366092' }
};
headerRow.alignment = { horizontal: 'center', vertical: 'middle' };
// 데이터 추가
materials.forEach(material => {
const formattedData = formatMaterialForExcel(material, options.category || 'UNKNOWN');
const rowData = headers.map(header => {
const key = Object.keys(formattedData).find(k =>
formattedData[k] !== undefined &&
(header.includes(k) || k.includes(header.replace(/[()]/g, '').split(' ')[0]))
);
return key ? formattedData[key] : '';
});
worksheet.addRow(rowData);
});
// 컬럼 너비 자동 조정
worksheet.columns.forEach(column => {
let maxLength = 0;
column.eachCell({ includeEmpty: true }, (cell) => {
const columnLength = cell.value ? cell.value.toString().length : 10;
if (columnLength > maxLength) {
maxLength = columnLength;
}
});
column.width = Math.min(Math.max(maxLength + 2, 10), 50);
});
// Blob 생성
const excelBuffer = await workbook.xlsx.writeBuffer();
return new Blob([excelBuffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
} catch (error) {
console.error('엑셀 Blob 생성 실패:', error);
throw error;
}
};