볼트 분류 개선 및 업로드 성능 최적화

- 볼트 길이 추출 로직 개선: '70.0000 LG' 형태 인식 추가
- 재질 중복 표시 수정: 'ASTM A193 ASTM A193 B7' → 'B7'
- A193/A194 등급 추출 로직 개선: 'GR B7/2H' 형태 지원
- bolt_details 테이블에 pressure_rating 컬럼 추가
- 볼트 분류기 오분류 방지: 플랜지/피팅이 볼트로 분류되지 않도록 수정
- 업로드 성능 개선: 키워드 기반 빠른 분류기 선택 로직 추가
- 분류 키워드 대폭 확장: 피팅/파이프/플랜지 키워드 추가
This commit is contained in:
Hyungi Ahn
2025-07-18 12:48:24 +09:00
parent 25ce3590ee
commit 3dd301cb57
13 changed files with 1184 additions and 106 deletions

View File

@@ -254,7 +254,9 @@ function MaterialList({ selectedProject }) {
'VALVE': 'success',
'FLANGE': 'warning',
'BOLT': 'info',
'OTHER': 'default'
'GASKET': 'error',
'INSTRUMENT': 'purple',
'OTHER': 'default'
};
return colors[itemType] || 'default';
};
@@ -540,6 +542,8 @@ function MaterialList({ selectedProject }) {
<MenuItem key="VALVE" value="VALVE">VALVE</MenuItem>
<MenuItem key="FLANGE" value="FLANGE">FLANGE</MenuItem>
<MenuItem key="BOLT" value="BOLT">BOLT</MenuItem>
<MenuItem key="GASKET" value="GASKET">GASKET</MenuItem>
<MenuItem key="INSTRUMENT" value="INSTRUMENT">INSTRUMENT</MenuItem>
<MenuItem key="OTHER" value="OTHER">기타</MenuItem>
</Select>
</FormControl>

View File

@@ -125,8 +125,8 @@ const MaterialsPage = () => {
spec_parts.push(fitting_subtype);
}
// NIPPLE 스케줄 정보 추가 (중요!)
const nipple_schedule = material.fitting_details?.schedule || material.pipe_details?.schedule;
// NIPPLE 스케줄 정보 추가 (fitting_details에서 가져옴)
const nipple_schedule = material.fitting_details?.schedule;
if (nipple_schedule && nipple_schedule !== 'UNKNOWN') {
spec_parts.push(nipple_schedule);
}
@@ -137,8 +137,8 @@ const MaterialsPage = () => {
spec_parts.push(connection_method);
}
// NIPPLE 길이 정보 추가 (mm → m 변환 또는 그대로)
const length_mm = material.length || material.pipe_details?.length_mm;
// NIPPLE 길이 정보 추가 (fitting_details에서 가져옴)
const length_mm = material.fitting_details?.length_mm;
if (length_mm && length_mm > 0) {
if (length_mm >= 1000) {
spec_parts.push(`${(length_mm / 1000).toFixed(2)}m`);
@@ -190,6 +190,228 @@ const MaterialsPage = () => {
unit: 'EA',
isLength: false
};
} else if (category === 'FLANGE') {
// FLANGE: 타입 + 압력등급 + 면가공 + 재질
const material_spec = material.flange_details?.material_spec || material.material_grade || '';
const main_nom = material.main_nom || '';
const flange_type = material.flange_details?.flange_type || 'UNKNOWN';
const pressure_rating = material.flange_details?.pressure_rating || '';
const facing_type = material.flange_details?.facing_type || '';
// 플랜지 스펙 생성
const flange_spec_parts = [];
// 플랜지 타입 (WN, BL, SO 등)
if (flange_type && flange_type !== 'UNKNOWN') {
flange_spec_parts.push(flange_type);
}
// 면 가공 (RF, FF, RTJ 등)
if (facing_type && facing_type !== 'UNKNOWN') {
flange_spec_parts.push(facing_type);
}
// 압력등급 (150LB, 300LB 등)
if (pressure_rating && pressure_rating !== 'UNKNOWN') {
flange_spec_parts.push(pressure_rating);
}
const full_flange_spec = flange_spec_parts.join(', ');
specKey = `${category}|${full_flange_spec}|${material_spec}|${main_nom}`;
specData = {
category: 'FLANGE',
flange_type,
pressure_rating,
facing_type,
full_flange_spec,
material_spec,
size_display: main_nom,
main_nom,
unit: 'EA',
isLength: false
};
} else if (category === 'GASKET') {
// GASKET: 타입 + 소재 + 압력등급 + 사이즈
const main_nom = material.main_nom || '';
const gasket_type = material.gasket_details?.gasket_type || 'UNKNOWN';
const material_type = material.gasket_details?.material_type || 'UNKNOWN';
const pressure_rating = material.gasket_details?.pressure_rating || '';
// 가스켓 재질은 gasket_details에서 가져옴
const material_spec = material_type !== 'UNKNOWN' ? material_type : (material.material_grade || 'Unknown');
// SWG 상세 정보 파싱 (additional_info에서)
let detailed_construction = 'N/A';
let face_type = '';
let thickness = material.gasket_details?.thickness || null;
// API에서 gasket_details의 추가 정보를 확인 (브라우저 콘솔에서 확인용)
if (material.gasket_details && Object.keys(material.gasket_details).length > 0) {
console.log('Gasket details:', material.gasket_details);
}
// 상세 구성 정보 생성 (Face Type + Construction)
// H/F/I/O SS304/GRAPHITE/CS/CS 형태로 표시
if (material.original_description) {
const desc = material.original_description.toUpperCase();
// H/F/I/O 다음에 오는 재질 구성만 찾기 (H/F/I/O는 제외)
const fullMatch = desc.match(/H\/F\/I\/O\s+([A-Z0-9]+\/[A-Z]+\/[A-Z0-9]+\/[A-Z0-9]+)/);
if (fullMatch) {
// H/F/I/O와 재질 구성 둘 다 있는 경우
face_type = 'H/F/I/O';
const construction = fullMatch[1];
detailed_construction = `${face_type} ${construction}`;
} else {
// H/F/I/O만 있는 경우
const faceMatch = desc.match(/H\/F\/I\/O/);
if (faceMatch) {
detailed_construction = 'H/F/I/O';
} else {
// 재질 구성만 있는 경우 (H/F/I/O 없이)
const constructionOnlyMatch = desc.match(/([A-Z0-9]+\/[A-Z]+\/[A-Z0-9]+\/[A-Z0-9]+)/);
if (constructionOnlyMatch) {
detailed_construction = constructionOnlyMatch[1];
}
}
}
}
// 가스켓 스펙 생성
const gasket_spec_parts = [];
// 가스켓 타입 (SPIRAL_WOUND, O_RING 등)
if (gasket_type && gasket_type !== 'UNKNOWN') {
gasket_spec_parts.push(gasket_type.replace('_', ' '));
}
// 소재 (GRAPHITE, PTFE 등)
if (material_type && material_type !== 'UNKNOWN') {
gasket_spec_parts.push(material_type);
}
// 압력등급 (150LB, 300LB 등)
if (pressure_rating && pressure_rating !== 'UNKNOWN') {
gasket_spec_parts.push(pressure_rating);
}
const full_gasket_spec = gasket_spec_parts.join(', ');
specKey = `${category}|${full_gasket_spec}|${material_spec}|${main_nom}|${detailed_construction}`;
specData = {
category: 'GASKET',
gasket_type,
material_type,
pressure_rating,
full_gasket_spec,
material_spec,
size_display: main_nom,
main_nom,
detailed_construction,
thickness,
unit: 'EA',
isLength: false
};
} else if (category === 'BOLT') {
// BOLT: 타입 + 재질 + 사이즈 + 길이
const material_spec = material.material_grade || '';
const main_nom = material.main_nom || '';
const bolt_type = material.bolt_details?.bolt_type || 'BOLT';
const material_standard = material.bolt_details?.material_standard || '';
const material_grade = material.bolt_details?.material_grade || '';
const thread_type = material.bolt_details?.thread_type || '';
const diameter = material.bolt_details?.diameter || main_nom;
const length = material.bolt_details?.length || '';
const pressure_rating = material.bolt_details?.pressure_rating || '';
const coating_type = material.bolt_details?.coating_type || '';
// 볼트 스펙 생성
const bolt_spec_parts = [];
// 볼트 타입 (HEX_BOLT, STUD_BOLT 등)
if (bolt_type && bolt_type !== 'UNKNOWN') {
bolt_spec_parts.push(bolt_type.replace('_', ' '));
}
// 재질 (ASTM A193, ASTM A194 등)
if (material_standard) {
bolt_spec_parts.push(material_standard);
if (material_grade && material_grade !== material_standard) {
bolt_spec_parts.push(material_grade);
}
} else if (material_spec) {
bolt_spec_parts.push(material_spec);
}
// 나사 규격 (M12, 1/2" 등)
if (diameter) {
bolt_spec_parts.push(diameter);
}
// 코팅 타입 (ELECTRO_GALVANIZED 등)
if (coating_type && coating_type !== 'PLAIN') {
bolt_spec_parts.push(coating_type.replace('_', ' '));
}
const full_bolt_spec = bolt_spec_parts.join(', ');
specKey = `${category}|${full_bolt_spec}|${diameter}|${length}|${coating_type}|${pressure_rating}`;
specData = {
category: 'BOLT',
bolt_type,
thread_type,
full_bolt_spec,
material_standard,
material_grade,
diameter,
length,
coating_type,
pressure_rating,
size_display: diameter,
main_nom: diameter,
unit: 'EA',
isLength: false
};
} else if (category === 'INSTRUMENT') {
// INSTRUMENT: 타입 + 연결사이즈 + 측정범위
const main_nom = material.main_nom || '';
const instrument_type = material.instrument_details?.instrument_type || 'INSTRUMENT';
const measurement_range = material.instrument_details?.measurement_range || '';
const signal_type = material.instrument_details?.signal_type || '';
// 계기 스펙 생성
const instrument_spec_parts = [];
// 계기 타입 (PRESSURE_GAUGE, TEMPERATURE_TRANSMITTER 등)
if (instrument_type && instrument_type !== 'UNKNOWN') {
instrument_spec_parts.push(instrument_type.replace('_', ' '));
}
// 측정 범위 (0-100 PSI, 4-20mA 등)
if (measurement_range) {
instrument_spec_parts.push(measurement_range);
}
// 연결 사이즈 (1/4", 1/2" 등)
if (main_nom) {
instrument_spec_parts.push(`${main_nom} CONNECTION`);
}
const full_instrument_spec = instrument_spec_parts.join(', ');
specKey = `${category}|${full_instrument_spec}|${main_nom}`;
specData = {
category: 'INSTRUMENT',
instrument_type,
measurement_range,
signal_type,
full_instrument_spec,
size_display: main_nom,
main_nom,
unit: 'EA',
isLength: false
};
} else {
// 기타 자재: 기본 분류
const material_spec = material.material_grade || '';
@@ -385,15 +607,46 @@ const MaterialsPage = () => {
<TableCell align="right"><strong>수량</strong></TableCell>
</>
)}
{!['PIPE', 'FITTING'].includes(category) && (
{category === 'FLANGE' && (
<>
<TableCell><strong>플랜지 타입</strong></TableCell>
<TableCell><strong>재질</strong></TableCell>
<TableCell><strong>사이즈</strong></TableCell>
<TableCell align="right"><strong>수량</strong></TableCell>
</>
)}
{!['PIPE', 'FITTING'].includes(category) && (
{category === 'GASKET' && (
<>
<TableCell><strong>가스켓 타입</strong></TableCell>
<TableCell><strong>상세 구성</strong></TableCell>
<TableCell><strong>재질</strong></TableCell>
<TableCell><strong>두께</strong></TableCell>
<TableCell><strong>사이즈</strong></TableCell>
<TableCell align="right"><strong>수량</strong></TableCell>
</>
)}
{category === 'BOLT' && (
<>
<TableCell><strong>볼트 타입</strong></TableCell>
<TableCell><strong>재질</strong></TableCell>
<TableCell><strong>사이즈</strong></TableCell>
<TableCell><strong>길이</strong></TableCell>
<TableCell><strong>코팅</strong></TableCell>
<TableCell><strong>압력등급</strong></TableCell>
<TableCell align="right"><strong>수량</strong></TableCell>
</>
)}
{category === 'INSTRUMENT' && (
<>
<TableCell><strong>계기 타입</strong></TableCell>
<TableCell><strong>측정범위</strong></TableCell>
<TableCell><strong>연결사이즈</strong></TableCell>
<TableCell align="right"><strong>수량</strong></TableCell>
</>
)}
{!['PIPE', 'FITTING', 'FLANGE', 'GASKET', 'BOLT', 'INSTRUMENT'].includes(category) && (
<>
<TableCell><strong>재질</strong></TableCell>
<TableCell><strong>사이즈</strong></TableCell>
<TableCell align="right"><strong>수량</strong></TableCell>
</>
@@ -437,8 +690,13 @@ const MaterialsPage = () => {
</TableCell>
</>
)}
{(!['PIPE', 'FITTING'].includes(category)) && (
{category === 'FLANGE' && (
<>
<TableCell>
<Typography variant="body2" sx={{ maxWidth: 300, fontWeight: 'bold' }}>
{spec.full_flange_spec || spec.flange_type || 'UNKNOWN'}
</Typography>
</TableCell>
<TableCell>{spec.material_spec || 'Unknown'}</TableCell>
<TableCell>{spec.size_display || 'Unknown'}</TableCell>
<TableCell align="right">
@@ -448,8 +706,90 @@ const MaterialsPage = () => {
</TableCell>
</>
)}
{(!['PIPE', 'FITTING'].includes(category)) && (
{category === 'GASKET' && (
<>
<TableCell>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
{spec.gasket_type?.replace('_', ' ') || 'UNKNOWN'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2" sx={{ maxWidth: 200, fontSize: '0.85rem' }}>
{spec.detailed_construction || 'N/A'}
</Typography>
</TableCell>
<TableCell>{spec.material_spec || 'Unknown'}</TableCell>
<TableCell>
<Typography variant="body2">
{spec.thickness ? `${spec.thickness}mm` : 'N/A'}
</Typography>
</TableCell>
<TableCell>{spec.size_display || 'Unknown'}</TableCell>
<TableCell align="right">
<Typography variant="body2" fontWeight="bold">
{spec.totalQuantity} {spec.unit}
</Typography>
</TableCell>
</>
)}
{category === 'BOLT' && (
<>
<TableCell>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
{spec.bolt_type?.replace('_', ' ') || 'UNKNOWN'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">
{spec.material_standard || 'Unknown'} {spec.material_grade || ''}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">
{spec.diameter ? (spec.diameter.includes('"') ? spec.diameter : spec.diameter.replace('0.5', '1/2"').replace('0.75', '3/4"').replace('1.0', '1"').replace('1.5', '1 1/2"')) : 'Unknown'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">
{spec.length || 'N/A'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">
{spec.coating_type ? spec.coating_type.replace('_', ' ') : 'N/A'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">
{spec.pressure_rating || 'N/A'}
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="body2" fontWeight="bold">
{spec.totalQuantity} {spec.unit}
</Typography>
</TableCell>
</>
)}
{category === 'INSTRUMENT' && (
<>
<TableCell>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
{spec.instrument_type?.replace('_', ' ') || 'UNKNOWN'}
</Typography>
</TableCell>
<TableCell>{spec.measurement_range || 'Unknown'}</TableCell>
<TableCell>{spec.size_display || 'Unknown'}</TableCell>
<TableCell align="right">
<Typography variant="body2" fontWeight="bold">
{spec.totalQuantity} {spec.unit}
</Typography>
</TableCell>
</>
)}
{(!['PIPE', 'FITTING', 'FLANGE', 'GASKET', 'BOLT', 'INSTRUMENT'].includes(category)) && (
<>
<TableCell>{spec.material_spec || 'Unknown'}</TableCell>
<TableCell>{spec.size_display || 'Unknown'}</TableCell>
<TableCell align="right">
<Typography variant="body2" fontWeight="bold">