feat: BOM 페이지 개선 및 구매관리 기능 향상
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
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:
@@ -7,6 +7,60 @@ import re
|
||||
from typing import Dict, List, Optional
|
||||
from .material_classifier import classify_material, get_manufacturing_method_from_material
|
||||
|
||||
# ========== PIPE USER 요구사항 키워드 ==========
|
||||
PIPE_USER_REQUIREMENTS = {
|
||||
"IMPACT_TEST": {
|
||||
"keywords": ["IMPACT", "충격시험", "CHARPY", "CVN", "IMPACT TEST", "충격", "NOTCH"],
|
||||
"description": "충격시험 요구",
|
||||
"confidence": 0.95
|
||||
},
|
||||
"ASME_CODE": {
|
||||
"keywords": ["ASME", "ASME CODE", "CODE", "B31.1", "B31.3", "B31.4", "B31.8", "VIII"],
|
||||
"description": "ASME 코드 준수",
|
||||
"confidence": 0.95
|
||||
},
|
||||
"STRESS_RELIEF": {
|
||||
"keywords": ["STRESS RELIEF", "SR", "응력제거", "열처리", "HEAT TREATMENT"],
|
||||
"description": "응력제거 열처리",
|
||||
"confidence": 0.90
|
||||
},
|
||||
"RADIOGRAPHIC_TEST": {
|
||||
"keywords": ["RT", "RADIOGRAPHIC", "방사선시험", "X-RAY", "엑스레이"],
|
||||
"description": "방사선 시험",
|
||||
"confidence": 0.90
|
||||
},
|
||||
"ULTRASONIC_TEST": {
|
||||
"keywords": ["UT", "ULTRASONIC", "초음파시험", "초음파"],
|
||||
"description": "초음파 시험",
|
||||
"confidence": 0.90
|
||||
},
|
||||
"MAGNETIC_PARTICLE": {
|
||||
"keywords": ["MT", "MAGNETIC PARTICLE", "자분탐상", "자분"],
|
||||
"description": "자분탐상 시험",
|
||||
"confidence": 0.90
|
||||
},
|
||||
"LIQUID_PENETRANT": {
|
||||
"keywords": ["PT", "LIQUID PENETRANT", "침투탐상", "침투"],
|
||||
"description": "침투탐상 시험",
|
||||
"confidence": 0.90
|
||||
},
|
||||
"HYDROSTATIC_TEST": {
|
||||
"keywords": ["HYDROSTATIC", "수압시험", "PRESSURE TEST", "압력시험"],
|
||||
"description": "수압 시험",
|
||||
"confidence": 0.90
|
||||
},
|
||||
"LOW_TEMPERATURE": {
|
||||
"keywords": ["LOW TEMP", "저온", "LTCS", "LOW TEMPERATURE", "CRYOGENIC"],
|
||||
"description": "저온용",
|
||||
"confidence": 0.85
|
||||
},
|
||||
"HIGH_TEMPERATURE": {
|
||||
"keywords": ["HIGH TEMP", "고온", "HTCS", "HIGH TEMPERATURE"],
|
||||
"description": "고온용",
|
||||
"confidence": 0.85
|
||||
}
|
||||
}
|
||||
|
||||
# ========== PIPE 제조 방법별 분류 ==========
|
||||
PIPE_MANUFACTURING = {
|
||||
"SEAMLESS": {
|
||||
@@ -138,6 +192,27 @@ PIPE_SCHEDULE = {
|
||||
]
|
||||
}
|
||||
|
||||
def extract_pipe_user_requirements(description: str) -> List[str]:
|
||||
"""
|
||||
파이프 설명에서 User 요구사항 추출
|
||||
|
||||
Args:
|
||||
description: 파이프 설명
|
||||
|
||||
Returns:
|
||||
발견된 요구사항 리스트
|
||||
"""
|
||||
desc_upper = description.upper()
|
||||
found_requirements = []
|
||||
|
||||
for req_type, req_data in PIPE_USER_REQUIREMENTS.items():
|
||||
for keyword in req_data["keywords"]:
|
||||
if keyword in desc_upper:
|
||||
found_requirements.append(req_data["description"])
|
||||
break # 같은 타입에서 중복 방지
|
||||
|
||||
return found_requirements
|
||||
|
||||
def classify_pipe_for_purchase(dat_file: str, description: str, main_nom: str,
|
||||
length: Optional[float] = None) -> Dict:
|
||||
"""구매용 파이프 분류 - 끝단 가공 정보 제외"""
|
||||
@@ -215,13 +290,16 @@ def classify_pipe(dat_file: str, description: str, main_nom: str,
|
||||
# 3. 끝 가공 분류
|
||||
end_prep_result = classify_pipe_end_preparation(description)
|
||||
|
||||
# 4. 스케줄 분류
|
||||
schedule_result = classify_pipe_schedule(description)
|
||||
# 4. 스케줄 분류 (재질 정보 전달)
|
||||
schedule_result = classify_pipe_schedule(description, material_result)
|
||||
|
||||
# 5. 길이(절단 치수) 처리
|
||||
length_info = extract_pipe_length_info(length, description)
|
||||
|
||||
# 6. 최종 결과 조합
|
||||
# 6. User 요구사항 추출
|
||||
user_requirements = extract_pipe_user_requirements(description)
|
||||
|
||||
# 7. 최종 결과 조합
|
||||
return {
|
||||
"category": "PIPE",
|
||||
|
||||
@@ -260,6 +338,9 @@ def classify_pipe(dat_file: str, description: str, main_nom: str,
|
||||
"length_mm": length_info.get('length_mm')
|
||||
},
|
||||
|
||||
# User 요구사항
|
||||
"user_requirements": user_requirements,
|
||||
|
||||
# 전체 신뢰도
|
||||
"overall_confidence": calculate_pipe_confidence({
|
||||
"material": material_result.get('confidence', 0),
|
||||
@@ -328,19 +409,43 @@ def classify_pipe_end_preparation(description: str) -> Dict:
|
||||
"matched_code": "DEFAULT"
|
||||
}
|
||||
|
||||
def classify_pipe_schedule(description: str) -> Dict:
|
||||
"""파이프 스케줄 분류"""
|
||||
def classify_pipe_schedule(description: str, material_result: Dict = None) -> Dict:
|
||||
"""파이프 스케줄 분류 - 재질별 표현 개선"""
|
||||
|
||||
desc_upper = description.upper()
|
||||
|
||||
# 재질 정보 확인
|
||||
material_type = "CARBON" # 기본값
|
||||
if material_result:
|
||||
material_grade = material_result.get('grade', '').upper()
|
||||
material_standard = material_result.get('standard', '').upper()
|
||||
|
||||
# 스테인리스 스틸 판단
|
||||
if any(sus_indicator in material_grade or sus_indicator in material_standard
|
||||
for sus_indicator in ['SUS', 'SS', 'A312', 'A358', 'A376', '304', '316', '321', '347']):
|
||||
material_type = "STAINLESS"
|
||||
|
||||
# 1. 스케줄 패턴 확인
|
||||
for pattern in PIPE_SCHEDULE["patterns"]:
|
||||
match = re.search(pattern, desc_upper)
|
||||
if match:
|
||||
schedule_num = match.group(1)
|
||||
|
||||
# 재질별 스케줄 표현
|
||||
if material_type == "STAINLESS":
|
||||
# 스테인리스 스틸: SCH 40S, SCH 80S
|
||||
if schedule_num in ["10", "20", "40", "80", "120", "160"]:
|
||||
schedule_display = f"SCH {schedule_num}S"
|
||||
else:
|
||||
schedule_display = f"SCH {schedule_num}"
|
||||
else:
|
||||
# 카본 스틸: SCH 40, SCH 80
|
||||
schedule_display = f"SCH {schedule_num}"
|
||||
|
||||
return {
|
||||
"schedule": f"SCH {schedule_num}",
|
||||
"schedule": schedule_display,
|
||||
"schedule_number": schedule_num,
|
||||
"material_type": material_type,
|
||||
"confidence": 0.95,
|
||||
"matched_pattern": pattern
|
||||
}
|
||||
@@ -353,6 +458,7 @@ def classify_pipe_schedule(description: str) -> Dict:
|
||||
return {
|
||||
"schedule": f"{thickness}mm THK",
|
||||
"wall_thickness": f"{thickness}mm",
|
||||
"material_type": material_type,
|
||||
"confidence": 0.9,
|
||||
"matched_pattern": pattern
|
||||
}
|
||||
@@ -360,6 +466,7 @@ def classify_pipe_schedule(description: str) -> Dict:
|
||||
# 3. 기본값
|
||||
return {
|
||||
"schedule": "UNKNOWN",
|
||||
"material_type": material_type,
|
||||
"confidence": 0.0
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -35,13 +35,13 @@ const BOMManagementPage = ({
|
||||
|
||||
// 카테고리 정의
|
||||
const categories = [
|
||||
{ key: 'PIPE', label: 'Pipes', icon: '🔧', color: '#3b82f6' },
|
||||
{ key: 'FITTING', label: 'Fittings', icon: '⚙️', color: '#10b981' },
|
||||
{ key: 'FLANGE', label: 'Flanges', icon: '🔩', color: '#f59e0b' },
|
||||
{ key: 'VALVE', label: 'Valves', icon: '🚰', color: '#ef4444' },
|
||||
{ key: 'GASKET', label: 'Gaskets', icon: '⭕', color: '#8b5cf6' },
|
||||
{ key: 'BOLT', label: 'Bolts', icon: '🔩', color: '#6b7280' },
|
||||
{ key: 'SUPPORT', label: 'Supports', icon: '🏗️', color: '#f97316' }
|
||||
{ key: 'PIPE', label: 'Pipes', color: '#3b82f6' },
|
||||
{ key: 'FITTING', label: 'Fittings', color: '#10b981' },
|
||||
{ key: 'FLANGE', label: 'Flanges', color: '#f59e0b' },
|
||||
{ key: 'VALVE', label: 'Valves', color: '#ef4444' },
|
||||
{ key: 'GASKET', label: 'Gaskets', color: '#8b5cf6' },
|
||||
{ key: 'BOLT', label: 'Bolts', color: '#6b7280' },
|
||||
{ key: 'SUPPORT', label: 'Supports', color: '#f97316' }
|
||||
];
|
||||
|
||||
// 자료 로드 함수들
|
||||
@@ -398,9 +398,6 @@ const BOMManagementPage = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '20px', marginBottom: '8px' }}>
|
||||
{category.icon}
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
{category.label}
|
||||
</div>
|
||||
|
||||
@@ -13,15 +13,23 @@ const PurchaseRequestPage = ({ onNavigate, fileId, jobNo, selectedProject }) =>
|
||||
|
||||
useEffect(() => {
|
||||
loadRequests();
|
||||
}, [fileId, jobNo]);
|
||||
}, [fileId, jobNo, selectedProject]);
|
||||
|
||||
const loadRequests = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const params = {};
|
||||
if (fileId) params.file_id = fileId;
|
||||
if (jobNo) params.job_no = jobNo;
|
||||
|
||||
// 선택된 프로젝트가 있으면 해당 프로젝트만 조회
|
||||
if (selectedProject) {
|
||||
params.job_no = selectedProject.job_no || selectedProject.official_project_code;
|
||||
} else if (jobNo) {
|
||||
params.job_no = jobNo;
|
||||
}
|
||||
|
||||
if (fileId) params.file_id = fileId;
|
||||
|
||||
console.log('🔍 구매신청 목록 조회:', params);
|
||||
const response = await api.get('/purchase-request/list', { params });
|
||||
setRequests(response.data.requests || []);
|
||||
} catch (error) {
|
||||
|
||||
@@ -161,13 +161,9 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
const manufacturingMethod = pipeDetails.manufacturing_method || '';
|
||||
const endPreparation = pipeDetails.end_preparation || '';
|
||||
|
||||
// 제조방법과 끝단처리 조합으로 상세 타입 생성
|
||||
if (manufacturingMethod && endPreparation) {
|
||||
itemName = `${manufacturingMethod} PIPE (${endPreparation})`;
|
||||
} else if (manufacturingMethod) {
|
||||
// 제조방법만으로 상세 타입 생성 (끝단처리 정보 제거)
|
||||
if (manufacturingMethod) {
|
||||
itemName = `${manufacturingMethod} PIPE`;
|
||||
} else if (endPreparation) {
|
||||
itemName = `PIPE (${endPreparation})`;
|
||||
} else {
|
||||
// description에서 제조방법 추출 시도
|
||||
const desc = cleanDescription.toUpperCase();
|
||||
@@ -666,17 +662,17 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
|
||||
// F~O열: 카테고리별 전용 컬럼 구성 (10개 컬럼)
|
||||
if (category === 'PIPE') {
|
||||
// 파이프 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
||||
// 파이프 전용 컬럼 (F~O) - 끝단처리, 압력등급 제거
|
||||
base['크기'] = material.size_spec || '-'; // F열
|
||||
base['압력등급'] = pressure; // G열
|
||||
base['스케줄'] = schedule; // H열
|
||||
base['재질'] = grade; // I열
|
||||
base['제조방법'] = material.pipe_details?.manufacturing_method || '-'; // J열
|
||||
base['끝단처리'] = material.pipe_details?.end_preparation || '-'; // K열
|
||||
base['사용자요구'] = material.user_requirement || ''; // L열
|
||||
base['관리항목1'] = ''; // M열
|
||||
base['관리항목2'] = ''; // N열
|
||||
base['관리항목3'] = ''; // O열
|
||||
base['스케줄'] = schedule; // G열
|
||||
base['재질'] = grade; // H열
|
||||
base['제조방법'] = material.pipe_details?.manufacturing_method || '-'; // I열
|
||||
base['사용자요구'] = material.user_requirements?.join(', ') || ''; // J열 (분류기에서 추출)
|
||||
base['추가요청사항'] = material.user_requirement || ''; // K열 (사용자 입력)
|
||||
base['관리항목1'] = ''; // L열
|
||||
base['관리항목2'] = ''; // M열
|
||||
base['관리항목3'] = ''; // N열
|
||||
base['관리항목4'] = ''; // O열
|
||||
} else if (category === 'FITTING') {
|
||||
// 피팅 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
||||
base['크기'] = material.size_spec || '-'; // F열
|
||||
|
||||
Reference in New Issue
Block a user