feat: BOM 페이지 개선 및 구매관리 기능 향상
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- BOM 페이지에서 모든 카테고리 이모지 제거 (깔끔한 UI)
- 구매신청된 자재 비활성화 기능 개선
- 구매관리 페이지에서 선택된 프로젝트만 표시하도록 수정
- 파이프 카테고리 Excel 내보내기 개선:
  * 끝단처리, 압력등급 컬럼 제거
  * 사용자요구(분류기 추출) 및 추가요청사항 컬럼 추가
  * 품목명에서 끝단처리 정보 제거
  * 납기일 P열 고정 및 관리항목 자동 채움
- 파이프 분류기에서 사용자 요구사항 추출 기능 추가
- 재질별 스케줄 표시 개선 (SUS: SCH 40S, Carbon: SCH 40)
This commit is contained in:
hyungi
2025-10-16 13:27:14 +09:00
parent 64fd9ad3d2
commit 22baea38e1
11 changed files with 273 additions and 108 deletions

View File

@@ -442,7 +442,6 @@ const BoltMaterialsView = ({
padding: '60px 20px',
color: '#64748b'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🔩</div>
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
No Bolt Materials Found
</div>

View File

@@ -648,7 +648,6 @@ const FittingMaterialsView = ({
padding: '60px 20px',
color: '#64748b'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}></div>
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
No Fitting Materials Found
</div>

View File

@@ -494,7 +494,6 @@ const FlangeMaterialsView = ({
padding: '60px 20px',
color: '#64748b'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🔩</div>
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
No Flange Materials Found
</div>

View File

@@ -378,7 +378,6 @@ const GasketMaterialsView = ({
padding: '60px 20px',
color: '#64748b'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}></div>
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
No Gasket Materials Found
</div>

View File

@@ -18,41 +18,77 @@ const PipeMaterialsView = ({
const [columnFilters, setColumnFilters] = useState({});
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
// 파이프 구매 수량 계산 (기존 로직 복원)
// 파이프 구매 수량 계산 (백엔드 그룹화 데이터 활용)
const calculatePipePurchase = (material) => {
const pipeDetails = material.pipe_details || {};
const totalLength = pipeDetails.length || material.length || 0;
const standardLength = 6; // 표준 6M
const purchaseCount = Math.ceil(totalLength / standardLength);
const totalPurchaseLength = purchaseCount * standardLength;
const wasteLength = totalPurchaseLength - totalLength;
const wastePercentage = totalLength > 0 ? (wasteLength / totalLength * 100) : 0;
// 백엔드에서 이미 그룹화된 데이터 사용
let pipeCount = 1; // 기본값
let totalBomLengthMm = 0;
if (pipeDetails.pipe_count && pipeDetails.total_length_mm) {
// 백엔드에서 그룹화된 데이터 사용
pipeCount = pipeDetails.pipe_count; // 실제 단관 개수
totalBomLengthMm = pipeDetails.total_length_mm; // 이미 합산된 총 길이
} else {
// 개별 파이프 데이터인 경우
pipeCount = material.quantity || 1;
// 길이 정보 우선순위: length_mm > length > pipe_details.length_mm
let singlePipeLengthMm = 0;
if (material.length_mm) {
singlePipeLengthMm = material.length_mm;
} else if (material.length) {
singlePipeLengthMm = material.length * 1000; // m를 mm로 변환
} else if (pipeDetails.length_mm) {
singlePipeLengthMm = pipeDetails.length_mm;
}
totalBomLengthMm = singlePipeLengthMm * pipeCount;
}
// 여유분 포함 계산: 각 단관당 2mm 여유분 추가
const allowancePerPipe = 2; // mm
const totalAllowanceMm = allowancePerPipe * pipeCount;
const totalLengthWithAllowance = totalBomLengthMm + totalAllowanceMm; // mm
// 6,000mm(6m) 표준 길이로 필요한 본수 계산 (올림)
const standardLengthMm = 6000; // mm
const requiredStandardPipes = Math.ceil(totalLengthWithAllowance / standardLengthMm);
return {
totalLength,
standardLength,
purchaseCount,
totalPurchaseLength,
wasteLength,
wastePercentage
pipeCount, // 단관 개수
totalBomLengthMm, // 총 BOM 길이 (mm)
totalLengthWithAllowance, // 여유분 포함 총 길이 (mm)
totalLengthM: totalLengthWithAllowance / 1000, // 총 길이 (m)
requiredStandardPipes, // 필요한 표준 파이프 본수
standardLengthMm,
allowancePerPipe,
totalAllowanceMm,
// 디버깅용 정보
isGrouped: !!(pipeDetails.pipe_count && pipeDetails.total_length_mm)
};
};
// 파이프 정보 파싱 (기존 상세 로직 복원)
// 파이프 정보 파싱 (개선된 로직)
const parsePipeInfo = (material) => {
const calc = calculatePipePurchase(material);
const pipeDetails = material.pipe_details || {};
// User 요구사항 추출 (분류기에서 제공된 정보)
const userRequirements = material.user_requirements || [];
const userReqText = userRequirements.length > 0 ? userRequirements.join(', ') : '-';
return {
type: 'PIPE',
subtype: pipeDetails.manufacturing_method || 'SMLS',
// Type 컬럼 제거 (모두 PIPE로 동일)
type: pipeDetails.manufacturing_method || 'SMLS', // Subtype을 Type으로 변경
size: material.size_spec || '-',
schedule: pipeDetails.schedule || material.schedule || '-',
grade: material.full_material_grade || material.material_grade || '-',
length: calc.totalLength,
quantity: calc.purchaseCount,
unit: '본',
userRequirements: userReqText, // User 요구사항
length: calc.totalLengthWithAllowance, // 여유분 포함 총 길이 (mm)
quantity: calc.pipeCount, // 단관 개수
unit: `${calc.requiredStandardPipes}`, // 6m 표준 파이프 필요 본수
details: calc,
isPipe: true
};
@@ -96,18 +132,24 @@ const PipeMaterialsView = ({
return filtered;
};
// 전체 선택/해제
// 전체 선택/해제 (구매된 자재 제외)
const handleSelectAll = () => {
const filteredMaterials = getFilteredAndSortedMaterials();
if (selectedMaterials.size === filteredMaterials.length) {
const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id));
if (selectedMaterials.size === selectableMaterials.length) {
setSelectedMaterials(new Set());
} else {
setSelectedMaterials(new Set(filteredMaterials.map(m => m.id)));
setSelectedMaterials(new Set(selectableMaterials.map(m => m.id)));
}
};
// 개별 선택
// 개별 선택 (구매된 자재는 선택 불가)
const handleMaterialSelect = (materialId) => {
if (purchasedMaterials.has(materialId)) {
return; // 구매된 자재는 선택 불가
}
const newSelected = new Set(selectedMaterials);
if (newSelected.has(materialId)) {
newSelected.delete(materialId);
@@ -291,25 +333,35 @@ const PipeMaterialsView = ({
<div style={{
background: 'white',
borderRadius: '12px',
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
overflow: 'auto',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
maxHeight: '600px'
}}>
{/* 헤더 */}
{/* 테이블 내용 - 헤더와 본문이 함께 스크롤 */}
<div style={{
display: 'grid',
gridTemplateColumns: '50px 1fr 120px 120px 120px 150px 100px 80px 80px 200px',
gap: '16px',
padding: '16px',
background: '#f8fafc',
borderBottom: '1px solid #e2e8f0',
fontSize: '14px',
fontWeight: '600',
color: '#374151'
minWidth: '1200px'
}}>
{/* 헤더 */}
<div style={{
display: 'grid',
gridTemplateColumns: '50px 120px 120px 120px 150px 200px 120px 100px 120px 250px',
gap: '16px',
padding: '16px',
background: '#f8fafc',
borderBottom: '1px solid #e2e8f0',
fontSize: '14px',
fontWeight: '600',
color: '#374151',
textAlign: 'center'
}}>
<div>
<input
type="checkbox"
checked={selectedMaterials.size === filteredMaterials.length && filteredMaterials.length > 0}
checked={(() => {
const filteredMaterials = getFilteredAndSortedMaterials();
const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id));
return selectedMaterials.size === selectableMaterials.length && selectableMaterials.length > 0;
})()}
onChange={handleSelectAll}
style={{ cursor: 'pointer' }}
/>
@@ -326,18 +378,6 @@ const PipeMaterialsView = ({
>
Type
</FilterableHeader>
<FilterableHeader
sortKey="subtype"
filterKey="subtype"
sortConfig={sortConfig}
onSort={handleSort}
columnFilters={columnFilters}
onFilterChange={setColumnFilters}
showFilterDropdown={showFilterDropdown}
setShowFilterDropdown={setShowFilterDropdown}
>
Subtype
</FilterableHeader>
<FilterableHeader
sortKey="size"
filterKey="size"
@@ -374,6 +414,18 @@ const PipeMaterialsView = ({
>
Material Grade
</FilterableHeader>
<FilterableHeader
sortKey="userRequirements"
filterKey="userRequirements"
sortConfig={sortConfig}
onSort={handleSort}
columnFilters={columnFilters}
onFilterChange={setColumnFilters}
showFilterDropdown={showFilterDropdown}
setShowFilterDropdown={setShowFilterDropdown}
>
User Requirements
</FilterableHeader>
<FilterableHeader
sortKey="length"
filterKey="length"
@@ -384,7 +436,7 @@ const PipeMaterialsView = ({
showFilterDropdown={showFilterDropdown}
setShowFilterDropdown={setShowFilterDropdown}
>
Length (M)
Length (MM)
</FilterableHeader>
<FilterableHeader
sortKey="quantity"
@@ -396,14 +448,14 @@ const PipeMaterialsView = ({
showFilterDropdown={showFilterDropdown}
setShowFilterDropdown={setShowFilterDropdown}
>
Quantity
Quantity (EA)
</FilterableHeader>
<div>Unit</div>
<div>User Requirement</div>
<div>Purchase Unit</div>
<div>Additional Request</div>
</div>
{/* 데이터 행들 */}
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
{/* 데이터 행들 */}
<div>
{filteredMaterials.map((material, index) => {
const info = parsePipeInfo(material);
const isSelected = selectedMaterials.has(material.id);
@@ -414,12 +466,13 @@ const PipeMaterialsView = ({
key={material.id}
style={{
display: 'grid',
gridTemplateColumns: '50px 1fr 120px 120px 120px 150px 100px 80px 80px 200px',
gridTemplateColumns: '50px 120px 120px 120px 150px 200px 120px 100px 120px 250px',
gap: '16px',
padding: '16px',
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
background: isSelected ? '#eff6ff' : (isPurchased ? '#fef3c7' : 'white'),
transition: 'background 0.15s ease'
transition: 'background 0.15s ease',
textAlign: 'center'
}}
onMouseEnter={(e) => {
if (!isSelected && !isPurchased) {
@@ -437,11 +490,15 @@ const PipeMaterialsView = ({
type="checkbox"
checked={isSelected}
onChange={() => handleMaterialSelect(material.id)}
style={{ cursor: 'pointer' }}
disabled={isPurchased}
style={{
cursor: isPurchased ? 'not-allowed' : 'pointer',
opacity: isPurchased ? 0.5 : 1
}}
/>
</div>
<div style={{ fontSize: '14px', color: '#1f2937' }}>
PIPE
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '500' }}>
{info.type}
{isPurchased && (
<span style={{
marginLeft: '8px',
@@ -456,9 +513,6 @@ const PipeMaterialsView = ({
</span>
)}
</div>
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '500' }}>
{info.subtype}
</div>
<div style={{ fontSize: '14px', color: '#1f2937' }}>
{info.size}
</div>
@@ -468,13 +522,22 @@ const PipeMaterialsView = ({
<div style={{ fontSize: '14px', color: '#1f2937' }}>
{info.grade}
</div>
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'right' }}>
{info.length.toFixed(2)}
<div style={{
fontSize: '14px',
color: '#1f2937',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
{info.userRequirements}
</div>
<div style={{ fontSize: '14px', color: '#1f2937' }}>
{Math.round(info.length).toLocaleString()}
</div>
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
{info.quantity}
</div>
<div style={{ fontSize: '14px', color: '#6b7280' }}>
<div style={{ fontSize: '14px', color: '#6b7280', fontWeight: '500' }}>
{info.unit}
</div>
<div>
@@ -485,7 +548,7 @@ const PipeMaterialsView = ({
...userRequirements,
[material.id]: e.target.value
})}
placeholder="Enter requirement..."
placeholder="Enter additional request..."
style={{
width: '100%',
padding: '6px 8px',
@@ -498,6 +561,7 @@ const PipeMaterialsView = ({
</div>
);
})}
</div>
</div>
</div>
@@ -507,7 +571,6 @@ const PipeMaterialsView = ({
padding: '60px 20px',
color: '#64748b'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🔧</div>
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
No Pipe Materials Found
</div>

View File

@@ -359,7 +359,6 @@ const SupportMaterialsView = ({
padding: '60px 20px',
color: '#64748b'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🏗</div>
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
No Support Materials Found
</div>

View File

@@ -385,7 +385,6 @@ const ValveMaterialsView = ({
padding: '60px 20px',
color: '#64748b'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🚰</div>
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
No Valve Materials Found
</div>