🔧 재질 정보 표시 개선 및 UI 확장
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
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:
@@ -233,4 +233,9 @@ export default SimpleDashboard;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -550,4 +550,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -283,4 +283,9 @@ export default NavigationBar;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -263,4 +263,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -187,4 +187,9 @@ export default NavigationMenu;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -95,4 +95,9 @@ export default RevisionUploadDialog;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -314,4 +314,9 @@ export default SimpleFileUpload;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -277,4 +277,9 @@ export default DashboardPage;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -232,4 +232,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -129,4 +129,9 @@ export default LoginPage;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* 입력 필드 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -401,4 +401,9 @@ export default ProjectsPage;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -443,4 +443,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user