Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 파이프, 피팅, 플랜지, 밸브 카테고리에 새로운 엑셀 업로드 로직 적용 - createExcelBlob 함수로 클라이언트에서 엑셀 생성 후 서버 업로드 - /purchase-request/upload-excel API로 엑셀 파일 서버 저장 - 구매관리 페이지에서 원본 엑셀 파일 다운로드 가능 - 가스켓, 볼트, 서포트는 추후 개선 시 적용 예정 배포 버전: index-5e5aa4a4.js
1135 lines
43 KiB
JavaScript
1135 lines
43 KiB
JavaScript
import * as XLSX from 'xlsx';
|
||
import { saveAs } from 'file-saver';
|
||
import { calculatePurchaseQuantity } from './purchaseCalculator';
|
||
|
||
/**
|
||
* 자재 목록을 카테고리별로 그룹화
|
||
*/
|
||
const groupMaterialsByCategory = (materials) => {
|
||
const groups = {};
|
||
|
||
materials.forEach(material => {
|
||
const category = material.classified_category || material.category || 'UNCATEGORIZED';
|
||
if (!groups[category]) {
|
||
groups[category] = [];
|
||
}
|
||
groups[category].push(material);
|
||
});
|
||
|
||
return groups;
|
||
};
|
||
|
||
/**
|
||
* 동일한 자재끼리 합치기 (자재 설명 + 사이즈 기준)
|
||
* 엑셀 내보내기용 특별 처리:
|
||
* - PIPE: 끝단 정보 제거 (BOE-POE, POE-TOE 등)
|
||
* - NIPPLE: 길이별 구분 (75mm, 100mm 등)
|
||
*/
|
||
const consolidateMaterials = (materials, isComparison = false) => {
|
||
const consolidated = {};
|
||
|
||
materials.forEach(material => {
|
||
const category = material.classified_category || material.category || 'UNCATEGORIZED';
|
||
let description = material.original_description || material.description || '';
|
||
const sizeSpec = material.size_spec || '';
|
||
|
||
// 파이프 끝단 정보 제거 (엑셀 내보내기용)
|
||
if (category === 'PIPE') {
|
||
description = description
|
||
.replace(/\s+(BOE-POE|POE-TOE|BOE-TOE|BOE-BBE|BBE-POE|BBE-BBE|POE-POE|TOE-TOE)\s*$/i, '')
|
||
.replace(/\s+(BOE|POE|TOE|BBE)\s*-\s*(BOE|POE|TOE|BBE)\s*$/i, '')
|
||
.trim();
|
||
}
|
||
|
||
// 니플의 경우 길이 정보를 그룹화 키에 포함
|
||
let lengthInfo = '';
|
||
if (category === 'FITTING' && description.toLowerCase().includes('nipple')) {
|
||
const lengthMatch = description.match(/(\d+)\s*mm/i);
|
||
if (lengthMatch) {
|
||
lengthInfo = `_${lengthMatch[1]}mm`;
|
||
}
|
||
}
|
||
|
||
// 그룹화 키: 카테고리 + 정제된자재설명 + 사이즈 + 길이정보
|
||
const groupKey = `${category}|${description}|${sizeSpec}${lengthInfo}`;
|
||
|
||
if (!consolidated[groupKey]) {
|
||
consolidated[groupKey] = {
|
||
...material,
|
||
// 정제된 설명으로 덮어쓰기
|
||
original_description: description,
|
||
description: description,
|
||
quantity: 0,
|
||
totalLength: 0, // 파이프용
|
||
itemCount: 0, // 파이프 개수
|
||
lineNumbers: [], // 라인 번호들
|
||
// 비교 모드용
|
||
previous_quantity: 0,
|
||
current_quantity: 0,
|
||
quantity_change: 0
|
||
};
|
||
}
|
||
|
||
const group = consolidated[groupKey];
|
||
group.quantity += material.quantity || 0;
|
||
|
||
// 비교 모드인 경우 이전/현재 수량도 합산
|
||
if (isComparison) {
|
||
group.previous_quantity += material.previous_quantity || 0;
|
||
group.current_quantity += material.current_quantity || 0;
|
||
group.quantity_change += material.quantity_change || 0;
|
||
}
|
||
|
||
// 파이프인 경우 길이 계산
|
||
if (category === 'PIPE') {
|
||
const lengthMm = material.pipe_details?.length_mm || 0;
|
||
const lengthM = lengthMm / 1000;
|
||
|
||
if (isComparison) {
|
||
// 비교 모드에서는 이전/현재 길이 계산
|
||
group.totalLength += lengthM * (material.current_quantity || material.quantity || 0);
|
||
group.itemCount += material.current_quantity || material.quantity || 0;
|
||
|
||
// 이전 길이도 저장
|
||
if (!group.previousTotalLength) group.previousTotalLength = 0;
|
||
group.previousTotalLength += lengthM * (material.previous_quantity || 0);
|
||
} else {
|
||
group.totalLength += lengthM * (material.quantity || 0);
|
||
group.itemCount += material.quantity || 0;
|
||
}
|
||
}
|
||
|
||
// 라인 번호 수집
|
||
if (material.line_number) {
|
||
group.lineNumbers.push(material.line_number);
|
||
}
|
||
});
|
||
|
||
// 라인 번호를 문자열로 변환
|
||
Object.values(consolidated).forEach(group => {
|
||
group.line_number = group.lineNumbers.length > 0
|
||
? group.lineNumbers.join(', ')
|
||
: '-';
|
||
});
|
||
|
||
return Object.values(consolidated);
|
||
};
|
||
|
||
/**
|
||
* 자재 데이터를 엑셀용 형태로 변환
|
||
*/
|
||
const formatMaterialForExcel = (material, includeComparison = false) => {
|
||
const category = material.classified_category || material.category || '-';
|
||
|
||
// 엑셀용 자재 설명 정제
|
||
let cleanDescription = material.original_description || material.description || '-';
|
||
|
||
// 파이프 끝단 정보 제거
|
||
if (category === 'PIPE') {
|
||
cleanDescription = cleanDescription
|
||
.replace(/\s+(BOE-POE|POE-TOE|BOE-TOE|BOE-BBE|BBE-POE|BBE-BBE|POE-POE|TOE-TOE)\s*$/i, '')
|
||
.replace(/\s+(BOE|POE|TOE|BBE)\s*-\s*(BOE|POE|TOE|BBE)\s*$/i, '')
|
||
.trim();
|
||
}
|
||
|
||
// 니플의 경우 길이 정보 명시적 추가
|
||
if (category === 'FITTING' && cleanDescription.toLowerCase().includes('nipple')) {
|
||
if (material.fitting_details && material.fitting_details.length_mm) {
|
||
const lengthMm = Math.round(material.fitting_details.length_mm);
|
||
if (!cleanDescription.match(/\d+\s*mm/i)) {
|
||
cleanDescription += ` ${lengthMm}mm`;
|
||
}
|
||
} else {
|
||
const lengthMatch = material.original_description?.match(/(\d+)\s*mm/i);
|
||
if (lengthMatch && !cleanDescription.match(/\d+\s*mm/i)) {
|
||
cleanDescription += ` ${lengthMatch[1]}mm`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 구매 수량 계산
|
||
const purchaseInfo = calculatePurchaseQuantity(material);
|
||
|
||
// 변수 선언 (먼저 선언)
|
||
let itemName = '';
|
||
let detailInfo = '';
|
||
let gasketMaterial = '';
|
||
let gasketThickness = '';
|
||
if (category === 'PIPE') {
|
||
// 파이프 상세 타입 표시 개선
|
||
const pipeDetails = material.pipe_details || {};
|
||
const manufacturingMethod = pipeDetails.manufacturing_method || '';
|
||
const endPreparation = pipeDetails.end_preparation || '';
|
||
|
||
// 제조방법만으로 상세 타입 생성 (끝단처리 정보 제거)
|
||
if (manufacturingMethod) {
|
||
itemName = `${manufacturingMethod} PIPE`;
|
||
} else {
|
||
// description에서 제조방법 추출 시도
|
||
const desc = cleanDescription.toUpperCase();
|
||
if (desc.includes('SEAMLESS')) {
|
||
itemName = 'SEAMLESS PIPE';
|
||
} else if (desc.includes('WELDED')) {
|
||
itemName = 'WELDED PIPE';
|
||
} else if (desc.includes('ERW')) {
|
||
itemName = 'ERW PIPE';
|
||
} else if (desc.includes('SMLS')) {
|
||
itemName = 'SEAMLESS PIPE';
|
||
} else {
|
||
itemName = 'PIPE';
|
||
}
|
||
}
|
||
} else if (category === 'FITTING') {
|
||
// 피팅 상세 타입 표시 - 프론트엔드 표시와 동일한 로직 사용
|
||
const fittingDetails = material.fitting_details || {};
|
||
const classificationDetails = material.classification_details || {};
|
||
const fittingTypeInfo = classificationDetails.fitting_type || {};
|
||
|
||
const fittingType = fittingTypeInfo.type || fittingDetails.fitting_type || '';
|
||
const fittingSubtype = fittingTypeInfo.subtype || fittingDetails.fitting_subtype || '';
|
||
|
||
// 프론트엔드와 동일한 displayType 로직 사용
|
||
let displayType = '';
|
||
|
||
if (fittingType === 'OLET') {
|
||
// OLET 풀네임 표시
|
||
switch (fittingSubtype) {
|
||
case 'SOCKOLET':
|
||
displayType = 'SOCK-O-LET';
|
||
break;
|
||
case 'WELDOLET':
|
||
displayType = 'WELD-O-LET';
|
||
break;
|
||
case 'ELLOLET':
|
||
displayType = 'ELL-O-LET';
|
||
break;
|
||
case 'THREADOLET':
|
||
displayType = 'THREAD-O-LET';
|
||
break;
|
||
case 'ELBOLET':
|
||
displayType = 'ELB-O-LET';
|
||
break;
|
||
case 'NIPOLET':
|
||
displayType = 'NIP-O-LET';
|
||
break;
|
||
case 'COUPOLET':
|
||
displayType = 'COUP-O-LET';
|
||
break;
|
||
default:
|
||
// Description에서 직접 추출
|
||
const descUpper = cleanDescription.toUpperCase();
|
||
if (descUpper.includes('SOCK-O-LET') || descUpper.includes('SOCKOLET')) {
|
||
displayType = 'SOCK-O-LET';
|
||
} else if (descUpper.includes('WELD-O-LET') || descUpper.includes('WELDOLET')) {
|
||
displayType = 'WELD-O-LET';
|
||
} else if (descUpper.includes('ELL-O-LET') || descUpper.includes('ELLOLET')) {
|
||
displayType = 'ELL-O-LET';
|
||
} else if (descUpper.includes('THREAD-O-LET') || descUpper.includes('THREADOLET')) {
|
||
displayType = 'THREAD-O-LET';
|
||
} else if (descUpper.includes('ELB-O-LET') || descUpper.includes('ELBOLET')) {
|
||
displayType = 'ELB-O-LET';
|
||
} else if (descUpper.includes('NIP-O-LET') || descUpper.includes('NIPOLET')) {
|
||
displayType = 'NIP-O-LET';
|
||
} else if (descUpper.includes('COUP-O-LET') || descUpper.includes('COUPOLET')) {
|
||
displayType = 'COUP-O-LET';
|
||
} else {
|
||
displayType = 'OLET';
|
||
}
|
||
}
|
||
} else 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 (cleanDescription.toUpperCase().includes('TEE RED')) {
|
||
displayType = 'TEE REDUCING';
|
||
} else if (cleanDescription.toUpperCase().includes('RED CONC')) {
|
||
displayType = 'REDUCER CONC';
|
||
} else if (cleanDescription.toUpperCase().includes('RED ECC')) {
|
||
displayType = 'REDUCER ECC';
|
||
} else if (cleanDescription.toUpperCase().includes('CAP')) {
|
||
if (cleanDescription.includes('NPT(F)')) {
|
||
displayType = 'CAP NPT(F)';
|
||
} else if (cleanDescription.includes('SW')) {
|
||
displayType = 'CAP SW';
|
||
} else if (cleanDescription.includes('BW')) {
|
||
displayType = 'CAP BW';
|
||
} else {
|
||
displayType = 'CAP';
|
||
}
|
||
} else if (cleanDescription.toUpperCase().includes('PLUG')) {
|
||
if (cleanDescription.toUpperCase().includes('HEX')) {
|
||
if (cleanDescription.includes('NPT(M)')) {
|
||
displayType = 'HEX PLUG NPT(M)';
|
||
} else {
|
||
displayType = 'HEX PLUG';
|
||
}
|
||
} else if (cleanDescription.includes('NPT(M)')) {
|
||
displayType = 'PLUG NPT(M)';
|
||
} else if (cleanDescription.includes('NPT')) {
|
||
displayType = 'PLUG NPT';
|
||
} else {
|
||
displayType = 'PLUG';
|
||
}
|
||
} else if (fittingType === 'NIPPLE') {
|
||
const length = fittingDetails.length_mm || fittingDetails.avg_length_mm;
|
||
let nippleType = 'NIPPLE';
|
||
if (length) nippleType += ` ${length}mm`;
|
||
displayType = nippleType;
|
||
} else if (fittingType === 'ELBOW') {
|
||
// 엘보 상세 정보 표시
|
||
let elbowDetails = [];
|
||
|
||
// 각도 정보
|
||
if (fittingSubtype.includes('90DEG') || cleanDescription.includes('90')) {
|
||
elbowDetails.push('90°');
|
||
} else if (fittingSubtype.includes('45DEG') || cleanDescription.includes('45')) {
|
||
elbowDetails.push('45°');
|
||
}
|
||
|
||
// 반경 정보
|
||
if (fittingSubtype.includes('LONG_RADIUS') || cleanDescription.toUpperCase().includes('LR')) {
|
||
elbowDetails.push('LR');
|
||
} else if (fittingSubtype.includes('SHORT_RADIUS') || cleanDescription.toUpperCase().includes('SR')) {
|
||
elbowDetails.push('SR');
|
||
}
|
||
|
||
displayType = elbowDetails.length > 0 ? `ELBOW ${elbowDetails.join(' ')}` : 'ELBOW';
|
||
} else {
|
||
displayType = fittingType || 'FITTING';
|
||
}
|
||
|
||
itemName = displayType;
|
||
} else if (category === 'FLANGE') {
|
||
// 플랜지 상세 타입 표시 - 프론트엔드 표시와 동일한 로직 사용
|
||
const flangeDetails = material.flange_details || {};
|
||
const rawFlangeType = flangeDetails.flange_type || '';
|
||
const rawFacingType = flangeDetails.facing_type || '';
|
||
|
||
// 플랜지 타입 풀네임 매핑 (영어)
|
||
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',
|
||
'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',
|
||
'PADDLE_BLIND': 'PADDLE BLIND',
|
||
'SPACER': 'SPACER',
|
||
'SWIVEL': 'SWIVEL FLANGE',
|
||
'DRIP_RING': 'DRIP RING',
|
||
'NOZZLE': 'NOZZLE FLANGE'
|
||
};
|
||
|
||
// 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')) {
|
||
displayType = 'ORIFICE FLANGE';
|
||
} else if (desc.includes('SPECTACLE')) {
|
||
displayType = 'SPECTACLE BLIND';
|
||
} else if (desc.includes('PADDLE')) {
|
||
displayType = 'PADDLE BLIND';
|
||
} else if (desc.includes('SPACER')) {
|
||
displayType = 'SPACER';
|
||
} else if (desc.includes('REDUCING') || desc.includes('RED')) {
|
||
displayType = 'REDUCING FLANGE';
|
||
} else if (desc.includes('BLIND')) {
|
||
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')) {
|
||
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')) {
|
||
displayType = 'SLIP ON FLANGE';
|
||
} else if (desc.includes('SW')) {
|
||
displayType = 'SOCKET WELD FLANGE';
|
||
} else {
|
||
displayType = 'FLANGE';
|
||
}
|
||
}
|
||
|
||
itemName = displayType;
|
||
} else if (category === 'VALVE') {
|
||
// 밸브 상세 타입 표시
|
||
const valveDetails = material.valve_details || {};
|
||
const valveType = valveDetails.valve_type || '';
|
||
|
||
if (valveType === 'GATE') {
|
||
itemName = '게이트 밸브';
|
||
} else if (valveType === 'BALL') {
|
||
itemName = '볼 밸브';
|
||
} else if (valveType === 'GLOBE') {
|
||
itemName = '글로브 밸브';
|
||
} else if (valveType === 'CHECK') {
|
||
itemName = '체크 밸브';
|
||
} else if (valveType === 'BUTTERFLY') {
|
||
itemName = '버터플라이 밸브';
|
||
} else if (valveType === 'NEEDLE') {
|
||
itemName = '니들 밸브';
|
||
} else if (valveType === 'RELIEF') {
|
||
itemName = '릴리프 밸브';
|
||
} else {
|
||
// description에서 추출
|
||
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 = '릴리프 밸브';
|
||
} else {
|
||
itemName = '밸브';
|
||
}
|
||
}
|
||
} else if (category === 'GASKET') {
|
||
// 가스켓 상세 타입 표시
|
||
const gasketDetails = material.gasket_details || {};
|
||
const gasketType = gasketDetails.gasket_type || '';
|
||
const gasketSubtype = gasketDetails.gasket_subtype || '';
|
||
|
||
if (gasketType === 'SPIRAL_WOUND') {
|
||
itemName = '스파이럴 워운드 가스켓';
|
||
} else if (gasketType === 'RING_JOINT') {
|
||
itemName = '링 조인트 가스켓';
|
||
} else if (gasketType === 'FULL_FACE') {
|
||
itemName = '풀 페이스 가스켓';
|
||
} else if (gasketType === 'RAISED_FACE') {
|
||
itemName = '레이즈드 페이스 가스켓';
|
||
} else if (gasketSubtype && gasketSubtype !== gasketType) {
|
||
itemName = gasketSubtype;
|
||
} else if (gasketType) {
|
||
itemName = gasketType;
|
||
} else {
|
||
// gasket_details가 없으면 description에서 추출
|
||
const desc = cleanDescription.toUpperCase();
|
||
if (desc.includes('SWG') || desc.includes('SPIRAL')) {
|
||
itemName = '스파이럴 워운드 가스켓';
|
||
} else if (desc.includes('RTJ') || desc.includes('RING')) {
|
||
itemName = '링 조인트 가스켓';
|
||
} else if (desc.includes('FF') || desc.includes('FULL FACE')) {
|
||
itemName = '풀 페이스 가스켓';
|
||
} else if (desc.includes('RF') || desc.includes('RAISED')) {
|
||
itemName = '레이즈드 페이스 가스켓';
|
||
} else {
|
||
itemName = '가스켓';
|
||
}
|
||
}
|
||
} 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 = '체크밸브용 볼트';
|
||
} else {
|
||
// description에서 추출
|
||
const desc = cleanDescription.toUpperCase();
|
||
if (desc.includes('PSV')) {
|
||
itemName = 'PSV 볼트';
|
||
} else if (desc.includes('LT')) {
|
||
itemName = '저온용 볼트';
|
||
} 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 = '볼트';
|
||
}
|
||
}
|
||
} else if (category === 'SUPPORT' || category === 'U_BOLT') {
|
||
// 서포트 상세 타입 표시
|
||
const desc = cleanDescription.toUpperCase();
|
||
if (desc.includes('URETHANE') || desc.includes('BLOCK SHOE')) {
|
||
itemName = '우레탄 블록 슈';
|
||
} else if (desc.includes('CLAMP')) {
|
||
itemName = '클램프';
|
||
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
|
||
itemName = '유볼트';
|
||
} else if (desc.includes('HANGER')) {
|
||
itemName = '행거';
|
||
} else if (desc.includes('SPRING')) {
|
||
itemName = '스프링 서포트';
|
||
} else {
|
||
itemName = '서포트';
|
||
}
|
||
} else {
|
||
itemName = category || 'UNKNOWN';
|
||
}
|
||
|
||
// 압력 등급 추출 (카테고리별 처리)
|
||
let pressure = '-';
|
||
|
||
if (category === 'GASKET') {
|
||
// 가스켓의 경우 gasket_details에서 압력등급 추출
|
||
if (material.gasket_details && material.gasket_details.pressure_rating) {
|
||
pressure = material.gasket_details.pressure_rating;
|
||
} else {
|
||
// gasket_details가 없으면 description에서 추출
|
||
const pressureMatch = cleanDescription.match(/(\d+)LB/i);
|
||
if (pressureMatch) {
|
||
pressure = `${pressureMatch[1]}LB`;
|
||
}
|
||
}
|
||
} else {
|
||
// 다른 카테고리는 기존 방식
|
||
const pressureMatch = cleanDescription.match(/(\d+)LB/i);
|
||
if (pressureMatch) {
|
||
pressure = `${pressureMatch[1]}LB`;
|
||
}
|
||
}
|
||
|
||
// 스케줄/길이 추출 (카테고리별 처리)
|
||
let schedule = '-';
|
||
|
||
if (category === 'BOLT') {
|
||
// 볼트의 경우 길이 정보 추출
|
||
const lengthPatterns = [
|
||
/(\d+(?:\.\d+)?)\s*LG/i, // 145.0000 LG, 75 LG
|
||
/(\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 형태
|
||
];
|
||
|
||
for (const pattern of lengthPatterns) {
|
||
const match = cleanDescription.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];
|
||
}
|
||
schedule = `${lengthValue}mm`;
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
// 다른 카테고리는 스케줄 추출
|
||
const scheduleMatch = cleanDescription.match(/SCH\s*(\d+[A-Z]*(?:\s*[xX×]\s*SCH\s*\d+[A-Z]*)?)/i);
|
||
if (scheduleMatch) {
|
||
schedule = scheduleMatch[0];
|
||
}
|
||
}
|
||
|
||
// 재질 추출 (카테고리별 처리)
|
||
let grade = '-';
|
||
|
||
if (category === 'GASKET') {
|
||
// 가스켓의 경우 재질 필드에는 H/F/I/O 타입 정보 표시
|
||
const hfioMatch = cleanDescription.match(/H\/F\/I\/O/i);
|
||
if (hfioMatch) {
|
||
grade = 'H/F/I/O';
|
||
} else {
|
||
// 다른 가스켓 타입들
|
||
if (cleanDescription.includes('SWG') || cleanDescription.includes('SPIRAL')) {
|
||
grade = 'SWG';
|
||
} else if (cleanDescription.includes('RTJ') || cleanDescription.includes('RING')) {
|
||
grade = 'RTJ';
|
||
} else if (cleanDescription.includes('FF') || cleanDescription.includes('FULL FACE')) {
|
||
grade = 'FF';
|
||
} else if (cleanDescription.includes('RF') || cleanDescription.includes('RAISED')) {
|
||
grade = 'RF';
|
||
} else {
|
||
grade = 'GASKET';
|
||
}
|
||
}
|
||
} else if (category === 'BOLT') {
|
||
// 볼트 전용 재질 추출 (복합 ASTM 패턴 지원)
|
||
const boltGradePatterns = [
|
||
// 복합 ASTM 패턴 (A193/A194 GR B7/2H)
|
||
/(ASTM\s+A\d+\/A\d+\s+GR\s+[A-Z0-9\/]+)/i,
|
||
// 단일 ASTM 패턴 (ASTM A193 GR B7)
|
||
/(ASTM\s+A\d+\s+GR\s+[A-Z0-9]+)/i,
|
||
// ASTM 번호만 (ASTM A193/A194)
|
||
/(ASTM\s+A\d+(?:\/A\d+)?)/i,
|
||
// 일반 ASTM 패턴
|
||
/(ASTM\s+[A-Z0-9\s\/]+(?:TP\d+|GR\s*[A-Z0-9\/]+|WP\d+)?)/i
|
||
];
|
||
|
||
for (const pattern of boltGradePatterns) {
|
||
const match = cleanDescription.match(pattern);
|
||
if (match) {
|
||
grade = match[1].trim();
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ASTM이 없는 경우 기본 재질 패턴 시도
|
||
if (grade === '-') {
|
||
const basicGradeMatch = cleanDescription.match(/(A\d+(?:\/A\d+)?\s+(?:GR\s+)?[A-Z0-9\/]+)/i);
|
||
if (basicGradeMatch) {
|
||
grade = basicGradeMatch[1].trim();
|
||
}
|
||
}
|
||
|
||
// 백엔드에서 제공하는 재질 정보 우선 사용
|
||
if (material.full_material_grade && material.full_material_grade !== '-') {
|
||
grade = material.full_material_grade;
|
||
} else if (material.material_grade && material.material_grade !== '-') {
|
||
grade = material.material_grade;
|
||
}
|
||
} else {
|
||
// 기존 ASTM 패턴 (다른 카테고리용)
|
||
const gradeMatch = cleanDescription.match(/(ASTM\s+[A-Z0-9\s]+(?:TP\d+|GR\s*[A-Z0-9]+|WP\d+)?)/i);
|
||
if (gradeMatch) {
|
||
grade = gradeMatch[1].trim();
|
||
}
|
||
}
|
||
|
||
// 카테고리별 상세 정보 추출 (이미 위에서 선언됨)
|
||
|
||
if (category === 'BOLT') {
|
||
// 볼트의 경우 표면처리 정보 추출
|
||
const surfaceTreatments = [];
|
||
|
||
// 원본 설명에서 표면처리 패턴 확인
|
||
const surfacePatterns = {
|
||
'ELEC.GALV': 'ELEC.GALV',
|
||
'ELEC GALV': 'ELEC.GALV',
|
||
'GALVANIZED': 'GALVANIZED',
|
||
'GALV': 'GALV',
|
||
'HOT DIP GALV': 'HDG',
|
||
'HDG': 'HDG',
|
||
'ZINC PLATED': 'ZINC PLATED',
|
||
'ZINC': 'ZINC',
|
||
'PLAIN': 'PLAIN'
|
||
};
|
||
|
||
for (const [pattern, treatment] of Object.entries(surfacePatterns)) {
|
||
if (cleanDescription.includes(pattern)) {
|
||
surfaceTreatments.push(treatment);
|
||
break; // 첫 번째 매치만 사용
|
||
}
|
||
}
|
||
|
||
detailInfo = surfaceTreatments.join(', ') || '-';
|
||
} else if (category === 'GASKET') {
|
||
// 실제 재질 구성 정보 (SS304/GRAPHITE/SS304/SS304)
|
||
if (material.gasket_details) {
|
||
const materialType = material.gasket_details.material_type || '';
|
||
const fillerMaterial = material.gasket_details.filler_material || '';
|
||
|
||
if (materialType && fillerMaterial) {
|
||
// DB에서 가져온 정보로 구성
|
||
gasketMaterial = `${materialType}/${fillerMaterial}`;
|
||
}
|
||
}
|
||
|
||
// gasket_details가 없거나 불완전하면 description에서 추출
|
||
if (!gasketMaterial) {
|
||
// SS304/GRAPHITE/SS304/SS304 패턴 추출
|
||
const fullMaterialMatch = cleanDescription.match(/SS304\/GRAPHITE\/SS304\/SS304/i);
|
||
if (fullMaterialMatch) {
|
||
gasketMaterial = 'SS304/GRAPHITE/SS304/SS304';
|
||
} else {
|
||
// 간단한 패턴 (SS304/GRAPHITE)
|
||
const simpleMaterialMatch = cleanDescription.match(/(SS\d+|304|316)\s*[\/+]\s*(GRAPHITE|PTFE|VITON|EPDM)/i);
|
||
if (simpleMaterialMatch) {
|
||
gasketMaterial = `${simpleMaterialMatch[1]}/${simpleMaterialMatch[2]}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 두께 정보 별도 추출
|
||
if (material.gasket_details && material.gasket_details.thickness) {
|
||
gasketThickness = `${material.gasket_details.thickness}mm`;
|
||
} else {
|
||
// description에서 두께 추출 (4.5mm 패턴)
|
||
const thicknessMatch = cleanDescription.match(/(\d+\.?\d*)\s*mm/i);
|
||
if (thicknessMatch) {
|
||
gasketThickness = `${thicknessMatch[1]}mm`;
|
||
}
|
||
}
|
||
|
||
// 기타 상세 정보 (Fire Safe 등)
|
||
const otherDetails = [];
|
||
if (material.gasket_details && material.gasket_details.fire_safe) {
|
||
otherDetails.push('Fire Safe');
|
||
}
|
||
|
||
detailInfo = otherDetails.join(', ');
|
||
}
|
||
|
||
// 수량 계산 (PIPE는 본 단위)
|
||
let quantity = purchaseInfo.purchaseQuantity || material.quantity || 0;
|
||
|
||
if (category === 'PIPE') {
|
||
// PIPE의 경우 본 단위로 계산
|
||
if (material.total_length) {
|
||
// 총 길이를 6000mm로 나누어 본 수 계산
|
||
quantity = Math.ceil(material.total_length / 6000);
|
||
} else if (material.pipe_details && material.pipe_details.total_length_mm) {
|
||
quantity = Math.ceil(material.pipe_details.total_length_mm / 6000);
|
||
} else if (material.pipe_count) {
|
||
quantity = material.pipe_count;
|
||
}
|
||
}
|
||
|
||
// 새로운 엑셀 양식: A~E 고정, F~O 카테고리별, P 납기일
|
||
const base = {
|
||
'TAGNO': '', // A열: 비워둠
|
||
'품목명': itemName, // B열: 카테고리별 상세 타입
|
||
'수량': quantity, // C열: 수량
|
||
'통화구분': 'KRW', // D열: 기본값
|
||
'단가': 1 // E열: 일괄 1로 설정
|
||
};
|
||
|
||
// F~O열: 카테고리별 전용 컬럼 구성 (10개 컬럼)
|
||
if (category === 'PIPE') {
|
||
// 파이프 전용 컬럼 (F~O) - 끝단처리, 압력등급 제거
|
||
base['크기'] = material.size_spec || '-'; // F열
|
||
base['스케줄'] = schedule; // G열
|
||
base['재질'] = grade; // H열
|
||
base['제조방법'] = material.pipe_details?.manufacturing_method || '-'; // I열
|
||
base['사용자요구'] = material.user_requirements?.join(', ') || ''; // J열 (분류기에서 추출)
|
||
base['추가요청사항'] = material.user_requirement || ''; // K열 (사용자 입력)
|
||
base['관리항목1'] = ''; // L열
|
||
base['관리항목2'] = ''; // M열
|
||
base['관리항목3'] = ''; // N열
|
||
base['관리항목4'] = ''; // O열
|
||
} else if (category === 'FITTING') {
|
||
// 피팅 전용 컬럼 (F~O) - 새로운 구조
|
||
base['크기'] = material.size_spec || '-'; // F열
|
||
base['압력등급'] = pressure; // G열
|
||
base['스케줄'] = schedule; // H열
|
||
base['재질'] = grade; // I열
|
||
base['사용자요구'] = material.user_requirements?.join(', ') || ''; // J열 (분류기에서 추출)
|
||
base['추가요청사항'] = material.user_requirement || ''; // K열 (사용자 입력)
|
||
base['관리항목1'] = ''; // L열
|
||
base['관리항목2'] = ''; // M열
|
||
base['관리항목3'] = ''; // N열
|
||
base['관리항목4'] = ''; // O열
|
||
} else if (category === 'FLANGE') {
|
||
// 플랜지 전용 컬럼 (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': 'FLAT FACE',
|
||
'FLAT_FACE': 'FLAT FACE',
|
||
'RTJ': 'RING TYPE JOINT',
|
||
'RING_TYPE_JOINT': 'RING TYPE JOINT'
|
||
};
|
||
base['페이싱'] = facingTypeMap[rawFacingType] || rawFacingType || '-'; // G열
|
||
|
||
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열
|
||
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열
|
||
} else if (category === 'GASKET') {
|
||
// 가스켓 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
||
base['크기'] = material.size_spec || '-'; // F열
|
||
base['압력등급'] = pressure; // G열
|
||
base['구조'] = grade; // H열: H/F/I/O, SWG 등 (타입 정보 제거)
|
||
base['재질'] = gasketMaterial || '-'; // I열: SS304/GRAPHITE/SS304/SS304
|
||
base['두께'] = gasketThickness || '-'; // J열: 4.5mm
|
||
base['사용자요구'] = material.user_requirement || ''; // K열
|
||
base['관리항목1'] = ''; // L열
|
||
base['관리항목2'] = ''; // M열
|
||
base['관리항목3'] = ''; // N열
|
||
base['관리항목4'] = ''; // O열
|
||
} else if (category === 'BOLT') {
|
||
// 볼트 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
||
base['크기'] = material.size_spec || '-'; // F열
|
||
base['압력등급'] = pressure; // G열
|
||
base['길이'] = schedule; // H열: 볼트는 길이 정보
|
||
base['재질'] = grade; // I열
|
||
base['추가요구'] = detailInfo || '-'; // J열: 표면처리 등
|
||
base['사용자요구'] = material.user_requirement || ''; // K열
|
||
base['관리항목1'] = ''; // L열
|
||
base['관리항목2'] = ''; // M열
|
||
base['관리항목3'] = ''; // N열
|
||
base['관리항목4'] = ''; // O열
|
||
} else {
|
||
// 기타 카테고리 기본 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
||
base['크기'] = material.size_spec || '-'; // F열
|
||
base['압력등급'] = pressure; // G열
|
||
base['스케줄'] = schedule; // H열
|
||
base['재질'] = grade; // I열
|
||
base['상세내역'] = detailInfo || '-'; // J열
|
||
base['사용자요구'] = material.user_requirement || ''; // K열
|
||
base['관리항목1'] = ''; // L열
|
||
base['관리항목2'] = ''; // M열
|
||
base['관리항목3'] = ''; // N열
|
||
base['관리항목4'] = ''; // O열
|
||
}
|
||
|
||
// P열: 납기일 (고정)
|
||
base['납기일(YYYY-MM-DD)'] = new Date().toISOString().split('T')[0];
|
||
|
||
// 비교 모드인 경우 추가 정보
|
||
if (includeComparison) {
|
||
if (material.previous_quantity !== undefined) {
|
||
const prevPurchaseInfo = calculatePurchaseQuantity({
|
||
...material,
|
||
quantity: material.previous_quantity,
|
||
totalLength: material.previousTotalLength || 0
|
||
});
|
||
|
||
base['이전수량'] = prevPurchaseInfo.purchaseQuantity || 0;
|
||
base['수량변경'] = (purchaseInfo.purchaseQuantity - prevPurchaseInfo.purchaseQuantity);
|
||
}
|
||
|
||
base['변경유형'] = material.change_type || (
|
||
material.previous_quantity !== undefined ? '수량 변경' :
|
||
material.quantity_change === undefined ? '신규' : '변경'
|
||
);
|
||
}
|
||
|
||
return base;
|
||
};
|
||
|
||
/**
|
||
* 일반 자재 목록 엑셀 내보내기
|
||
*/
|
||
export const exportMaterialsToExcel = (materials, filename, additionalInfo = {}) => {
|
||
try {
|
||
console.log('🔧 exportMaterialsToExcel 시작:', materials.length, '개 자재');
|
||
|
||
// 카테고리별로 그룹화
|
||
const categoryGroups = groupMaterialsByCategory(materials);
|
||
console.log('📁 카테고리별 그룹:', Object.keys(categoryGroups).map(k => `${k}: ${categoryGroups[k].length}개`));
|
||
|
||
// 전체 자재 합치기 (먼저 계산)
|
||
const consolidatedMaterials = consolidateMaterials(materials);
|
||
console.log('📦 합쳐진 자재:', consolidatedMaterials.length, '개');
|
||
|
||
// 새 워크북 생성
|
||
const workbook = XLSX.utils.book_new();
|
||
|
||
// 카테고리별 시트 생성 (합쳐진 자재)
|
||
Object.entries(categoryGroups).forEach(([category, items]) => {
|
||
console.log(`📄 ${category} 시트 생성 중... (${items.length}개 자재)`);
|
||
const consolidatedItems = consolidateMaterials(items);
|
||
console.log(` → 합쳐진 결과: ${consolidatedItems.length}개`);
|
||
|
||
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material));
|
||
console.log(` → 포맷 완료: ${formattedItems.length}개`);
|
||
|
||
if (formattedItems.length > 0) {
|
||
const categorySheet = XLSX.utils.json_to_sheet(formattedItems);
|
||
|
||
// 헤더 스타일링 (연하늘색 배경)
|
||
const range = XLSX.utils.decode_range(categorySheet['!ref']);
|
||
|
||
// 헤더 행에 스타일 적용 (첫 번째 행)
|
||
for (let col = range.s.c; col <= range.e.c; col++) {
|
||
const cellRef = XLSX.utils.encode_cell({ r: 0, c: col });
|
||
|
||
if (categorySheet[cellRef]) {
|
||
// 기존 셀 값 유지하면서 스타일만 추가
|
||
const cellValue = categorySheet[cellRef].v;
|
||
const cellType = categorySheet[cellRef].t;
|
||
|
||
categorySheet[cellRef] = {
|
||
v: cellValue,
|
||
t: cellType || 's',
|
||
s: {
|
||
fill: {
|
||
patternType: "solid",
|
||
fgColor: { rgb: "B3D9FF" }
|
||
},
|
||
font: {
|
||
bold: true,
|
||
color: { rgb: "000000" },
|
||
sz: 12,
|
||
name: "맑은 고딕"
|
||
},
|
||
alignment: {
|
||
horizontal: "center",
|
||
vertical: "center"
|
||
},
|
||
border: {
|
||
top: { style: "thin", color: { rgb: "666666" } },
|
||
bottom: { style: "thin", color: { rgb: "666666" } },
|
||
left: { style: "thin", color: { rgb: "666666" } },
|
||
right: { style: "thin", color: { rgb: "666666" } }
|
||
}
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
// 컬럼 너비 자동 조정
|
||
const colWidths = [];
|
||
if (formattedItems.length > 0) {
|
||
const headers = Object.keys(formattedItems[0]);
|
||
|
||
headers.forEach((header, index) => {
|
||
let maxWidth = header.length; // 헤더 길이
|
||
|
||
// 각 행의 데이터 길이 확인
|
||
formattedItems.forEach(item => {
|
||
const cellValue = String(item[header] || '');
|
||
maxWidth = Math.max(maxWidth, cellValue.length);
|
||
});
|
||
|
||
// 최소 10, 최대 50으로 제한
|
||
colWidths[index] = { wch: Math.min(Math.max(maxWidth + 2, 10), 50) };
|
||
});
|
||
}
|
||
|
||
categorySheet['!cols'] = colWidths;
|
||
|
||
// 시트명에서 특수문자 제거 (엑셀 시트명 규칙)
|
||
const sheetName = category.replace(/[\\\/\*\?\[\]]/g, '_').substring(0, 31);
|
||
XLSX.utils.book_append_sheet(workbook, categorySheet, sheetName);
|
||
}
|
||
});
|
||
|
||
// 워크북 속성 설정 (스타일 지원)
|
||
workbook.Props = {
|
||
Title: "자재 목록",
|
||
Subject: "TK-MP-Project 자재 관리",
|
||
Author: "TK-MP System",
|
||
CreatedDate: new Date()
|
||
};
|
||
|
||
// 파일 저장 (스타일 포함)
|
||
const excelBuffer = XLSX.write(workbook, {
|
||
bookType: 'xlsx',
|
||
type: 'array',
|
||
cellStyles: true // 스타일 활성화
|
||
});
|
||
const data = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||
|
||
const finalFilename = `${filename}_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||
saveAs(data, finalFilename);
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('엑셀 내보내기 실패:', error);
|
||
alert('엑셀 파일 생성에 실패했습니다: ' + error.message);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 리비전 비교 결과 엑셀 내보내기
|
||
*/
|
||
export const exportComparisonToExcel = (comparisonData, filename, additionalInfo = {}) => {
|
||
try {
|
||
const { summary, new_items = [], modified_items = [], removed_items = [] } = comparisonData;
|
||
|
||
// 새 워크북 생성
|
||
const workbook = XLSX.utils.book_new();
|
||
|
||
// 요약 시트
|
||
const summaryData = [
|
||
['리비전 비교 정보', ''],
|
||
['Job No', additionalInfo.jobNo || ''],
|
||
['현재 리비전', additionalInfo.currentRevision || ''],
|
||
['이전 리비전', additionalInfo.previousRevision || ''],
|
||
['비교일', new Date().toLocaleDateString()],
|
||
['', ''],
|
||
['비교 결과 요약', ''],
|
||
['구분', '건수'],
|
||
['총 현재 자재', summary?.total_current_items || 0],
|
||
['총 이전 자재', summary?.total_previous_items || 0],
|
||
['신규 자재', summary?.new_items_count || 0],
|
||
['변경 자재', summary?.modified_items_count || 0],
|
||
['삭제 자재', summary?.removed_items_count || 0]
|
||
];
|
||
|
||
const summarySheet = XLSX.utils.aoa_to_sheet(summaryData);
|
||
XLSX.utils.book_append_sheet(workbook, summarySheet, '비교 요약');
|
||
|
||
// 신규 자재 시트 (카테고리별, 합쳐진 자재)
|
||
if (new_items.length > 0) {
|
||
const newItemsGroups = groupMaterialsByCategory(new_items);
|
||
Object.entries(newItemsGroups).forEach(([category, items]) => {
|
||
const consolidatedItems = consolidateMaterials(items, true);
|
||
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material, true));
|
||
const sheet = XLSX.utils.json_to_sheet(formattedItems);
|
||
const sheetName = `신규_${category.replace(/[\\\/\*\?\[\]]/g, '_')}`.substring(0, 31);
|
||
XLSX.utils.book_append_sheet(workbook, sheet, sheetName);
|
||
});
|
||
}
|
||
|
||
// 변경 자재 시트 (카테고리별, 합쳐진 자재)
|
||
if (modified_items.length > 0) {
|
||
const modifiedItemsGroups = groupMaterialsByCategory(modified_items);
|
||
Object.entries(modifiedItemsGroups).forEach(([category, items]) => {
|
||
const consolidatedItems = consolidateMaterials(items, true);
|
||
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material, true));
|
||
const sheet = XLSX.utils.json_to_sheet(formattedItems);
|
||
const sheetName = `변경_${category.replace(/[\\\/\*\?\[\]]/g, '_')}`.substring(0, 31);
|
||
XLSX.utils.book_append_sheet(workbook, sheet, sheetName);
|
||
});
|
||
}
|
||
|
||
// 삭제 자재 시트 (카테고리별, 합쳐진 자재)
|
||
if (removed_items.length > 0) {
|
||
const removedItemsGroups = groupMaterialsByCategory(removed_items);
|
||
Object.entries(removedItemsGroups).forEach(([category, items]) => {
|
||
const consolidatedItems = consolidateMaterials(items, true);
|
||
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material, true));
|
||
const sheet = XLSX.utils.json_to_sheet(formattedItems);
|
||
const sheetName = `삭제_${category.replace(/[\\\/\*\?\[\]]/g, '_')}`.substring(0, 31);
|
||
XLSX.utils.book_append_sheet(workbook, sheet, sheetName);
|
||
});
|
||
}
|
||
|
||
// 파일 저장
|
||
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||
const data = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||
|
||
const finalFilename = `${filename}_비교결과_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||
saveAs(data, finalFilename);
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('엑셀 내보내기 실패:', error);
|
||
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;
|
||
}
|
||
};
|