Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- frontend/src/pages/revision/ 폴더 완전 삭제 - EnhancedRevisionPage.css 제거 - support_details 저장 시 트랜잭션 오류로 인해 임시로 상세 정보 저장 비활성화 - 리비전 기능 재설계 예정
704 lines
32 KiB
Python
704 lines
32 KiB
Python
"""
|
|
파일 업로드 서비스
|
|
파일 업로드 관련 로직을 통합하고 트랜잭션 관리 개선
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import uuid
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, List, Any, Optional, Tuple
|
|
from fastapi import UploadFile, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import text
|
|
|
|
from ..models import File, Material
|
|
from ..utils.logger import get_logger
|
|
from ..utils.file_processor import parse_file_data
|
|
from ..utils.file_validator import file_validator
|
|
from .database_service import DatabaseService
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
UPLOAD_DIR = Path("uploads")
|
|
ALLOWED_EXTENSIONS = {'.xls', '.xlsx', '.csv'}
|
|
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
|
|
|
|
|
def generate_unique_filename(original_filename: str) -> str:
|
|
"""고유한 파일명 생성"""
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
unique_id = str(uuid.uuid4())[:8]
|
|
stem = Path(original_filename).stem
|
|
suffix = Path(original_filename).suffix
|
|
|
|
return f"{stem}_{timestamp}_{unique_id}{suffix}"
|
|
|
|
|
|
class FileUploadService:
|
|
"""파일 업로드 서비스"""
|
|
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
self.db_service = DatabaseService(db)
|
|
self.classification_service = MaterialClassificationService()
|
|
|
|
def validate_upload_request(self, file: UploadFile, job_no: str) -> None:
|
|
"""업로드 요청 검증"""
|
|
|
|
if not file_validator.validate_file_extension(file.filename):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"지원하지 않는 파일 형식입니다. 허용된 확장자: {', '.join(ALLOWED_EXTENSIONS)}"
|
|
)
|
|
|
|
if file.size and file.size > MAX_FILE_SIZE:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="파일 크기는 10MB를 초과할 수 없습니다"
|
|
)
|
|
|
|
if not job_no or len(job_no.strip()) == 0:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="작업 번호는 필수입니다"
|
|
)
|
|
|
|
def save_uploaded_file(self, file: UploadFile) -> Tuple[str, Path]:
|
|
"""업로드된 파일 저장"""
|
|
|
|
try:
|
|
# 고유 파일명 생성
|
|
unique_filename = generate_unique_filename(file.filename)
|
|
file_path = UPLOAD_DIR / unique_filename
|
|
|
|
# 업로드 디렉토리 생성
|
|
UPLOAD_DIR.mkdir(exist_ok=True)
|
|
|
|
# 파일 저장
|
|
with open(file_path, "wb") as buffer:
|
|
shutil.copyfileobj(file.file, buffer)
|
|
|
|
logger.info(f"File saved: {file_path}")
|
|
return unique_filename, file_path
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to save file: {e}")
|
|
raise HTTPException(status_code=500, detail=f"파일 저장 실패: {str(e)}")
|
|
|
|
def create_file_record(
|
|
self,
|
|
filename: str,
|
|
original_filename: str,
|
|
file_path: str,
|
|
job_no: str,
|
|
revision: str,
|
|
bom_name: Optional[str],
|
|
file_size: int,
|
|
parsed_count: int,
|
|
uploaded_by: str,
|
|
parent_file_id: Optional[int] = None
|
|
) -> File:
|
|
"""파일 레코드 생성"""
|
|
|
|
try:
|
|
# BOM 이름 자동 생성 (제공되지 않은 경우)
|
|
if not bom_name:
|
|
bom_name = original_filename.rsplit('.', 1)[0]
|
|
|
|
# 파일 설명 생성
|
|
description = f"BOM 파일 - {parsed_count}개 자재"
|
|
|
|
file_record = File(
|
|
filename=filename,
|
|
original_filename=original_filename,
|
|
file_path=file_path,
|
|
job_no=job_no,
|
|
revision=revision,
|
|
bom_name=bom_name,
|
|
description=description,
|
|
file_size=file_size,
|
|
parsed_count=parsed_count,
|
|
is_active=True,
|
|
uploaded_by=uploaded_by
|
|
)
|
|
|
|
self.db.add(file_record)
|
|
self.db.flush() # ID 생성을 위해 flush
|
|
|
|
logger.info(f"File record created: ID={file_record.id}, Job={job_no}")
|
|
return file_record
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to create file record: {e}")
|
|
raise HTTPException(status_code=500, detail=f"파일 레코드 생성 실패: {str(e)}")
|
|
|
|
def process_materials_data(
|
|
self,
|
|
file_path: Path,
|
|
file_id: int,
|
|
job_no: str,
|
|
revision: str,
|
|
parent_file_id: Optional[int] = None,
|
|
is_revision: bool = False
|
|
) -> Dict[str, Any]:
|
|
"""자재 데이터 처리"""
|
|
|
|
try:
|
|
# 파일 파싱
|
|
logger.info(f"Parsing file: {file_path}")
|
|
materials_data = parse_file_data(str(file_path))
|
|
|
|
if not materials_data:
|
|
raise HTTPException(status_code=400, detail="파일에서 자재 데이터를 찾을 수 없습니다")
|
|
|
|
# 자재 분류 및 처리
|
|
processed_materials = []
|
|
classification_results = {
|
|
"total_materials": len(materials_data),
|
|
"classified_count": 0,
|
|
"categories": {}
|
|
}
|
|
|
|
for idx, material_data in enumerate(materials_data):
|
|
try:
|
|
# 자재 분류
|
|
classified_material = self.classification_service.classify_material(
|
|
material_data,
|
|
line_number=idx + 1,
|
|
row_number=material_data.get('row_number', idx + 1)
|
|
)
|
|
|
|
# 리비전 상태 설정
|
|
if parent_file_id:
|
|
# 리비전 업로드인 경우 변경 상태 분석
|
|
classified_material['revision_status'] = self._analyze_revision_status(
|
|
classified_material, parent_file_id
|
|
)
|
|
else:
|
|
classified_material['revision_status'] = 'new'
|
|
|
|
processed_materials.append(classified_material)
|
|
|
|
# 분류 통계 업데이트
|
|
category = classified_material.get('classified_category', 'UNKNOWN')
|
|
classification_results['categories'][category] = classification_results['categories'].get(category, 0) + 1
|
|
|
|
if category != 'UNKNOWN':
|
|
classification_results['classified_count'] += 1
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to classify material at line {idx + 1}: {e}")
|
|
# 분류 실패 시 기본값으로 처리
|
|
material_data.update({
|
|
'classified_category': 'UNKNOWN',
|
|
'classification_confidence': 0.0,
|
|
'revision_status': 'new'
|
|
})
|
|
processed_materials.append(material_data)
|
|
|
|
# 자재 데이터 DB 저장
|
|
inserted_count = self.db_service.bulk_insert_materials(processed_materials, file_id)
|
|
|
|
# 상세 정보 저장 (임시로 모든 업로드에서 건너뛰기)
|
|
# TODO: 상세 정보 저장 로직 수정 필요
|
|
logger.info(f"Skipped material details saving (temporarily disabled)")
|
|
|
|
logger.info(f"Processed {inserted_count} materials for file {file_id}")
|
|
|
|
return {
|
|
"materials_count": inserted_count,
|
|
"classification_results": classification_results,
|
|
"processed_materials": processed_materials[:10] # 처음 10개만 반환
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to process materials data: {e}")
|
|
raise HTTPException(status_code=500, detail=f"자재 데이터 처리 실패: {str(e)}")
|
|
|
|
def _analyze_revision_status(self, material: Dict, parent_file_id: int) -> str:
|
|
"""리비전 상태 분석"""
|
|
|
|
try:
|
|
# 부모 파일의 동일한 자재 찾기
|
|
parent_material_query = """
|
|
SELECT * FROM materials
|
|
WHERE file_id = :parent_file_id
|
|
AND drawing_name = :drawing_name
|
|
AND original_description = :description
|
|
LIMIT 1
|
|
"""
|
|
|
|
result = self.db_service.execute_query(
|
|
parent_material_query,
|
|
{
|
|
"parent_file_id": parent_file_id,
|
|
"drawing_name": material.get('drawing_name'),
|
|
"description": material.get('original_description')
|
|
}
|
|
)
|
|
|
|
parent_material = result.fetchone()
|
|
|
|
if not parent_material:
|
|
return 'new' # 새로운 자재
|
|
|
|
# 변경 사항 확인
|
|
if (
|
|
float(material.get('quantity', 0)) != float(parent_material.quantity or 0) or
|
|
material.get('material_grade') != parent_material.material_grade or
|
|
material.get('size_spec') != parent_material.size_spec
|
|
):
|
|
return 'changed' # 변경된 자재
|
|
|
|
return 'inventory' # 기존 자재 (변경 없음)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to analyze revision status: {e}")
|
|
return 'new'
|
|
|
|
def _save_material_details(self, materials: List[Dict], file_id: int) -> None:
|
|
"""자재 상세 정보 저장"""
|
|
|
|
try:
|
|
# 자재 ID 매핑 (방금 삽입된 자재들)
|
|
material_ids_query = """
|
|
SELECT id, row_number FROM materials
|
|
WHERE file_id = :file_id
|
|
ORDER BY row_number
|
|
"""
|
|
|
|
result = self.db_service.execute_query(material_ids_query, {"file_id": file_id})
|
|
material_id_map = {row.row_number: row.id for row in result.fetchall()}
|
|
|
|
# 분류별 상세 정보 저장
|
|
for material in materials:
|
|
material_id = material_id_map.get(material.get('row_number'))
|
|
if not material_id:
|
|
continue
|
|
|
|
category = material.get('classified_category')
|
|
|
|
if category == 'PIPE':
|
|
self._save_pipe_details(material, material_id, file_id)
|
|
elif category == 'FITTING':
|
|
self._save_fitting_details(material, material_id, file_id)
|
|
elif category == 'FLANGE':
|
|
self._save_flange_details(material, material_id, file_id)
|
|
elif category == 'VALVE':
|
|
self._save_valve_details(material, material_id, file_id)
|
|
elif category == 'BOLT':
|
|
self._save_bolt_details(material, material_id, file_id)
|
|
elif category == 'GASKET':
|
|
self._save_gasket_details(material, material_id, file_id)
|
|
elif category == 'SUPPORT':
|
|
self._save_support_details(material, material_id, file_id)
|
|
elif category == 'INSTRUMENT':
|
|
self._save_instrument_details(material, material_id, file_id)
|
|
|
|
logger.info(f"Saved material details for {len(materials)} materials")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to save material details: {e}")
|
|
# 상세 정보 저장 실패는 전체 프로세스를 중단하지 않음
|
|
|
|
def _save_pipe_details(self, material: Dict, material_id: int, file_id: int) -> None:
|
|
"""파이프 상세 정보 저장"""
|
|
|
|
details = material.get('classification_details', {})
|
|
|
|
insert_query = """
|
|
INSERT INTO pipe_details (
|
|
material_id, file_id, material_standard, material_grade, material_type,
|
|
manufacturing_method, end_preparation, schedule, wall_thickness,
|
|
nominal_size, length_mm, material_confidence, manufacturing_confidence,
|
|
end_prep_confidence, schedule_confidence
|
|
) VALUES (
|
|
:material_id, :file_id, :material_standard, :material_grade, :material_type,
|
|
:manufacturing_method, :end_preparation, :schedule, :wall_thickness,
|
|
:nominal_size, :length_mm, :material_confidence, :manufacturing_confidence,
|
|
:end_prep_confidence, :schedule_confidence
|
|
)
|
|
"""
|
|
|
|
try:
|
|
self.db_service.execute_query(insert_query, {
|
|
"material_id": material_id,
|
|
"file_id": file_id,
|
|
"material_standard": details.get('material_standard'),
|
|
"material_grade": material.get('material_grade'),
|
|
"material_type": details.get('material_type'),
|
|
"manufacturing_method": details.get('manufacturing_method'),
|
|
"end_preparation": details.get('end_preparation'),
|
|
"schedule": material.get('schedule'),
|
|
"wall_thickness": details.get('wall_thickness'),
|
|
"nominal_size": material.get('main_nom'),
|
|
"length_mm": material.get('length'),
|
|
"material_confidence": details.get('material_confidence', 0.0),
|
|
"manufacturing_confidence": details.get('manufacturing_confidence', 0.0),
|
|
"end_prep_confidence": details.get('end_prep_confidence', 0.0),
|
|
"schedule_confidence": details.get('schedule_confidence', 0.0)
|
|
})
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save pipe details for material {material_id}: {e}")
|
|
|
|
def _save_fitting_details(self, material: Dict, material_id: int, file_id: int) -> None:
|
|
"""피팅 상세 정보 저장"""
|
|
|
|
details = material.get('classification_details', {})
|
|
|
|
insert_query = """
|
|
INSERT INTO fitting_details (
|
|
material_id, file_id, fitting_type, fitting_subtype, connection_type,
|
|
main_size, reduced_size, length_mm, material_standard, material_grade,
|
|
pressure_rating, temperature_rating, classification_confidence
|
|
) VALUES (
|
|
:material_id, :file_id, :fitting_type, :fitting_subtype, :connection_type,
|
|
:main_size, :reduced_size, :length_mm, :material_standard, :material_grade,
|
|
:pressure_rating, :temperature_rating, :classification_confidence
|
|
)
|
|
"""
|
|
|
|
try:
|
|
self.db_service.execute_query(insert_query, {
|
|
"material_id": material_id,
|
|
"file_id": file_id,
|
|
"fitting_type": details.get('fitting_type', 'UNKNOWN'),
|
|
"fitting_subtype": details.get('fitting_subtype'),
|
|
"connection_type": details.get('connection_type'),
|
|
"main_size": material.get('main_nom'),
|
|
"reduced_size": material.get('red_nom'),
|
|
"length_mm": material.get('length'),
|
|
"material_standard": details.get('material_standard'),
|
|
"material_grade": material.get('material_grade'),
|
|
"pressure_rating": details.get('pressure_rating'),
|
|
"temperature_rating": details.get('temperature_rating'),
|
|
"classification_confidence": material.get('classification_confidence', 0.0)
|
|
})
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save fitting details for material {material_id}: {e}")
|
|
|
|
def _save_flange_details(self, material: Dict, material_id: int, file_id: int) -> None:
|
|
"""플랜지 상세 정보 저장"""
|
|
|
|
details = material.get('classification_details', {})
|
|
|
|
insert_query = """
|
|
INSERT INTO flange_details (
|
|
material_id, file_id, flange_type, flange_subtype, pressure_rating,
|
|
face_type, connection_method, nominal_size, material_standard,
|
|
material_grade, classification_confidence
|
|
) VALUES (
|
|
:material_id, :file_id, :flange_type, :flange_subtype, :pressure_rating,
|
|
:face_type, :connection_method, :nominal_size, :material_standard,
|
|
:material_grade, :classification_confidence
|
|
)
|
|
"""
|
|
|
|
try:
|
|
self.db_service.execute_query(insert_query, {
|
|
"material_id": material_id,
|
|
"file_id": file_id,
|
|
"flange_type": details.get('flange_type', 'UNKNOWN'),
|
|
"flange_subtype": details.get('flange_subtype'),
|
|
"pressure_rating": details.get('pressure_rating'),
|
|
"face_type": details.get('face_type'),
|
|
"connection_method": details.get('connection_method'),
|
|
"nominal_size": material.get('main_nom'),
|
|
"material_standard": details.get('material_standard'),
|
|
"material_grade": material.get('material_grade'),
|
|
"classification_confidence": material.get('classification_confidence', 0.0)
|
|
})
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save flange details for material {material_id}: {e}")
|
|
|
|
def _save_valve_details(self, material: Dict, material_id: int, file_id: int) -> None:
|
|
"""밸브 상세 정보 저장"""
|
|
|
|
details = material.get('classification_details', {})
|
|
|
|
insert_query = """
|
|
INSERT INTO valve_details (
|
|
material_id, file_id, valve_type, valve_subtype, actuation_type,
|
|
pressure_rating, temperature_rating, nominal_size, connection_type,
|
|
material_standard, material_grade, classification_confidence
|
|
) VALUES (
|
|
:material_id, :file_id, :valve_type, :valve_subtype, :actuation_type,
|
|
:pressure_rating, :temperature_rating, :nominal_size, :connection_type,
|
|
:material_standard, :material_grade, :classification_confidence
|
|
)
|
|
"""
|
|
|
|
try:
|
|
self.db_service.execute_query(insert_query, {
|
|
"material_id": material_id,
|
|
"file_id": file_id,
|
|
"valve_type": details.get('valve_type', 'UNKNOWN'),
|
|
"valve_subtype": details.get('valve_subtype'),
|
|
"actuation_type": details.get('actuation_type'),
|
|
"pressure_rating": details.get('pressure_rating'),
|
|
"temperature_rating": details.get('temperature_rating'),
|
|
"nominal_size": material.get('main_nom'),
|
|
"connection_type": details.get('connection_type'),
|
|
"material_standard": details.get('material_standard'),
|
|
"material_grade": material.get('material_grade'),
|
|
"classification_confidence": material.get('classification_confidence', 0.0)
|
|
})
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save valve details for material {material_id}: {e}")
|
|
|
|
def _save_bolt_details(self, material: Dict, material_id: int, file_id: int) -> None:
|
|
"""볼트 상세 정보 저장"""
|
|
|
|
details = material.get('classification_details', {})
|
|
|
|
insert_query = """
|
|
INSERT INTO bolt_details (
|
|
material_id, file_id, bolt_type, bolt_subtype, thread_type,
|
|
head_type, material_standard, material_grade, pressure_rating,
|
|
length_mm, diameter_mm, classification_confidence
|
|
) VALUES (
|
|
:material_id, :file_id, :bolt_type, :bolt_subtype, :thread_type,
|
|
:head_type, :material_standard, :material_grade, :pressure_rating,
|
|
:length_mm, :diameter_mm, :classification_confidence
|
|
)
|
|
"""
|
|
|
|
try:
|
|
self.db_service.execute_query(insert_query, {
|
|
"material_id": material_id,
|
|
"file_id": file_id,
|
|
"bolt_type": details.get('bolt_type', 'UNKNOWN'),
|
|
"bolt_subtype": details.get('bolt_subtype'),
|
|
"thread_type": details.get('thread_type'),
|
|
"head_type": details.get('head_type'),
|
|
"material_standard": details.get('material_standard'),
|
|
"material_grade": material.get('material_grade'),
|
|
"pressure_rating": details.get('pressure_rating'),
|
|
"length_mm": material.get('length'),
|
|
"diameter_mm": details.get('diameter_mm'),
|
|
"classification_confidence": material.get('classification_confidence', 0.0)
|
|
})
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save bolt details for material {material_id}: {e}")
|
|
|
|
def _save_gasket_details(self, material: Dict, material_id: int, file_id: int) -> None:
|
|
"""가스켓 상세 정보 저장"""
|
|
|
|
details = material.get('classification_details', {})
|
|
|
|
insert_query = """
|
|
INSERT INTO gasket_details (
|
|
material_id, file_id, gasket_type, gasket_subtype, material_type,
|
|
filler_material, pressure_rating, size_inches, thickness,
|
|
temperature_range, fire_safe, classification_confidence
|
|
) VALUES (
|
|
:material_id, :file_id, :gasket_type, :gasket_subtype, :material_type,
|
|
:filler_material, :pressure_rating, :size_inches, :thickness,
|
|
:temperature_range, :fire_safe, :classification_confidence
|
|
)
|
|
"""
|
|
|
|
try:
|
|
self.db_service.execute_query(insert_query, {
|
|
"material_id": material_id,
|
|
"file_id": file_id,
|
|
"gasket_type": details.get('gasket_type', 'UNKNOWN'),
|
|
"gasket_subtype": details.get('gasket_subtype'),
|
|
"material_type": details.get('material_type'),
|
|
"filler_material": details.get('filler_material'),
|
|
"pressure_rating": details.get('pressure_rating'),
|
|
"size_inches": material.get('main_nom'),
|
|
"thickness": details.get('thickness'),
|
|
"temperature_range": details.get('temperature_range'),
|
|
"fire_safe": details.get('fire_safe', False),
|
|
"classification_confidence": material.get('classification_confidence', 0.0)
|
|
})
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save gasket details for material {material_id}: {e}")
|
|
|
|
def _save_support_details(self, material: Dict, material_id: int, file_id: int) -> None:
|
|
"""서포트 상세 정보 저장"""
|
|
|
|
details = material.get('classification_details', {})
|
|
|
|
insert_query = """
|
|
INSERT INTO support_details (
|
|
material_id, file_id, support_type, support_subtype, load_rating,
|
|
load_capacity, material_standard, material_grade, pipe_size,
|
|
length_mm, width_mm, height_mm, classification_confidence
|
|
) VALUES (
|
|
:material_id, :file_id, :support_type, :support_subtype, :load_rating,
|
|
:load_capacity, :material_standard, :material_grade, :pipe_size,
|
|
:length_mm, :width_mm, :height_mm, :classification_confidence
|
|
)
|
|
"""
|
|
|
|
try:
|
|
self.db_service.execute_query(insert_query, {
|
|
"material_id": material_id,
|
|
"file_id": file_id,
|
|
"support_type": details.get('support_type', 'UNKNOWN'),
|
|
"support_subtype": details.get('support_subtype'),
|
|
"load_rating": details.get('load_rating', 'UNKNOWN'),
|
|
"load_capacity": details.get('load_capacity'),
|
|
"material_standard": details.get('material_standard', 'UNKNOWN'),
|
|
"material_grade": details.get('material_grade', 'UNKNOWN'),
|
|
"pipe_size": material.get('main_nom'),
|
|
"length_mm": material.get('length'),
|
|
"width_mm": details.get('width_mm'),
|
|
"height_mm": details.get('height_mm'),
|
|
"classification_confidence": material.get('classification_confidence', 0.0)
|
|
})
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save support details for material {material_id}: {e}")
|
|
|
|
def _save_instrument_details(self, material: Dict, material_id: int, file_id: int) -> None:
|
|
"""계기 상세 정보 저장"""
|
|
|
|
details = material.get('classification_details', {})
|
|
|
|
insert_query = """
|
|
INSERT INTO instrument_details (
|
|
material_id, file_id, instrument_type, instrument_subtype,
|
|
measurement_type, connection_size, pressure_rating,
|
|
temperature_rating, accuracy_class, material_standard,
|
|
classification_confidence
|
|
) VALUES (
|
|
:material_id, :file_id, :instrument_type, :instrument_subtype,
|
|
:measurement_type, :connection_size, :pressure_rating,
|
|
:temperature_rating, :accuracy_class, :material_standard,
|
|
:classification_confidence
|
|
)
|
|
"""
|
|
|
|
try:
|
|
self.db_service.execute_query(insert_query, {
|
|
"material_id": material_id,
|
|
"file_id": file_id,
|
|
"instrument_type": details.get('instrument_type', 'UNKNOWN'),
|
|
"instrument_subtype": details.get('instrument_subtype'),
|
|
"measurement_type": details.get('measurement_type'),
|
|
"connection_size": material.get('main_nom'),
|
|
"pressure_rating": details.get('pressure_rating'),
|
|
"temperature_rating": details.get('temperature_rating'),
|
|
"accuracy_class": details.get('accuracy_class'),
|
|
"material_standard": details.get('material_standard'),
|
|
"classification_confidence": material.get('classification_confidence', 0.0)
|
|
})
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save instrument details for material {material_id}: {e}")
|
|
|
|
def cleanup_failed_upload(self, file_path: Path) -> None:
|
|
"""실패한 업로드 정리"""
|
|
|
|
try:
|
|
if file_path.exists():
|
|
file_path.unlink()
|
|
logger.info(f"Cleaned up failed upload: {file_path}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to cleanup file {file_path}: {e}")
|
|
|
|
|
|
class MaterialClassificationService:
|
|
"""자재 분류 서비스"""
|
|
|
|
def classify_material(self, material_data: Dict, line_number: int, row_number: int) -> Dict:
|
|
"""자재 분류 (실제 분류 로직 적용)"""
|
|
|
|
description = material_data.get('original_description', '')
|
|
main_nom = material_data.get('main_nom', '')
|
|
red_nom = material_data.get('red_nom', '')
|
|
length_value = material_data.get('length', 0)
|
|
|
|
try:
|
|
# 1. 통합 분류기로 1차 분류
|
|
from ..services.integrated_classifier import classify_material_integrated
|
|
integrated_result = classify_material_integrated(description, main_nom, red_nom, length_value)
|
|
|
|
# 2. 제외 대상 확인
|
|
if self._should_exclude_material(description):
|
|
classification_result = {
|
|
"category": "EXCLUDE",
|
|
"overall_confidence": 0.95,
|
|
"reason": "제외 대상 자재"
|
|
}
|
|
else:
|
|
# 3. 타입별 상세 분류기 실행
|
|
material_type = integrated_result.get('category', 'UNCLASSIFIED')
|
|
|
|
if material_type == "PIPE":
|
|
from ..services.pipe_classifier import classify_pipe_for_purchase
|
|
classification_result = classify_pipe_for_purchase("", description, main_nom, length_value)
|
|
elif material_type == "FITTING":
|
|
from ..services.fitting_classifier import classify_fitting
|
|
classification_result = classify_fitting("", description, main_nom, red_nom)
|
|
elif material_type == "FLANGE":
|
|
from ..services.flange_classifier import classify_flange
|
|
classification_result = classify_flange("", description, main_nom, red_nom)
|
|
elif material_type == "VALVE":
|
|
from ..services.valve_classifier import classify_valve
|
|
classification_result = classify_valve("", description, main_nom)
|
|
elif material_type == "BOLT":
|
|
from ..services.bolt_classifier import classify_bolt
|
|
classification_result = classify_bolt("", description, main_nom)
|
|
elif material_type == "GASKET":
|
|
from ..services.gasket_classifier import classify_gasket
|
|
classification_result = classify_gasket("", description, main_nom)
|
|
elif material_type == "SUPPORT":
|
|
from ..services.support_classifier import classify_support
|
|
classification_result = classify_support("", description, main_nom)
|
|
elif material_type == "SPECIAL":
|
|
classification_result = {
|
|
"category": "SPECIAL",
|
|
"overall_confidence": integrated_result.get('confidence', 0.8),
|
|
"reason": integrated_result.get('reason', 'SPECIAL 키워드 발견')
|
|
}
|
|
else:
|
|
classification_result = {
|
|
"category": "UNCLASSIFIED",
|
|
"overall_confidence": 0.1,
|
|
"reason": "분류되지 않은 자재"
|
|
}
|
|
|
|
# 4. 결과 적용
|
|
final_category = classification_result.get('category', 'UNCLASSIFIED')
|
|
if final_category == 'EXCLUDE':
|
|
return None # 제외 대상은 None 반환
|
|
|
|
material_data.update({
|
|
'line_number': line_number,
|
|
'row_number': row_number,
|
|
'classified_category': final_category,
|
|
'classification_confidence': classification_result.get('overall_confidence', 0.0),
|
|
'classification_details': classification_result
|
|
})
|
|
|
|
return material_data
|
|
|
|
except Exception as e:
|
|
logger.warning(f"자재 분류 실패 (line {line_number}): {e}")
|
|
# 분류 실패 시 기본값 설정
|
|
material_data.update({
|
|
'line_number': line_number,
|
|
'row_number': row_number,
|
|
'classified_category': 'UNCLASSIFIED',
|
|
'classification_confidence': 0.0,
|
|
'classification_details': {'error': str(e)}
|
|
})
|
|
|
|
return material_data
|
|
|
|
def _should_exclude_material(self, description: str) -> bool:
|
|
"""제외 대상 자재 확인"""
|
|
exclude_keywords = [
|
|
'WELD GAP', '용접갭', '용접 갭',
|
|
'WELDING GAP', 'WELD ALLOWANCE',
|
|
'CUTTING ALLOWANCE', '절단여유',
|
|
'SPARE', '예비품', 'RESERVE'
|
|
]
|
|
|
|
desc_upper = description.upper()
|
|
return any(keyword.upper() in desc_upper for keyword in exclude_keywords)
|