feat: 자재 관리 페이지 대규모 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산)
- UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인)
- 자재별 그룹핑 로직 개선:
  * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시
  * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등)
  * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시
  * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계)
  * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시
  * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑
- 전체 카테고리 버튼 제거 (표시 복잡도 감소)
- 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
This commit is contained in:
Hyungi Ahn
2025-09-09 09:24:45 +09:00
parent 4f8e395f87
commit 83b90ef05c
101 changed files with 10841 additions and 4813 deletions

View 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;