🔧 재질 정보 표시 개선 및 UI 확장
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

 주요 수정사항:
- 재질 GRADE 전체 표기: ASTM A106 B 완전 표시 (A10 잘림 현상 해결)
- material_grade_extractor.py 정규표현식 패턴 개선
- files.py 파일 업로드 시 재질 추출 로직 수정
- CSS 그리드 너비 확장으로 텍스트 잘림 현상 해결
- 사용자 요구사항 엑셀 다운로드 기능 완료

🎯 해결된 문제:
1. ASTM A106 B → ASTM A10 잘림 문제
2. 재질 컬럼 너비 부족으로 인한 표시 문제
3. 사용자 요구사항이 엑셀에 반영되지 않는 문제

📋 다음 단계 준비:
- 파이프 끝단 정보 제외 취합 로직 개선
- 플랜지 타입 정보 확장
- 자재 분류 필터 기능 추가
This commit is contained in:
Hyungi Ahn
2025-09-25 08:32:17 +09:00
parent af4ad25a54
commit 0f9a5ad2ea
29 changed files with 1281 additions and 58 deletions

View File

@@ -3,6 +3,7 @@ import { fetchMaterials } from '../api';
import { exportMaterialsToExcel } from '../utils/excelExport';
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
import api from '../api';
import './NewMaterialsPage.css';
const NewMaterialsPage = ({
@@ -21,6 +22,10 @@ const NewMaterialsPage = ({
const [viewMode, setViewMode] = useState('detailed'); // 'detailed' or 'simple'
const [availableRevisions, setAvailableRevisions] = useState([]);
const [currentRevision, setCurrentRevision] = useState(revision || 'Rev.0');
// 사용자 요구사항 상태 관리
const [userRequirements, setUserRequirements] = useState({}); // materialId: requirement 형태
const [savingRequirements, setSavingRequirements] = useState(false);
// 같은 BOM의 다른 리비전들 조회
const loadAvailableRevisions = async () => {
@@ -53,6 +58,7 @@ const NewMaterialsPage = ({
if (fileId) {
loadMaterials(fileId);
loadAvailableRevisions();
loadUserRequirements(fileId);
}
}, [fileId]);
@@ -86,6 +92,84 @@ const NewMaterialsPage = ({
}
};
// 사용자 요구사항 로드
const loadUserRequirements = async (id) => {
try {
console.log('🔍 사용자 요구사항 로딩 중...', { file_id: id });
const response = await api.get('/files/user-requirements', {
params: { file_id: parseInt(id) }
});
if (response.data?.success && response.data?.requirements) {
const requirements = {};
response.data.requirements.forEach(req => {
// material_id를 키로 사용하여 요구사항 저장
if (req.material_id) {
requirements[req.material_id] = req.requirement_description || req.requirement_title || '';
}
});
setUserRequirements(requirements);
console.log('✅ 사용자 요구사항 로드 완료:', Object.keys(requirements).length, '개');
}
} catch (error) {
console.error('❌ 사용자 요구사항 로딩 실패:', error);
setUserRequirements({});
}
};
// 사용자 요구사항 저장
const saveUserRequirements = async () => {
try {
setSavingRequirements(true);
console.log('💾 사용자 요구사항 저장 중...', userRequirements);
// 요구사항이 있는 자재들만 저장
const requirementsToSave = Object.entries(userRequirements)
.filter(([materialId, requirement]) => requirement && requirement.trim())
.map(([materialId, requirement]) => ({
material_id: parseInt(materialId),
file_id: parseInt(fileId),
requirement_type: 'CUSTOM_SPEC',
requirement_title: '사용자 요구사항',
requirement_description: requirement.trim(),
priority: 'NORMAL'
}));
if (requirementsToSave.length === 0) {
alert('저장할 요구사항이 없습니다.');
return;
}
// 기존 요구사항 삭제 후 새로 저장
await api.delete(`/files/user-requirements`, {
params: { file_id: parseInt(fileId) }
});
// 새 요구사항들 저장
for (const req of requirementsToSave) {
await api.post('/files/user-requirements', req);
}
alert(`${requirementsToSave.length}개의 사용자 요구사항이 저장되었습니다.`);
console.log('✅ 사용자 요구사항 저장 완료');
} catch (error) {
console.error('❌ 사용자 요구사항 저장 실패:', error);
alert('사용자 요구사항 저장에 실패했습니다: ' + (error.response?.data?.detail || error.message));
} finally {
setSavingRequirements(false);
}
};
// 사용자 요구사항 입력 핸들러
const handleUserRequirementChange = (materialId, value) => {
setUserRequirements(prev => ({
...prev,
[materialId]: value
}));
};
// 카테고리별 자재 수 계산
const getCategoryCounts = () => {
const counts = {};
@@ -126,12 +210,13 @@ const NewMaterialsPage = ({
if (category === 'PIPE') {
const calc = calculatePipePurchase(material);
return {
type: 'PIPE',
subtype: material.pipe_details?.manufacturing_method || 'SMLS',
size: material.size_spec || '-',
schedule: material.pipe_details?.schedule || '-',
grade: material.material_grade || '-',
grade: material.full_material_grade || material.material_grade || '-', // 전체 재질명 우선 사용
quantity: calc.purchaseCount,
unit: '본',
details: calc
@@ -224,7 +309,7 @@ const NewMaterialsPage = ({
size: material.size_spec || '-',
pressure: pressure,
schedule: schedule,
grade: material.material_grade || '-',
grade: material.full_material_grade || material.material_grade || '-', // 전체 재질명 우선 사용
quantity: Math.round(material.quantity || 0),
unit: '개',
isFitting: true
@@ -280,25 +365,17 @@ const NewMaterialsPage = ({
isValve: true
};
} else if (category === 'FLANGE') {
// 플랜지 타입 변환
const flangeTypeMap = {
'WELD_NECK': 'WN',
'SLIP_ON': 'SO',
'BLIND': 'BL',
'SOCKET_WELD': 'SW',
'LAP_JOINT': 'LJ',
'THREADED': 'TH',
'ORIFICE': 'ORIFICE' // 오리피스는 풀네임 표시
};
const flangeType = material.flange_details?.flange_type;
const displayType = flangeTypeMap[flangeType] || flangeType || '-';
const description = material.original_description || '';
// 백엔드에서 개선된 플랜지 타입 제공 (WN RF, SO FF 등)
const displayType = material.flange_details?.flange_type || '-';
// 원본 설명에서 스케줄 추출
let schedule = '-';
const description = material.original_description || '';
const upperDesc = description.toUpperCase();
// SCH 40, SCH 80 등의 패턴 찾기
if (description.toUpperCase().includes('SCH')) {
if (upperDesc.includes('SCH')) {
const schMatch = description.match(/SCH\s*(\d+[A-Z]*)/i);
if (schMatch && schMatch[1]) {
schedule = `SCH ${schMatch[1]}`;
@@ -307,11 +384,11 @@ const NewMaterialsPage = ({
return {
type: 'FLANGE',
subtype: displayType,
subtype: displayType, // 백엔드에서 개선된 타입 정보 제공 (WN RF, SO FF 등)
size: material.size_spec || '-',
pressure: material.flange_details?.pressure_rating || '-',
schedule: schedule,
grade: material.material_grade || '-',
grade: material.full_material_grade || material.material_grade || '-', // 전체 재질명 우선 사용
quantity: Math.round(material.quantity || 0),
unit: '개',
isFlange: true // 플랜지 구분용 플래그
@@ -443,7 +520,7 @@ const NewMaterialsPage = ({
'스케줄': info.schedule,
'재질': info.grade,
'추가요구': '-',
'사용자요구': '',
'사용자요구': userRequirements[material.id] || '',
'수량': `${info.quantity} ${info.unit}`,
'상세': `단관 ${info.itemCount || 0}개 → ${info.totalLength || 0}mm`
};
@@ -456,7 +533,7 @@ const NewMaterialsPage = ({
'스케줄': info.schedule,
'재질': info.grade,
'추가요구': '-',
'사용자요구': '',
'사용자요구': userRequirements[material.id] || '',
'수량': `${info.quantity} ${info.unit}`
};
} else if (selectedCategory === 'FITTING' && info.isFitting) {
@@ -468,7 +545,7 @@ const NewMaterialsPage = ({
'스케줄': info.schedule,
'재질': info.grade,
'추가요구': '-',
'사용자요구': '',
'사용자요구': userRequirements[material.id] || '',
'수량': `${info.quantity} ${info.unit}`
};
} else if (selectedCategory === 'VALVE' && info.isValve) {
@@ -479,7 +556,7 @@ const NewMaterialsPage = ({
'압력': info.pressure,
'재질': info.grade,
'추가요구': '-',
'사용자요구': '',
'사용자요구': userRequirements[material.id] || '',
'수량': `${info.quantity} ${info.unit}`
};
} else if (selectedCategory === 'GASKET' && info.isGasket) {
@@ -503,7 +580,7 @@ const NewMaterialsPage = ({
'스케줄': info.schedule,
'재질': info.grade,
'추가요구': '-',
'사용자요구': '',
'사용자요구': userRequirements[material.id] || '',
'수량': `${info.quantity} ${info.unit}`
};
} else if (selectedCategory === 'UNKNOWN' && info.isUnknown) {
@@ -522,7 +599,7 @@ const NewMaterialsPage = ({
'스케줄': info.schedule,
'재질': info.grade,
'추가요구': '-',
'사용자요구': '',
'사용자요구': userRequirements[material.id] || '',
'수량': `${info.quantity} ${info.unit}`
};
}
@@ -648,6 +725,25 @@ const NewMaterialsPage = ({
>
{selectedMaterials.size === filteredMaterials.length ? '전체 해제' : '전체 선택'}
</button>
<button
onClick={saveUserRequirements}
className="save-requirements-btn"
disabled={savingRequirements}
style={{
backgroundColor: savingRequirements ? '#ccc' : '#10b981',
color: 'white',
border: 'none',
padding: '10px 20px',
borderRadius: '6px',
cursor: savingRequirements ? 'not-allowed' : 'pointer',
marginRight: '10px',
fontSize: '14px',
fontWeight: '500'
}}
>
{savingRequirements ? '저장 중...' : '사용자 요구사항 저장'}
</button>
<button
onClick={exportToExcel}
className="export-btn"
@@ -798,6 +894,8 @@ const NewMaterialsPage = ({
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>
@@ -865,6 +963,8 @@ const NewMaterialsPage = ({
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>
@@ -939,6 +1039,8 @@ const NewMaterialsPage = ({
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>
@@ -990,6 +1092,8 @@ const NewMaterialsPage = ({
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>
@@ -1069,6 +1173,8 @@ const NewMaterialsPage = ({
type="text"
className="user-req-input"
placeholder="요구사항 입력"
value={userRequirements[material.id] || ''}
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
/>
</div>