+ {material.user_requirements?.join(', ') || '-'}
- {info.unit}
+
+ {info.quantity} {info.unit}
{
@@ -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();
diff --git a/frontend/src/components/bom/materials/PipeMaterialsView.jsx b/frontend/src/components/bom/materials/PipeMaterialsView.jsx
index 6400a43..c55d743 100644
--- a/frontend/src/components/bom/materials/PipeMaterialsView.jsx
+++ b/frontend/src/components/bom/materials/PipeMaterialsView.jsx
@@ -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();
diff --git a/frontend/src/components/bom/materials/ValveMaterialsView.jsx b/frontend/src/components/bom/materials/ValveMaterialsView.jsx
index 4d38b31..3d45109 100644
--- a/frontend/src/components/bom/materials/ValveMaterialsView.jsx
+++ b/frontend/src/components/bom/materials/ValveMaterialsView.jsx
@@ -10,7 +10,9 @@ const ValveMaterialsView = ({
userRequirements,
setUserRequirements,
purchasedMaterials,
+ onPurchasedMaterialsUpdate,
fileId,
+ jobNo,
user
}) => {
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
diff --git a/frontend/src/pages/BOMManagementPage.jsx b/frontend/src/pages/BOMManagementPage.jsx
index 012ba93..3c0d274 100644
--- a/frontend/src/pages/BOMManagementPage.jsx
+++ b/frontend/src/pages/BOMManagementPage.jsx
@@ -167,7 +167,16 @@ const BOMManagementPage = ({
userRequirements,
setUserRequirements,
purchasedMaterials,
+ onPurchasedMaterialsUpdate: (materialIds) => {
+ setPurchasedMaterials(prev => {
+ const newSet = new Set(prev);
+ materialIds.forEach(id => newSet.add(id));
+ console.log(`📦 구매신청 자재 추가: 기존 ${prev.size}개 → 신규 ${newSet.size}개`);
+ return newSet;
+ });
+ },
fileId,
+ jobNo,
user,
onNavigate
};
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx
index 58ff764..019c78e 100644
--- a/frontend/src/pages/DashboardPage.jsx
+++ b/frontend/src/pages/DashboardPage.jsx
@@ -43,9 +43,20 @@ const DashboardPage = ({
// 프로젝트 비활성화
const handleDeactivateProject = (project) => {
- if (window.confirm(`"${project.job_name || project.job_no}" 프로젝트를 비활성화하시겠습니까?`)) {
- setInactiveProjects(prev => new Set([...prev, project.job_no]));
- if (selectedProject?.job_no === project.job_no) {
+ const projectId = project.job_no || project.official_project_code || project.id;
+ const projectName = project.job_name || project.project_name || projectId;
+
+ console.log('🔍 비활성화 요청:', { project, projectId, projectName });
+
+ if (window.confirm(`"${projectName}" 프로젝트를 비활성화하시겠습니까?`)) {
+ setInactiveProjects(prev => {
+ const newSet = new Set([...prev, projectId]);
+ console.log('📦 비활성화 프로젝트 업데이트:', { prev: Array.from(prev), new: Array.from(newSet) });
+ return newSet;
+ });
+
+ const selectedProjectId = selectedProject?.job_no || selectedProject?.official_project_code || selectedProject?.id;
+ if (selectedProjectId === projectId) {
setSelectedProject(null);
}
setShowProjectDropdown(false);
@@ -323,7 +334,10 @@ const DashboardPage = ({
) : (
projects
- .filter(project => !inactiveProjects.has(project.job_no))
+ .filter(project => {
+ const projectId = project.job_no || project.official_project_code || project.id;
+ return !inactiveProjects.has(projectId);
+ })
.map((project) => (
{
}
}
} else if (category === 'FITTING') {
- // 피팅 상세 타입 표시 (OLET 등 풀네임)
+ // 피팅 상세 타입 표시 - 프론트엔드 표시와 동일한 로직 사용
const fittingDetails = material.fitting_details || {};
- const fittingType = fittingDetails.fitting_type || '';
- const fittingSubtype = fittingDetails.fitting_subtype || '';
+ 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':
- itemName = 'SOCK-O-LET';
+ displayType = 'SOCK-O-LET';
break;
case 'WELDOLET':
- itemName = 'WELD-O-LET';
+ displayType = 'WELD-O-LET';
break;
case 'ELLOLET':
- itemName = 'ELL-O-LET';
+ displayType = 'ELL-O-LET';
break;
case 'THREADOLET':
- itemName = 'THREAD-O-LET';
+ displayType = 'THREAD-O-LET';
break;
case 'ELBOLET':
- itemName = 'ELB-O-LET';
+ displayType = 'ELB-O-LET';
break;
case 'NIPOLET':
- itemName = 'NIP-O-LET';
+ displayType = 'NIP-O-LET';
break;
case 'COUPOLET':
- itemName = 'COUP-O-LET';
+ displayType = 'COUP-O-LET';
break;
default:
- itemName = 'OLET';
+ // 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도');
+ elbowDetails.push('90°');
} else if (fittingSubtype.includes('45DEG') || cleanDescription.includes('45')) {
- elbowDetails.push('45도');
- } else {
- elbowDetails.push('90도'); // 기본값
+ elbowDetails.push('45°');
}
// 반경 정보
@@ -232,25 +295,12 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
elbowDetails.push('SR');
}
- // 연결 방식
- if (cleanDescription.includes('SW')) {
- elbowDetails.push('SW');
- } else if (cleanDescription.includes('BW')) {
- elbowDetails.push('BW');
- }
-
- itemName = `엘보 ${elbowDetails.join(' ')}`.trim();
- } else if (fittingType === 'TEE') {
- // 티 타입 표시
- const teeType = fittingSubtype === 'EQUAL' ? '등경' : fittingSubtype === 'REDUCING' ? '축소' : '';
- itemName = `티 ${teeType}`.trim();
- } else if (fittingType === 'REDUCER') {
- // 리듀서 타입 표시
- const reducerType = fittingSubtype === 'CONCENTRIC' ? '동심' : fittingSubtype === 'ECCENTRIC' ? '편심' : '';
- itemName = `리듀서 ${reducerType}`.trim();
+ displayType = elbowDetails.length > 0 ? `ELBOW ${elbowDetails.join(' ')}` : 'ELBOW';
} else {
- itemName = fittingType || 'FITTING';
+ displayType = fittingType || 'FITTING';
}
+
+ itemName = displayType;
} else if (category === 'FLANGE') {
// 플랜지 상세 타입 표시
const flangeDetails = material.flange_details || {};
@@ -674,17 +724,17 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
base['관리항목3'] = ''; // N열
base['관리항목4'] = ''; // O열
} else if (category === 'FITTING') {
- // 피팅 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
+ // 피팅 전용 컬럼 (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열
+ 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') {
// 플랜지 타입 풀네임 매핑 (영어)
const flangeTypeMap = {