🔧 재질 정보 표시 개선 및 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

@@ -233,4 +233,9 @@ export default SimpleDashboard;

View File

@@ -550,4 +550,9 @@

View File

@@ -283,4 +283,9 @@ export default NavigationBar;

View File

@@ -263,4 +263,9 @@

View File

@@ -187,4 +187,9 @@ export default NavigationMenu;

View File

@@ -95,4 +95,9 @@ export default RevisionUploadDialog;

View File

@@ -314,4 +314,9 @@ export default SimpleFileUpload;

View File

@@ -277,4 +277,9 @@ export default DashboardPage;

View File

@@ -232,4 +232,9 @@

View File

@@ -129,4 +129,9 @@ export default LoginPage;

View File

@@ -8,6 +8,7 @@
background: #f8f9fa;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
overflow-x: auto;
}
/* 헤더 */
@@ -259,11 +260,13 @@
.materials-grid {
background: white;
margin: 0;
min-width: 1500px;
overflow-x: auto;
}
.detailed-grid-header {
display: grid;
grid-template-columns: 40px 80px 120px 80px 80px 140px 100px 150px 100px;
grid-template-columns: 40px 80px 250px 80px 80px 350px 100px 150px 100px;
padding: 12px 24px;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
@@ -276,42 +279,42 @@
/* 플랜지 전용 헤더 - 10개 컬럼 */
.detailed-grid-header.flange-header {
grid-template-columns: 40px 80px 80px 80px 100px 80px 140px 100px 150px 100px;
grid-template-columns: 40px 80px 150px 80px 100px 80px 350px 100px 150px 100px;
}
/* 플랜지 전용 행 - 10개 컬럼 */
.detailed-material-row.flange-row {
grid-template-columns: 40px 80px 80px 80px 100px 80px 140px 100px 150px 100px;
grid-template-columns: 40px 80px 150px 80px 100px 80px 350px 100px 150px 100px;
}
/* 피팅 전용 헤더 - 10개 컬럼 */
.detailed-grid-header.fitting-header {
grid-template-columns: 40px 80px 150px 80px 80px 80px 140px 100px 150px 100px;
grid-template-columns: 40px 80px 280px 80px 80px 80px 350px 100px 150px 100px;
}
/* 피팅 전용 행 - 10개 컬럼 */
.detailed-material-row.fitting-row {
grid-template-columns: 40px 80px 150px 80px 80px 80px 140px 100px 150px 100px;
grid-template-columns: 40px 80px 280px 80px 80px 80px 350px 100px 150px 100px;
}
/* 밸브 전용 헤더 - 9개 컬럼 (스케줄 제거, 타입 너비 증가) */
.detailed-grid-header.valve-header {
grid-template-columns: 40px 120px 100px 80px 80px 140px 100px 150px 100px;
grid-template-columns: 40px 220px 100px 80px 80px 350px 100px 150px 100px;
}
/* 밸브 전용 행 - 9개 컬럼 (스케줄 제거, 타입 너비 증가) */
.detailed-material-row.valve-row {
grid-template-columns: 40px 120px 100px 80px 80px 140px 100px 150px 100px;
grid-template-columns: 40px 220px 100px 80px 80px 350px 100px 150px 100px;
}
/* 가스켓 전용 헤더 - 11개 컬럼 (타입 좁게, 상세내역 넓게, 두께 추가) */
.detailed-grid-header.gasket-header {
grid-template-columns: 40px 80px 60px 80px 80px 100px 180px 60px 80px 150px 100px;
grid-template-columns: 40px 80px 120px 80px 80px 100px 400px 60px 80px 150px 100px;
}
/* 가스켓 전용 행 - 11개 컬럼 (타입 좁게, 상세내역 넓게, 두께 추가) */
.detailed-material-row.gasket-row {
grid-template-columns: 40px 80px 60px 80px 80px 100px 180px 60px 80px 150px 100px;
grid-template-columns: 40px 80px 120px 80px 80px 100px 400px 60px 80px 150px 100px;
}
/* UNKNOWN 전용 헤더 - 5개 컬럼 */
@@ -326,21 +329,23 @@
/* UNKNOWN 설명 셀 스타일 */
.description-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: visible;
text-overflow: initial;
white-space: normal;
word-break: break-word;
}
.description-text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: visible;
text-overflow: initial;
white-space: normal;
word-break: break-word;
}
.detailed-material-row {
display: grid;
grid-template-columns: 40px 80px 120px 80px 80px 140px 100px 150px 100px;
grid-template-columns: 40px 80px 250px 80px 80px 350px 100px 150px 100px;
padding: 12px 24px;
border-bottom: 1px solid #f3f4f6;
align-items: center;
@@ -357,10 +362,13 @@
}
.material-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: visible !important;
text-overflow: initial !important;
white-space: normal !important;
padding-right: 12px;
word-break: break-word;
min-width: 120px;
max-width: none !important;
}
.material-cell input[type="checkbox"] {
@@ -431,6 +439,11 @@
.material-grade {
color: #1f2937;
font-weight: 500;
white-space: normal !important;
word-break: break-word !important;
overflow: visible !important;
text-overflow: initial !important;
min-width: 300px !important;
}
/* 입력 필드 */

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>

View File

@@ -401,4 +401,9 @@ export default ProjectsPage;

View File

@@ -443,4 +443,9 @@