feat: 피팅류 엑셀 내보내기 개선 및 프로젝트 비활성화 버그 수정
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
✨ 피팅류 엑셀 내보내기 개선: - 품목명에 상세 피팅 타입 표시 (SOCK-O-LET, ELBOW 90° LR 등) - G열부터 압력등급/스케줄/재질/사용자요구/추가요청사항 체계적 배치 - 분류기 추출 요구사항(J열)과 사용자 입력 요구사항(K열) 분리 - P열 납기일 고정 규칙 유지, 관리항목 자동 채움 🐛 프로젝트 비활성화 버그 수정: - 백엔드: job_no 필드 추가로 프론트엔드 호환성 확보 - 프론트엔드: 안전한 프로젝트 식별자 처리 로직 구현 - 개별 프로젝트 비활성화 시 전체 프로젝트 영향 문제 해결 - 디버깅 로그 추가로 상태 변경 추적 가능 🔧 기타 개선사항: - BOM 페이지 이모지 제거 - 구매신청 후 자재 비활성화 기능 구현 - 모든 카테고리 뷰에 onPurchasedMaterialsUpdate 콜백 추가
This commit is contained in:
@@ -10,7 +10,9 @@ const FittingMaterialsView = ({
|
||||
userRequirements,
|
||||
setUserRequirements,
|
||||
purchasedMaterials,
|
||||
onPurchasedMaterialsUpdate,
|
||||
fileId,
|
||||
jobNo,
|
||||
user,
|
||||
onNavigate
|
||||
}) => {
|
||||
@@ -245,16 +247,73 @@ const FittingMaterialsView = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 스케줄 표시 (분리 스케줄 지원)
|
||||
// 스케줄 표시 (분리 스케줄 지원) - 개선된 로직
|
||||
// 레듀싱 자재인지 확인
|
||||
const isReducingFitting = displayType.includes('REDUCING') || displayType.includes('RED') ||
|
||||
description.toUpperCase().includes('RED') ||
|
||||
description.toUpperCase().includes('REDUCING');
|
||||
|
||||
if (hasDifferentSchedules && mainSchedule && redSchedule) {
|
||||
schedule = `${mainSchedule}×${redSchedule}`;
|
||||
} else if (mainSchedule) {
|
||||
} else if (isReducingFitting && mainSchedule && mainSchedule !== 'UNKNOWN') {
|
||||
// 레듀싱 자재는 같은 스케줄이라도 명시적으로 표시
|
||||
schedule = `${mainSchedule}×${mainSchedule}`;
|
||||
} else if (mainSchedule && mainSchedule !== 'UNKNOWN') {
|
||||
schedule = mainSchedule;
|
||||
} else {
|
||||
// Description에서 스케줄 추출
|
||||
const scheduleMatch = description.match(/SCH\s*(\d+[A-Z]*)/i);
|
||||
if (scheduleMatch) {
|
||||
schedule = `SCH ${scheduleMatch[1]}`;
|
||||
// Description에서 스케줄 추출 - 더 강력한 패턴 매칭
|
||||
const schedulePatterns = [
|
||||
/SCH\s*(\d+S?)/i, // SCH 40, SCH 80S
|
||||
/SCHEDULE\s*(\d+S?)/i, // SCHEDULE 40
|
||||
/스케줄\s*(\d+S?)/i, // 스케줄 40
|
||||
/(\d+S?)\s*SCH/i, // 40 SCH (역순)
|
||||
/SCH\.?\s*(\d+S?)/i, // SCH.40
|
||||
/SCH\s*(\d+S?)\s*[xX×]\s*SCH\s*(\d+S?)/i // SCH 40 x SCH 80
|
||||
];
|
||||
|
||||
for (const pattern of schedulePatterns) {
|
||||
const match = description.match(pattern);
|
||||
if (match) {
|
||||
if (match.length > 2) {
|
||||
// 분리 스케줄 패턴 (SCH 40 x SCH 80)
|
||||
schedule = `SCH ${match[1]}×SCH ${match[2]}`;
|
||||
} else {
|
||||
const scheduleNum = match[1];
|
||||
if (isReducingFitting) {
|
||||
// 레듀싱 자재는 같은 스케줄이라도 명시적으로 표시
|
||||
schedule = `SCH ${scheduleNum}×SCH ${scheduleNum}`;
|
||||
} else {
|
||||
schedule = `SCH ${scheduleNum}`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 여전히 찾지 못했다면 더 넓은 패턴 시도
|
||||
if (schedule === '-') {
|
||||
const broadPatterns = [
|
||||
/\b(\d+)\s*LB/i, // 압력 등급에서 유추
|
||||
/\b(40|80|120|160)\b/i, // 일반적인 스케줄 숫자
|
||||
/\b(10|20|30|40|60|80|100|120|140|160)\b/i // 모든 표준 스케줄
|
||||
];
|
||||
|
||||
for (const pattern of broadPatterns) {
|
||||
const match = description.match(pattern);
|
||||
if (match) {
|
||||
const num = match[1];
|
||||
// 압력 등급이 아닌 경우만 스케줄로 간주
|
||||
if (!description.includes(`${num}LB`)) {
|
||||
if (isReducingFitting) {
|
||||
// 레듀싱 자재는 같은 스케줄이라도 명시적으로 표시
|
||||
schedule = `SCH ${num}×SCH ${num}`;
|
||||
} else {
|
||||
schedule = `SCH ${num}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +412,36 @@ const FittingMaterialsView = ({
|
||||
}));
|
||||
|
||||
try {
|
||||
// 1. 구매신청 생성
|
||||
const allMaterialIds = selectedMaterialsData.map(m => m.id);
|
||||
const response = await api.post('/purchase-request/create', {
|
||||
file_id: fileId,
|
||||
job_no: jobNo,
|
||||
category: 'FITTING',
|
||||
material_ids: allMaterialIds,
|
||||
materials_data: dataWithRequirements.map(m => ({
|
||||
material_id: m.id,
|
||||
description: m.original_description,
|
||||
category: m.classified_category,
|
||||
size: m.size_inch || m.size_spec,
|
||||
schedule: m.schedule,
|
||||
material_grade: m.material_grade || m.full_material_grade,
|
||||
quantity: m.quantity,
|
||||
unit: m.unit,
|
||||
user_requirement: userRequirements[m.id] || ''
|
||||
}))
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log(`✅ 구매신청 완료: ${response.data.request_no}`);
|
||||
|
||||
// 2. 구매신청된 자재 ID를 purchasedMaterials에 추가
|
||||
if (onPurchasedMaterialsUpdate) {
|
||||
onPurchasedMaterialsUpdate(allMaterialIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 서버에 엑셀 파일 저장 요청
|
||||
await api.post('/files/save-excel', {
|
||||
file_id: fileId,
|
||||
category: 'FITTING',
|
||||
@@ -361,21 +450,27 @@ const FittingMaterialsView = ({
|
||||
user_id: user?.id
|
||||
});
|
||||
|
||||
// 4. 클라이언트에서 다운로드
|
||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
||||
category: 'FITTING',
|
||||
filename: excelFileName,
|
||||
uploadDate: new Date().toLocaleDateString()
|
||||
});
|
||||
|
||||
alert('엑셀 파일이 생성되고 서버에 저장되었습니다.');
|
||||
alert(`구매신청 ${response.data?.request_no || ''}이 생성되고 엑셀 파일이 저장되었습니다.`);
|
||||
} catch (error) {
|
||||
console.error('엑셀 저장 실패:', error);
|
||||
console.error('엑셀 저장 또는 구매신청 실패:', error);
|
||||
// 실패해도 다운로드는 진행
|
||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
||||
category: 'FITTING',
|
||||
filename: excelFileName,
|
||||
uploadDate: new Date().toLocaleDateString()
|
||||
});
|
||||
alert('엑셀 파일은 다운로드되었지만 구매신청 생성에 실패했습니다.');
|
||||
}
|
||||
|
||||
// 선택 해제
|
||||
setSelectedMaterials(new Set());
|
||||
};
|
||||
|
||||
const filteredMaterials = getFilteredAndSortedMaterials();
|
||||
@@ -506,21 +601,24 @@ const FittingMaterialsView = ({
|
||||
<div style={{
|
||||
background: 'white',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
overflow: 'auto',
|
||||
maxHeight: '600px',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
{/* 헤더 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 80px 80px 200px',
|
||||
gap: '16px',
|
||||
padding: '16px',
|
||||
background: '#f8fafc',
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: '#374151'
|
||||
}}>
|
||||
<div style={{ minWidth: '1380px' }}>
|
||||
{/* 헤더 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 120px 200px',
|
||||
gap: '16px',
|
||||
padding: '16px',
|
||||
background: '#f8fafc',
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -537,13 +635,12 @@ const FittingMaterialsView = ({
|
||||
<div>Pressure</div>
|
||||
<div>Schedule</div>
|
||||
<div>Material Grade</div>
|
||||
<div>Quantity</div>
|
||||
<div>Unit</div>
|
||||
<div>User Requirement</div>
|
||||
<div>User Requirements</div>
|
||||
<div>Purchase Quantity</div>
|
||||
<div>Additional Request</div>
|
||||
</div>
|
||||
|
||||
{/* 데이터 행들 */}
|
||||
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
||||
{/* 데이터 행들 */}
|
||||
{filteredMaterials.map((material, index) => {
|
||||
const info = parseFittingInfo(material);
|
||||
const isSelected = selectedMaterials.has(material.id);
|
||||
@@ -554,12 +651,13 @@ const FittingMaterialsView = ({
|
||||
key={material.id}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 80px 80px 200px',
|
||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 120px 200px',
|
||||
gap: '16px',
|
||||
padding: '16px',
|
||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||
background: isSelected ? '#eff6ff' : (isPurchased ? '#fef3c7' : 'white'),
|
||||
transition: 'background 0.15s ease'
|
||||
transition: 'background 0.15s ease',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isSelected && !isPurchased) {
|
||||
@@ -612,11 +710,17 @@ const FittingMaterialsView = ({
|
||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
||||
{info.grade}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
|
||||
{info.quantity}
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#1f2937',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
{material.user_requirements?.join(', ') || '-'}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#6b7280' }}>
|
||||
{info.unit}
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
|
||||
{info.quantity} {info.unit}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
@@ -626,7 +730,7 @@ const FittingMaterialsView = ({
|
||||
...userRequirements,
|
||||
[material.id]: e.target.value
|
||||
})}
|
||||
placeholder="Enter requirement..."
|
||||
placeholder="Enter additional request..."
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
|
||||
@@ -10,7 +10,9 @@ const FlangeMaterialsView = ({
|
||||
userRequirements,
|
||||
setUserRequirements,
|
||||
purchasedMaterials,
|
||||
onPurchasedMaterialsUpdate,
|
||||
fileId,
|
||||
jobNo,
|
||||
user,
|
||||
onNavigate
|
||||
}) => {
|
||||
@@ -199,6 +201,34 @@ const FlangeMaterialsView = ({
|
||||
}));
|
||||
|
||||
try {
|
||||
// 1. 구매신청 생성
|
||||
const allMaterialIds = selectedMaterialsData.map(m => m.id);
|
||||
const response = await api.post('/purchase-request/create', {
|
||||
file_id: fileId,
|
||||
job_no: jobNo,
|
||||
category: 'FLANGE',
|
||||
material_ids: allMaterialIds,
|
||||
materials_data: dataWithRequirements.map(m => ({
|
||||
material_id: m.id,
|
||||
description: m.original_description,
|
||||
category: m.classified_category,
|
||||
size: m.size_inch || m.size_spec,
|
||||
schedule: m.schedule,
|
||||
material_grade: m.material_grade || m.full_material_grade,
|
||||
quantity: m.quantity,
|
||||
unit: m.unit,
|
||||
user_requirement: userRequirements[m.id] || ''
|
||||
}))
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log(`✅ 구매신청 완료: ${response.data.request_no}`);
|
||||
if (onPurchasedMaterialsUpdate) {
|
||||
onPurchasedMaterialsUpdate(allMaterialIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 서버에 엑셀 파일 저장
|
||||
await api.post('/files/save-excel', {
|
||||
file_id: fileId,
|
||||
category: 'FLANGE',
|
||||
@@ -207,21 +237,25 @@ const FlangeMaterialsView = ({
|
||||
user_id: user?.id
|
||||
});
|
||||
|
||||
// 3. 클라이언트 다운로드
|
||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
||||
category: 'FLANGE',
|
||||
filename: excelFileName,
|
||||
uploadDate: new Date().toLocaleDateString()
|
||||
});
|
||||
|
||||
alert('엑셀 파일이 생성되고 서버에 저장되었습니다.');
|
||||
alert(`구매신청 ${response.data?.request_no || ''}이 생성되고 엑셀 파일이 저장되었습니다.`);
|
||||
} catch (error) {
|
||||
console.error('엑셀 저장 실패:', error);
|
||||
console.error('엑셀 저장 또는 구매신청 실패:', error);
|
||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
||||
category: 'FLANGE',
|
||||
filename: excelFileName,
|
||||
uploadDate: new Date().toLocaleDateString()
|
||||
});
|
||||
alert('엑셀 파일은 다운로드되었지만 구매신청 생성에 실패했습니다.');
|
||||
}
|
||||
|
||||
setSelectedMaterials(new Set());
|
||||
};
|
||||
|
||||
const filteredMaterials = getFilteredAndSortedMaterials();
|
||||
|
||||
@@ -10,7 +10,9 @@ const PipeMaterialsView = ({
|
||||
userRequirements,
|
||||
setUserRequirements,
|
||||
purchasedMaterials,
|
||||
onPurchasedMaterialsUpdate,
|
||||
fileId,
|
||||
jobNo,
|
||||
user,
|
||||
onNavigate
|
||||
}) => {
|
||||
@@ -177,7 +179,36 @@ const PipeMaterialsView = ({
|
||||
}));
|
||||
|
||||
try {
|
||||
// 서버에 엑셀 파일 저장 요청
|
||||
// 1. 구매신청 생성
|
||||
const allMaterialIds = selectedMaterialsData.map(m => m.id);
|
||||
const response = await api.post('/purchase-request/create', {
|
||||
file_id: fileId,
|
||||
job_no: jobNo,
|
||||
category: 'PIPE',
|
||||
material_ids: allMaterialIds,
|
||||
materials_data: dataWithRequirements.map(m => ({
|
||||
material_id: m.id,
|
||||
description: m.original_description,
|
||||
category: m.classified_category,
|
||||
size: m.size_inch || m.size_spec,
|
||||
schedule: m.schedule,
|
||||
material_grade: m.material_grade || m.full_material_grade,
|
||||
quantity: m.quantity,
|
||||
unit: m.unit,
|
||||
user_requirement: userRequirements[m.id] || ''
|
||||
}))
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log(`✅ 구매신청 완료: ${response.data.request_no}`);
|
||||
|
||||
// 2. 구매신청된 자재 ID를 purchasedMaterials에 추가
|
||||
if (onPurchasedMaterialsUpdate) {
|
||||
onPurchasedMaterialsUpdate(allMaterialIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 서버에 엑셀 파일 저장 요청
|
||||
await api.post('/files/save-excel', {
|
||||
file_id: fileId,
|
||||
category: 'PIPE',
|
||||
@@ -186,23 +217,27 @@ const PipeMaterialsView = ({
|
||||
user_id: user?.id
|
||||
});
|
||||
|
||||
// 클라이언트에서 다운로드
|
||||
// 4. 클라이언트에서 다운로드
|
||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
||||
category: 'PIPE',
|
||||
filename: excelFileName,
|
||||
uploadDate: new Date().toLocaleDateString()
|
||||
});
|
||||
|
||||
alert('엑셀 파일이 생성되고 서버에 저장되었습니다.');
|
||||
alert(`구매신청 ${response.data?.request_no || ''}이 생성되고 엑셀 파일이 저장되었습니다.`);
|
||||
} catch (error) {
|
||||
console.error('엑셀 저장 실패:', error);
|
||||
console.error('엑셀 저장 또는 구매신청 실패:', error);
|
||||
// 실패해도 다운로드는 진행
|
||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
||||
category: 'PIPE',
|
||||
filename: excelFileName,
|
||||
uploadDate: new Date().toLocaleDateString()
|
||||
});
|
||||
alert('엑셀 파일은 다운로드되었지만 구매신청 생성에 실패했습니다.');
|
||||
}
|
||||
|
||||
// 선택 해제
|
||||
setSelectedMaterials(new Set());
|
||||
};
|
||||
|
||||
const filteredMaterials = getFilteredAndSortedMaterials();
|
||||
|
||||
@@ -10,7 +10,9 @@ const ValveMaterialsView = ({
|
||||
userRequirements,
|
||||
setUserRequirements,
|
||||
purchasedMaterials,
|
||||
onPurchasedMaterialsUpdate,
|
||||
fileId,
|
||||
jobNo,
|
||||
user
|
||||
}) => {
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
||||
|
||||
Reference in New Issue
Block a user