feat: 자재 관리 페이지 대규모 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산) - UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인) - 자재별 그룹핑 로직 개선: * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시 * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등) * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시 * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계) * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시 * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑 - 전체 카테고리 버튼 제거 (표시 복잡도 감소) - 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
This commit is contained in:
971
frontend/src/pages/NewMaterialsPage.jsx
Normal file
971
frontend/src/pages/NewMaterialsPage.jsx
Normal file
@@ -0,0 +1,971 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { fetchMaterials } from '../api';
|
||||
import './NewMaterialsPage.css';
|
||||
|
||||
const NewMaterialsPage = ({
|
||||
onNavigate,
|
||||
selectedProject,
|
||||
fileId,
|
||||
jobNo,
|
||||
bomName,
|
||||
revision,
|
||||
filename
|
||||
}) => {
|
||||
const [materials, setMaterials] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedCategory, setSelectedCategory] = useState('PIPE');
|
||||
const [selectedMaterials, setSelectedMaterials] = useState(new Set());
|
||||
const [viewMode, setViewMode] = useState('detailed'); // 'detailed' or 'simple'
|
||||
|
||||
// 자재 데이터 로드
|
||||
useEffect(() => {
|
||||
if (fileId) {
|
||||
loadMaterials(fileId);
|
||||
}
|
||||
}, [fileId]);
|
||||
|
||||
const loadMaterials = async (id) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🔍 자재 데이터 로딩 중...', { file_id: id });
|
||||
|
||||
const response = await fetchMaterials({
|
||||
file_id: parseInt(id),
|
||||
limit: 10000
|
||||
});
|
||||
|
||||
if (response.data?.materials) {
|
||||
const materialsData = response.data.materials;
|
||||
console.log(`✅ ${materialsData.length}개 자재 로드 완료`);
|
||||
|
||||
// 파이프 데이터 검증
|
||||
const pipes = materialsData.filter(m => m.classified_category === 'PIPE');
|
||||
if (pipes.length > 0) {
|
||||
console.log('📊 파이프 데이터 샘플:', pipes[0]);
|
||||
}
|
||||
|
||||
setMaterials(materialsData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 자재 로딩 실패:', error);
|
||||
setMaterials([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 카테고리별 자재 수 계산
|
||||
const getCategoryCounts = () => {
|
||||
const counts = {};
|
||||
materials.forEach(material => {
|
||||
const category = material.classified_category || 'UNKNOWN';
|
||||
counts[category] = (counts[category] || 0) + 1;
|
||||
});
|
||||
return counts;
|
||||
};
|
||||
|
||||
// 파이프 구매 수량 계산 함수
|
||||
const calculatePipePurchase = (material) => {
|
||||
// 백엔드에서 이미 그룹핑된 데이터 사용
|
||||
const totalLength = material.pipe_details?.total_length_mm || 0;
|
||||
const pipeCount = material.pipe_details?.pipe_count || material.quantity || 0;
|
||||
|
||||
// 절단 손실: 각 단관마다 2mm
|
||||
const cuttingLoss = pipeCount * 2;
|
||||
|
||||
// 총 필요 길이
|
||||
const requiredLength = totalLength + cuttingLoss;
|
||||
|
||||
// 6M(6000mm) 단위로 구매 본수 계산
|
||||
const purchaseCount = Math.ceil(requiredLength / 6000);
|
||||
|
||||
return {
|
||||
pipeCount, // 단관 개수
|
||||
totalLength, // 총 BOM 길이
|
||||
cuttingLoss, // 절단 손실
|
||||
requiredLength, // 필요 길이
|
||||
purchaseCount // 구매 본수
|
||||
};
|
||||
};
|
||||
|
||||
// 자재 정보 파싱
|
||||
const parseMaterialInfo = (material) => {
|
||||
const category = material.classified_category;
|
||||
|
||||
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 || '-',
|
||||
quantity: calc.purchaseCount,
|
||||
unit: '본',
|
||||
details: calc
|
||||
};
|
||||
} else if (category === 'FITTING') {
|
||||
const fittingDetails = material.fitting_details || {};
|
||||
const fittingType = fittingDetails.fitting_type || '';
|
||||
const fittingSubtype = fittingDetails.fitting_subtype || '';
|
||||
const description = material.original_description || '';
|
||||
|
||||
// 피팅 타입별 상세 표시
|
||||
let displayType = '';
|
||||
|
||||
// CAP과 PLUG 먼저 확인 (fitting_type이 없을 수 있음)
|
||||
if (description.toUpperCase().includes('CAP')) {
|
||||
// CAP: 연결 방식 표시 (예: CAP, NPT(F), 3000LB, ASTM A105)
|
||||
if (description.includes('NPT(F)')) {
|
||||
displayType = 'CAP NPT(F)';
|
||||
} else if (description.includes('SW')) {
|
||||
displayType = 'CAP SW';
|
||||
} else if (description.includes('BW')) {
|
||||
displayType = 'CAP BW';
|
||||
} else {
|
||||
displayType = 'CAP';
|
||||
}
|
||||
} else if (description.toUpperCase().includes('PLUG')) {
|
||||
// PLUG: 타입과 연결 방식 표시 (예: HEX.PLUG, NPT(M), 6000LB, ASTM A105)
|
||||
if (description.toUpperCase().includes('HEX')) {
|
||||
if (description.includes('NPT(M)')) {
|
||||
displayType = 'HEX PLUG NPT(M)';
|
||||
} else {
|
||||
displayType = 'HEX PLUG';
|
||||
}
|
||||
} else if (description.includes('NPT(M)')) {
|
||||
displayType = 'PLUG NPT(M)';
|
||||
} else if (description.includes('NPT')) {
|
||||
displayType = 'PLUG NPT';
|
||||
} else {
|
||||
displayType = 'PLUG';
|
||||
}
|
||||
} else if (fittingType === 'NIPPLE') {
|
||||
// 니플: 길이와 끝단 가공 정보
|
||||
const length = fittingDetails.length_mm || fittingDetails.avg_length_mm;
|
||||
displayType = length ? `NIPPLE ${length}mm` : 'NIPPLE';
|
||||
} else if (fittingType === 'ELBOW') {
|
||||
// 엘보: 각도와 연결 방식
|
||||
const angle = fittingSubtype === '90DEG' ? '90°' : fittingSubtype === '45DEG' ? '45°' : '';
|
||||
const connection = description.includes('SW') ? 'SW' : description.includes('BW') ? 'BW' : '';
|
||||
displayType = `ELBOW ${angle} ${connection}`.trim();
|
||||
} else if (fittingType === 'TEE') {
|
||||
// 티: 타입과 연결 방식
|
||||
const teeType = fittingSubtype === 'EQUAL' ? 'EQ' : fittingSubtype === 'REDUCING' ? 'RED' : '';
|
||||
const connection = description.includes('SW') ? 'SW' : description.includes('BW') ? 'BW' : '';
|
||||
displayType = `TEE ${teeType} ${connection}`.trim();
|
||||
} else if (fittingType === 'REDUCER') {
|
||||
// 레듀서: 콘센트릭/에센트릭
|
||||
const reducerType = fittingSubtype === 'CONCENTRIC' ? 'CONC' : fittingSubtype === 'ECCENTRIC' ? 'ECC' : '';
|
||||
const sizes = fittingDetails.reduced_size ? `${material.size_spec}×${fittingDetails.reduced_size}` : material.size_spec;
|
||||
displayType = `RED ${reducerType} ${sizes}`.trim();
|
||||
} else if (fittingType === 'SWAGE') {
|
||||
// 스웨이지: 타입 명시
|
||||
const swageType = fittingSubtype || '';
|
||||
displayType = `SWAGE ${swageType}`.trim();
|
||||
} else if (!displayType) {
|
||||
// 기타 피팅 타입
|
||||
displayType = fittingType || 'FITTING';
|
||||
}
|
||||
|
||||
// 압력 등급과 스케줄 추출
|
||||
let pressure = '-';
|
||||
let schedule = '-';
|
||||
|
||||
// 압력 등급 찾기 (3000LB, 6000LB 등)
|
||||
const pressureMatch = description.match(/(\d+)LB/i);
|
||||
if (pressureMatch) {
|
||||
pressure = `${pressureMatch[1]}LB`;
|
||||
}
|
||||
|
||||
// 스케줄 찾기
|
||||
if (description.includes('SCH')) {
|
||||
const schMatch = description.match(/SCH\s*(\d+[A-Z]*)/i);
|
||||
if (schMatch) {
|
||||
schedule = `SCH ${schMatch[1]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'FITTING',
|
||||
subtype: displayType,
|
||||
size: material.size_spec || '-',
|
||||
pressure: pressure,
|
||||
schedule: schedule,
|
||||
grade: material.material_grade || '-',
|
||||
quantity: Math.round(material.quantity || 0),
|
||||
unit: '개',
|
||||
isFitting: true
|
||||
};
|
||||
} else if (category === 'VALVE') {
|
||||
const valveDetails = material.valve_details || {};
|
||||
const description = material.original_description || '';
|
||||
|
||||
// 밸브 타입 파싱 (GATE, BALL, CHECK, GLOBE 등)
|
||||
let valveType = valveDetails.valve_type || '';
|
||||
if (!valveType && description) {
|
||||
if (description.includes('GATE')) valveType = 'GATE';
|
||||
else if (description.includes('BALL')) valveType = 'BALL';
|
||||
else if (description.includes('CHECK')) valveType = 'CHECK';
|
||||
else if (description.includes('GLOBE')) valveType = 'GLOBE';
|
||||
else if (description.includes('BUTTERFLY')) valveType = 'BUTTERFLY';
|
||||
}
|
||||
|
||||
// 연결 방식 파싱 (FLG, SW, THRD 등)
|
||||
let connectionType = '';
|
||||
if (description.includes('FLG')) {
|
||||
connectionType = 'FLG';
|
||||
} else if (description.includes('SW X THRD')) {
|
||||
connectionType = 'SW×THRD';
|
||||
} else if (description.includes('SW')) {
|
||||
connectionType = 'SW';
|
||||
} else if (description.includes('THRD')) {
|
||||
connectionType = 'THRD';
|
||||
} else if (description.includes('BW')) {
|
||||
connectionType = 'BW';
|
||||
}
|
||||
|
||||
// 압력 등급 파싱
|
||||
let pressure = '-';
|
||||
const pressureMatch = description.match(/(\d+)LB/i);
|
||||
if (pressureMatch) {
|
||||
pressure = `${pressureMatch[1]}LB`;
|
||||
}
|
||||
|
||||
// 스케줄은 밸브에는 일반적으로 없음
|
||||
let schedule = '-';
|
||||
|
||||
return {
|
||||
type: 'VALVE',
|
||||
valveType: valveType,
|
||||
connectionType: connectionType,
|
||||
size: material.size_spec || '-',
|
||||
pressure: pressure,
|
||||
schedule: schedule,
|
||||
grade: material.material_grade || '-',
|
||||
quantity: Math.round(material.quantity || 0),
|
||||
unit: '개',
|
||||
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 || '-';
|
||||
|
||||
// 원본 설명에서 스케줄 추출
|
||||
let schedule = '-';
|
||||
const description = material.original_description || '';
|
||||
|
||||
// SCH 40, SCH 80 등의 패턴 찾기
|
||||
if (description.toUpperCase().includes('SCH')) {
|
||||
const schMatch = description.match(/SCH\s*(\d+[A-Z]*)/i);
|
||||
if (schMatch && schMatch[1]) {
|
||||
schedule = `SCH ${schMatch[1]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'FLANGE',
|
||||
subtype: displayType,
|
||||
size: material.size_spec || '-',
|
||||
pressure: material.flange_details?.pressure_rating || '-',
|
||||
schedule: schedule,
|
||||
grade: material.material_grade || '-',
|
||||
quantity: Math.round(material.quantity || 0),
|
||||
unit: '개',
|
||||
isFlange: true // 플랜지 구분용 플래그
|
||||
};
|
||||
} else if (category === 'BOLT') {
|
||||
const qty = Math.round(material.quantity || 0);
|
||||
const safetyQty = Math.ceil(qty * 1.05); // 5% 여유율
|
||||
const purchaseQty = Math.ceil(safetyQty / 4) * 4; // 4의 배수
|
||||
return {
|
||||
type: 'BOLT',
|
||||
subtype: material.bolt_details?.bolt_type || '-',
|
||||
size: material.size_spec || '-',
|
||||
schedule: material.bolt_details?.length || '-',
|
||||
grade: material.material_grade || '-',
|
||||
quantity: purchaseQty,
|
||||
unit: 'SETS'
|
||||
};
|
||||
} else if (category === 'GASKET') {
|
||||
const qty = Math.round(material.quantity || 0);
|
||||
const purchaseQty = Math.ceil(qty / 5) * 5; // 5의 배수
|
||||
|
||||
// original_description에서 재질 정보 파싱
|
||||
const description = material.original_description || '';
|
||||
let materialStructure = '-'; // H/F/I/O 부분
|
||||
let materialDetail = '-'; // SS304/GRAPHITE/CS/CS 부분
|
||||
|
||||
// H/F/I/O와 재질 상세 정보 추출
|
||||
const materialMatch = description.match(/H\/F\/I\/O\s+(.+?)(?:,|$)/);
|
||||
if (materialMatch) {
|
||||
materialStructure = 'H/F/I/O';
|
||||
materialDetail = materialMatch[1].trim();
|
||||
// 두께 정보 제거 (별도 추출)
|
||||
materialDetail = materialDetail.replace(/,?\s*\d+(?:\.\d+)?mm$/, '').trim();
|
||||
}
|
||||
|
||||
// 압력 정보 추출
|
||||
let pressure = '-';
|
||||
const pressureMatch = description.match(/(\d+LB)/);
|
||||
if (pressureMatch) {
|
||||
pressure = pressureMatch[1];
|
||||
}
|
||||
|
||||
// 두께 정보 추출
|
||||
let thickness = '-';
|
||||
const thicknessMatch = description.match(/(\d+(?:\.\d+)?)\s*mm/i);
|
||||
if (thicknessMatch) {
|
||||
thickness = thicknessMatch[1] + 'mm';
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'GASKET',
|
||||
subtype: 'SWG', // 항상 SWG로 표시
|
||||
size: material.size_spec || '-',
|
||||
pressure: pressure,
|
||||
materialStructure: materialStructure,
|
||||
materialDetail: materialDetail,
|
||||
thickness: thickness,
|
||||
quantity: purchaseQty,
|
||||
unit: '개',
|
||||
isGasket: true
|
||||
};
|
||||
} else if (category === 'UNKNOWN') {
|
||||
return {
|
||||
type: 'UNKNOWN',
|
||||
description: material.original_description || 'Unknown Item',
|
||||
quantity: Math.round(material.quantity || 0),
|
||||
unit: '개',
|
||||
isUnknown: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: category || 'UNKNOWN',
|
||||
subtype: '-',
|
||||
size: material.size_spec || '-',
|
||||
schedule: '-',
|
||||
grade: material.material_grade || '-',
|
||||
quantity: Math.round(material.quantity || 0),
|
||||
unit: '개'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 필터링된 자재 목록
|
||||
const filteredMaterials = materials.filter(material => {
|
||||
return material.classified_category === selectedCategory;
|
||||
});
|
||||
|
||||
// 카테고리 색상 (제거 - CSS에서 처리)
|
||||
|
||||
// 전체 선택/해제
|
||||
const toggleAllSelection = () => {
|
||||
if (selectedMaterials.size === filteredMaterials.length) {
|
||||
setSelectedMaterials(new Set());
|
||||
} else {
|
||||
setSelectedMaterials(new Set(filteredMaterials.map(m => m.id)));
|
||||
}
|
||||
};
|
||||
|
||||
// 개별 선택
|
||||
const toggleMaterialSelection = (id) => {
|
||||
const newSelection = new Set(selectedMaterials);
|
||||
if (newSelection.has(id)) {
|
||||
newSelection.delete(id);
|
||||
} else {
|
||||
newSelection.add(id);
|
||||
}
|
||||
setSelectedMaterials(newSelection);
|
||||
};
|
||||
|
||||
// 엑셀 내보내기
|
||||
const exportToExcel = () => {
|
||||
const selectedData = materials.filter(m => selectedMaterials.has(m.id));
|
||||
console.log('📊 엑셀 내보내기:', selectedData.length, '개 항목');
|
||||
alert(`${selectedData.length}개 항목을 엑셀로 내보냅니다.`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>자재 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const categoryCounts = getCategoryCounts();
|
||||
|
||||
return (
|
||||
<div className="materials-page">
|
||||
{/* 헤더 */}
|
||||
<div className="materials-header">
|
||||
<div className="header-left">
|
||||
<button onClick={() => onNavigate('bom')} className="back-button">
|
||||
← BOM 업로드로 돌아가기
|
||||
</button>
|
||||
<h1>자재 목록</h1>
|
||||
{jobNo && (
|
||||
<span className="job-info">
|
||||
{jobNo} {revision && `(${revision})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<span className="material-count">
|
||||
총 {materials.length}개 자재
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카테고리 필터 */}
|
||||
<div className="category-filters">
|
||||
{Object.entries(categoryCounts).map(([category, count]) => (
|
||||
<button
|
||||
key={category}
|
||||
className={`category-btn ${selectedCategory === category ? 'active' : ''}`}
|
||||
onClick={() => setSelectedCategory(category)}
|
||||
>
|
||||
{category} <span className="count">{count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 액션 바 */}
|
||||
<div className="action-bar">
|
||||
<div className="selection-info">
|
||||
{selectedMaterials.size}개 중 {filteredMaterials.length}개 선택
|
||||
</div>
|
||||
<div className="action-buttons">
|
||||
<button
|
||||
onClick={toggleAllSelection}
|
||||
className="select-all-btn"
|
||||
>
|
||||
{selectedMaterials.size === filteredMaterials.length ? '전체 해제' : '전체 선택'}
|
||||
</button>
|
||||
<button
|
||||
onClick={exportToExcel}
|
||||
className="export-btn"
|
||||
disabled={selectedMaterials.size === 0}
|
||||
>
|
||||
엑셀 내보내기 ({selectedMaterials.size})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 자재 테이블 */}
|
||||
<div className="materials-grid">
|
||||
{/* 플랜지 전용 헤더 */}
|
||||
{selectedCategory === 'FLANGE' ? (
|
||||
<div className="detailed-grid-header flange-header">
|
||||
<div>선택</div>
|
||||
<div>종류</div>
|
||||
<div>타입</div>
|
||||
<div>크기</div>
|
||||
<div>압력(파운드)</div>
|
||||
<div>스케줄</div>
|
||||
<div>재질</div>
|
||||
<div>추가요구</div>
|
||||
<div>사용자요구</div>
|
||||
<div>수량</div>
|
||||
</div>
|
||||
) : selectedCategory === 'FITTING' ? (
|
||||
<div className="detailed-grid-header fitting-header">
|
||||
<div>선택</div>
|
||||
<div>종류</div>
|
||||
<div>타입/상세</div>
|
||||
<div>크기</div>
|
||||
<div>압력</div>
|
||||
<div>스케줄</div>
|
||||
<div>재질</div>
|
||||
<div>추가요구</div>
|
||||
<div>사용자요구</div>
|
||||
<div>수량</div>
|
||||
</div>
|
||||
) : selectedCategory === 'GASKET' ? (
|
||||
<div className="detailed-grid-header gasket-header">
|
||||
<div>선택</div>
|
||||
<div>종류</div>
|
||||
<div>타입</div>
|
||||
<div>크기</div>
|
||||
<div>압력</div>
|
||||
<div>재질</div>
|
||||
<div>상세내역</div>
|
||||
<div>두께</div>
|
||||
<div>추가요구</div>
|
||||
<div>사용자요구</div>
|
||||
<div>수량</div>
|
||||
</div>
|
||||
) : selectedCategory === 'VALVE' ? (
|
||||
<div className="detailed-grid-header valve-header">
|
||||
<div>선택</div>
|
||||
<div>타입</div>
|
||||
<div>연결방식</div>
|
||||
<div>크기</div>
|
||||
<div>압력</div>
|
||||
<div>재질</div>
|
||||
<div>추가요구</div>
|
||||
<div>사용자요구</div>
|
||||
<div>수량</div>
|
||||
</div>
|
||||
) : selectedCategory === 'UNKNOWN' ? (
|
||||
<div className="detailed-grid-header unknown-header">
|
||||
<div>선택</div>
|
||||
<div>종류</div>
|
||||
<div>설명</div>
|
||||
<div>사용자요구</div>
|
||||
<div>수량</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="detailed-grid-header">
|
||||
<div>선택</div>
|
||||
<div>종류</div>
|
||||
<div>타입</div>
|
||||
<div>크기</div>
|
||||
<div>스케줄</div>
|
||||
<div>재질</div>
|
||||
<div>추가요구</div>
|
||||
<div>사용자요구</div>
|
||||
<div>수량</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredMaterials.map((material) => {
|
||||
const info = parseMaterialInfo(material);
|
||||
|
||||
// 피팅인 경우 10개 컬럼
|
||||
if (info.isFitting) {
|
||||
return (
|
||||
<div
|
||||
key={material.id}
|
||||
className={`detailed-material-row fitting-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
|
||||
>
|
||||
{/* 선택 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMaterials.has(material.id)}
|
||||
onChange={() => toggleMaterialSelection(material.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 종류 */}
|
||||
<div className="material-cell">
|
||||
<span className={`type-badge ${info.type.toLowerCase()}`}>
|
||||
{info.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 타입/상세 */}
|
||||
<div className="material-cell">
|
||||
<span className="subtype-text">{info.subtype}</span>
|
||||
</div>
|
||||
|
||||
{/* 크기 */}
|
||||
<div className="material-cell">
|
||||
<span className="size-text">{info.size}</span>
|
||||
</div>
|
||||
|
||||
{/* 압력 */}
|
||||
<div className="material-cell">
|
||||
<span className="pressure-info">{info.pressure}</span>
|
||||
</div>
|
||||
|
||||
{/* 스케줄 */}
|
||||
<div className="material-cell">
|
||||
<span>{info.schedule}</span>
|
||||
</div>
|
||||
|
||||
{/* 재질 */}
|
||||
<div className="material-cell">
|
||||
<span className="material-grade">{info.grade}</span>
|
||||
</div>
|
||||
|
||||
{/* 추가요구 */}
|
||||
<div className="material-cell">
|
||||
<span>-</span>
|
||||
</div>
|
||||
|
||||
{/* 사용자요구 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="text"
|
||||
className="user-req-input"
|
||||
placeholder="요구사항 입력"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수량 */}
|
||||
<div className="material-cell">
|
||||
<div className="quantity-info">
|
||||
<span className="quantity-value">
|
||||
{info.quantity} {info.unit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 밸브인 경우 10개 컬럼
|
||||
if (info.isValve) {
|
||||
return (
|
||||
<div
|
||||
key={material.id}
|
||||
className={`detailed-material-row valve-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
|
||||
>
|
||||
{/* 선택 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMaterials.has(material.id)}
|
||||
onChange={() => toggleMaterialSelection(material.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 타입 */}
|
||||
<div className="material-cell">
|
||||
<span className="valve-type">{info.valveType}</span>
|
||||
</div>
|
||||
|
||||
{/* 연결방식 */}
|
||||
<div className="material-cell">
|
||||
<span className="connection-type">{info.connectionType}</span>
|
||||
</div>
|
||||
|
||||
{/* 크기 */}
|
||||
<div className="material-cell">
|
||||
<span className="size-text">{info.size}</span>
|
||||
</div>
|
||||
|
||||
{/* 압력 */}
|
||||
<div className="material-cell">
|
||||
<span className="pressure-info">{info.pressure}</span>
|
||||
</div>
|
||||
|
||||
{/* 재질 */}
|
||||
<div className="material-cell">
|
||||
<span className="material-grade">{info.grade}</span>
|
||||
</div>
|
||||
|
||||
{/* 추가요구 */}
|
||||
<div className="material-cell">
|
||||
<span>-</span>
|
||||
</div>
|
||||
|
||||
{/* 사용자요구 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="text"
|
||||
className="user-req-input"
|
||||
placeholder="요구사항 입력"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수량 */}
|
||||
<div className="material-cell">
|
||||
<div className="quantity-info">
|
||||
<span className="quantity-value">
|
||||
{info.quantity} {info.unit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 플랜지인 경우 10개 컬럼
|
||||
if (info.isFlange) {
|
||||
return (
|
||||
<div
|
||||
key={material.id}
|
||||
className={`detailed-material-row flange-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
|
||||
>
|
||||
{/* 선택 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMaterials.has(material.id)}
|
||||
onChange={() => toggleMaterialSelection(material.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 종류 */}
|
||||
<div className="material-cell">
|
||||
<span className={`type-badge ${info.type.toLowerCase()}`}>
|
||||
{info.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 타입 */}
|
||||
<div className="material-cell">
|
||||
<span className="subtype-text">{info.subtype}</span>
|
||||
</div>
|
||||
|
||||
{/* 크기 */}
|
||||
<div className="material-cell">
|
||||
<span className="size-text">{info.size}</span>
|
||||
</div>
|
||||
|
||||
{/* 압력(파운드) */}
|
||||
<div className="material-cell">
|
||||
<span className="pressure-info">{info.pressure}</span>
|
||||
</div>
|
||||
|
||||
{/* 스케줄 */}
|
||||
<div className="material-cell">
|
||||
<span>{info.schedule}</span>
|
||||
</div>
|
||||
|
||||
{/* 재질 */}
|
||||
<div className="material-cell">
|
||||
<span className="material-grade">{info.grade}</span>
|
||||
</div>
|
||||
|
||||
{/* 추가요구 */}
|
||||
<div className="material-cell">
|
||||
<span>-</span>
|
||||
</div>
|
||||
|
||||
{/* 사용자요구 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="text"
|
||||
className="user-req-input"
|
||||
placeholder="요구사항 입력"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수량 */}
|
||||
<div className="material-cell">
|
||||
<div className="quantity-info">
|
||||
<span className="quantity-value">
|
||||
{info.quantity} {info.unit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// UNKNOWN인 경우 5개 컬럼
|
||||
if (info.isUnknown) {
|
||||
return (
|
||||
<div
|
||||
key={material.id}
|
||||
className={`detailed-material-row unknown-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
|
||||
>
|
||||
{/* 선택 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMaterials.has(material.id)}
|
||||
onChange={() => toggleMaterialSelection(material.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 종류 */}
|
||||
<div className="material-cell">
|
||||
<span className={`type-badge unknown`}>
|
||||
{info.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 설명 */}
|
||||
<div className="material-cell description-cell">
|
||||
<span className="description-text" title={info.description}>
|
||||
{info.description}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 사용자요구 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="text"
|
||||
className="user-req-input"
|
||||
placeholder="요구사항 입력"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수량 */}
|
||||
<div className="material-cell">
|
||||
<div className="quantity-info">
|
||||
<span className="quantity-value">
|
||||
{info.quantity} {info.unit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 가스켓인 경우 11개 컬럼
|
||||
if (info.isGasket) {
|
||||
return (
|
||||
<div
|
||||
key={material.id}
|
||||
className={`detailed-material-row gasket-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
|
||||
>
|
||||
{/* 선택 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMaterials.has(material.id)}
|
||||
onChange={() => toggleMaterialSelection(material.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 종류 */}
|
||||
<div className="material-cell">
|
||||
<span className={`type-badge ${info.type.toLowerCase()}`}>
|
||||
{info.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 타입 */}
|
||||
<div className="material-cell">
|
||||
<span className="subtype-text">{info.subtype}</span>
|
||||
</div>
|
||||
|
||||
{/* 크기 */}
|
||||
<div className="material-cell">
|
||||
<span className="size-text">{info.size}</span>
|
||||
</div>
|
||||
|
||||
{/* 압력 */}
|
||||
<div className="material-cell">
|
||||
<span className="pressure-info">{info.pressure}</span>
|
||||
</div>
|
||||
|
||||
{/* 재질 */}
|
||||
<div className="material-cell">
|
||||
<span className="material-structure">{info.materialStructure}</span>
|
||||
</div>
|
||||
|
||||
{/* 상세내역 */}
|
||||
<div className="material-cell">
|
||||
<span className="material-detail">{info.materialDetail}</span>
|
||||
</div>
|
||||
|
||||
{/* 두께 */}
|
||||
<div className="material-cell">
|
||||
<span className="thickness-info">{info.thickness}</span>
|
||||
</div>
|
||||
|
||||
{/* 추가요구 */}
|
||||
<div className="material-cell">
|
||||
<span>-</span>
|
||||
</div>
|
||||
|
||||
{/* 사용자요구 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="text"
|
||||
className="user-req-input"
|
||||
placeholder="요구사항 입력"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수량 */}
|
||||
<div className="material-cell">
|
||||
<div className="quantity-info">
|
||||
<span className="quantity-value">
|
||||
{info.quantity} {info.unit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 플랜지가 아닌 경우 9개 컬럼
|
||||
return (
|
||||
<div
|
||||
key={material.id}
|
||||
className={`detailed-material-row ${selectedMaterials.has(material.id) ? 'selected' : ''}`}
|
||||
>
|
||||
{/* 선택 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMaterials.has(material.id)}
|
||||
onChange={() => toggleMaterialSelection(material.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 종류 */}
|
||||
<div className="material-cell">
|
||||
<span className={`type-badge ${info.type.toLowerCase()}`}>
|
||||
{info.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 타입 */}
|
||||
<div className="material-cell">
|
||||
<span className="subtype-text">{info.subtype}</span>
|
||||
</div>
|
||||
|
||||
{/* 크기 */}
|
||||
<div className="material-cell">
|
||||
<span className="size-text">{info.size}</span>
|
||||
</div>
|
||||
|
||||
{/* 스케줄 */}
|
||||
<div className="material-cell">
|
||||
<span>{info.schedule}</span>
|
||||
</div>
|
||||
|
||||
{/* 재질 */}
|
||||
<div className="material-cell">
|
||||
<span className="material-grade">{info.grade}</span>
|
||||
</div>
|
||||
|
||||
{/* 추가요구 */}
|
||||
<div className="material-cell">
|
||||
<span>-</span>
|
||||
</div>
|
||||
|
||||
{/* 사용자요구 */}
|
||||
<div className="material-cell">
|
||||
<input
|
||||
type="text"
|
||||
className="user-req-input"
|
||||
placeholder="요구사항 입력"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수량 */}
|
||||
<div className="material-cell">
|
||||
<div className="quantity-info">
|
||||
<span className="quantity-value">
|
||||
{info.quantity} {info.unit}
|
||||
</span>
|
||||
{info.type === 'PIPE' && info.details && (
|
||||
<div className="quantity-details">
|
||||
<small>
|
||||
단관 {info.details.pipeCount}개 → {Math.round(info.details.totalLength)}mm
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewMaterialsPage;
|
||||
Reference in New Issue
Block a user