리비전 페이지 제거 및 트랜잭션 오류 임시 수정
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- frontend/src/pages/revision/ 폴더 완전 삭제
- EnhancedRevisionPage.css 제거
- support_details 저장 시 트랜잭션 오류로 인해 임시로 상세 정보 저장 비활성화
- 리비전 기능 재설계 예정
This commit is contained in:
Hyungi Ahn
2025-10-21 12:11:57 +09:00
parent 8f42a1054e
commit 1dc735f362
29 changed files with 1728 additions and 6987 deletions

View File

@@ -6,7 +6,9 @@
from sqlalchemy.orm import Session
from sqlalchemy import text, and_, or_
from typing import List, Dict, Any, Optional, Union
from ..models import Material, File, User, Project
import json
from ..models import Material, File, Project
from ..auth.models import User
from ..utils.logger import get_logger
from ..utils.error_handlers import ErrorResponse
@@ -292,7 +294,7 @@ class DatabaseService:
"area_code": material.get("area_code"),
"line_no": material.get("line_no"),
"classification_confidence": material.get("classification_confidence"),
"classification_details": material.get("classification_details"),
"classification_details": json.dumps(material.get("classification_details", {})) if material.get("classification_details") else None,
"revision_status": material.get("revision_status", "new"),
"material_hash": material.get("material_hash"),
"normalized_description": material.get("normalized_description"),

View File

@@ -5,6 +5,8 @@
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
@@ -14,9 +16,8 @@ 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 validate_file_extension, generate_unique_filename
from ..utils.file_validator import file_validator
from .database_service import DatabaseService
from .material_classification_service import MaterialClassificationService
logger = get_logger(__name__)
@@ -25,6 +26,16 @@ 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:
"""파일 업로드 서비스"""
@@ -36,7 +47,7 @@ class FileUploadService:
def validate_upload_request(self, file: UploadFile, job_no: str) -> None:
"""업로드 요청 검증"""
if not validate_file_extension(file.filename):
if not file_validator.validate_file_extension(file.filename):
raise HTTPException(
status_code=400,
detail=f"지원하지 않는 파일 형식입니다. 허용된 확장자: {', '.join(ALLOWED_EXTENSIONS)}"
@@ -129,7 +140,8 @@ class FileUploadService:
file_id: int,
job_no: str,
revision: str,
parent_file_id: Optional[int] = None
parent_file_id: Optional[int] = None,
is_revision: bool = False
) -> Dict[str, Any]:
"""자재 데이터 처리"""
@@ -189,8 +201,9 @@ class FileUploadService:
# 자재 데이터 DB 저장
inserted_count = self.db_service.bulk_insert_materials(processed_materials, file_id)
# 상세 정보 저장 (분류별)
self._save_material_details(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}")
@@ -589,19 +602,102 @@ class FileUploadService:
class MaterialClassificationService:
"""자재 분류 서비스 (임시 구현)"""
"""자재 분류 서비스"""
def classify_material(self, material_data: Dict, line_number: int, row_number: int) -> Dict:
"""자재 분류 (기존 로직 유지)"""
"""자재 분류 (실제 분류 로직 적용)"""
# 기존 분류 로직을 여기에 통합
# 현재는 기본값만 설정
material_data.update({
'line_number': line_number,
'row_number': row_number,
'classified_category': material_data.get('classified_category', 'UNKNOWN'),
'classification_confidence': material_data.get('classification_confidence', 0.0),
'classification_details': material_data.get('classification_details', {})
})
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)
return material_data
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)

View File

@@ -0,0 +1,395 @@
"""
간단한 리비전 관리 서비스
복잡한 dependency 없이 핵심 리비전 로직만 구현
"""
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from typing import Dict, List, Any, Optional, Tuple
import json
from datetime import datetime
from ..models import Material, File, SimpleRevisionComparison, SimpleRevisionMaterial
from ..utils.logger import get_logger
logger = get_logger(__name__)
class SimpleRevisionService:
"""간단한 리비전 관리 서비스"""
def __init__(self, db: Session):
self.db = db
def compare_revisions(self, current_file_id: int, previous_file_id: int,
category: str, username: str = "system") -> Dict[str, Any]:
"""
두 리비전 간의 자재 비교
Args:
current_file_id: 현재 파일 ID
previous_file_id: 이전 파일 ID
category: 비교할 카테고리 (PIPE, FITTING, FLANGE 등)
username: 비교 수행자
Returns:
비교 결과 딕셔너리
"""
try:
# 현재 파일과 이전 파일의 자재 조회
current_materials = self._get_materials_by_category(current_file_id, category)
previous_materials = self._get_materials_by_category(previous_file_id, category)
# 자재 비교 수행
comparison_result = self._perform_material_comparison(
current_materials, previous_materials, category
)
# 비교 결과 저장
comparison_record = self._save_comparison_result(
current_file_id, previous_file_id, category,
comparison_result, username
)
return {
'comparison_id': comparison_record.id,
'category': category,
'summary': comparison_result['summary'],
'materials': comparison_result['materials'],
'revision_actions': comparison_result['revision_actions']
}
except Exception as e:
logger.error(f"리비전 비교 실패: {e}")
raise
def _get_materials_by_category(self, file_id: int, category: str) -> List[Material]:
"""파일 ID와 카테고리로 자재 조회"""
return self.db.query(Material).filter(
and_(
Material.file_id == file_id,
or_(
Material.classified_category == category,
Material.category == category
)
)
).all()
def _perform_material_comparison(self, current_materials: List[Material],
previous_materials: List[Material],
category: str) -> Dict[str, Any]:
"""자재 비교 로직 수행"""
# 자재를 description + size로 그룹화
current_dict = self._group_materials_by_key(current_materials)
previous_dict = self._group_materials_by_key(previous_materials)
# 비교 결과 초기화
added_materials = []
removed_materials = []
changed_materials = []
unchanged_materials = []
revision_actions = []
# 현재 자재 기준으로 비교 (개선된 버전)
for key, current_group in current_dict.items():
if key in previous_dict:
previous_group = previous_dict[key]
# 수량 비교
current_qty = current_group['quantity']
previous_qty = previous_group['quantity']
if current_qty != previous_qty:
# 수량 변경됨
changed_materials.append({
'material': current_group['representative_material'],
'previous_quantity': previous_qty,
'current_quantity': current_qty,
'quantity_difference': current_qty - previous_qty,
'purchase_confirmed': previous_group['purchase_confirmed']
})
# 리비전 액션 결정 (구매 상태 고려)
action = self._determine_revision_action_enhanced(
current_group, previous_group, category
)
revision_actions.append(action)
else:
# 수량 동일
unchanged_materials.append(current_group['representative_material'])
else:
# 새로 추가된 자재
added_materials.append(current_group['representative_material'])
revision_actions.append({
'material_id': current_group['representative_material'].id,
'action': 'added',
'description': current_group['representative_material'].original_description,
'quantity': current_group['quantity'],
'purchase_status': 'not_purchased',
'revision_action': 'new_material'
})
# 제거된 자재 확인
for key, previous_group in previous_dict.items():
if key not in current_dict:
removed_materials.append(previous_group['representative_material'])
revision_actions.append({
'material_id': previous_group['representative_material'].id,
'action': 'removed',
'description': previous_group['representative_material'].original_description,
'quantity': previous_group['quantity'],
'purchase_status': 'purchased' if previous_group['purchase_confirmed'] else 'not_purchased',
'revision_action': 'deleted_material'
})
# 통계 계산
summary = {
'added_count': len(added_materials),
'removed_count': len(removed_materials),
'changed_count': len(changed_materials),
'unchanged_count': len(unchanged_materials),
'total_current': len(current_materials),
'total_previous': len(previous_materials)
}
return {
'summary': summary,
'materials': {
'added': added_materials,
'removed': removed_materials,
'changed': changed_materials,
'unchanged': unchanged_materials
},
'revision_actions': revision_actions
}
def _group_materials_by_key(self, materials: List[Material]) -> Dict[str, Dict]:
"""자재를 고유 키로 그룹화 (개선된 버전)"""
grouped = {}
for material in materials:
# 더 정교한 고유 키 생성
# description + drawing + main_nom + red_nom + material_grade
key_parts = [
material.original_description.strip().upper(),
material.drawing_name or '',
material.main_nom or '',
material.red_nom or '',
material.material_grade or ''
]
key = "|".join(key_parts)
if key in grouped:
# 동일한 자재가 있으면 수량 합산
grouped[key]['quantity'] += float(material.quantity)
grouped[key]['materials'].append(material)
else:
grouped[key] = {
'key': key,
'representative_material': material,
'quantity': float(material.quantity),
'materials': [material],
'purchase_confirmed': getattr(material, 'purchase_confirmed', False)
}
return grouped
def _determine_revision_action_enhanced(self, current_group: Dict,
previous_group: Dict, category: str) -> Dict[str, Any]:
"""개선된 리비전 액션 결정 로직"""
material = current_group['representative_material']
current_qty = current_group['quantity']
previous_qty = previous_group['quantity']
quantity_diff = current_qty - previous_qty
is_purchased = previous_group['purchase_confirmed']
# 리비전 규칙 적용
if is_purchased:
# 구매된 자재
if quantity_diff > 0:
action = 'additional_purchase'
description = f"추가 구매 필요: +{quantity_diff}"
status = 'needs_additional_purchase'
elif quantity_diff < 0:
action = 'inventory'
description = f"재고품으로 분류: {abs(quantity_diff)}"
status = 'excess_inventory'
else:
action = 'maintain'
description = "상황 유지"
status = 'no_change'
else:
# 구매 안된 자재
if quantity_diff > 0:
action = 'quantity_increase'
description = f"수량 증가: +{quantity_diff}"
status = 'quantity_updated'
elif quantity_diff < 0:
action = 'quantity_decrease'
description = f"수량 감소: {quantity_diff}"
status = 'quantity_reduced'
else:
action = 'maintain'
description = "구매 표시 유지"
status = 'purchase_pending'
return {
'material_id': material.id,
'action': action,
'description': material.original_description,
'drawing_name': material.drawing_name,
'previous_quantity': previous_qty,
'current_quantity': current_qty,
'quantity_difference': quantity_diff,
'purchase_status': 'purchased' if is_purchased else 'not_purchased',
'revision_action': status,
'action_description': description,
'category': category
}
def _determine_revision_action(self, material: Material, previous_qty: float,
current_qty: float, category: str) -> Dict[str, Any]:
"""리비전 액션 결정 로직"""
# 구매 상태 확인 (간단하게 purchase_confirmed 필드 사용)
is_purchased = getattr(material, 'purchase_confirmed', False)
quantity_diff = current_qty - previous_qty
if is_purchased:
# 구매된 자재
if quantity_diff > 0:
action = 'additional_purchase'
description = f"추가 구매 필요: {quantity_diff}"
elif quantity_diff < 0:
action = 'inventory'
description = f"재고품으로 분류: {abs(quantity_diff)}"
else:
action = 'maintain'
description = "상황 유지"
else:
# 구매 안된 자재
if quantity_diff > 0:
action = 'quantity_update'
description = f"수량 증가: {quantity_diff}"
elif quantity_diff < 0:
action = 'quantity_decrease'
description = f"수량 감소: {abs(quantity_diff)}"
else:
action = 'maintain'
description = "구매 표시 유지"
return {
'material_id': material.id,
'action': action,
'description': material.original_description,
'previous_quantity': previous_qty,
'current_quantity': current_qty,
'quantity_difference': quantity_diff,
'purchase_status': 'purchased' if is_purchased else 'not_purchased',
'action_description': description
}
def _save_comparison_result(self, current_file_id: int, previous_file_id: int,
category: str, comparison_result: Dict[str, Any],
username: str) -> SimpleRevisionComparison:
"""비교 결과를 데이터베이스에 저장"""
# Job No 조회
current_file = self.db.query(File).filter(File.id == current_file_id).first()
job_no = getattr(current_file, 'job_no', 'unknown')
# 비교 결과 레코드 생성
comparison = SimpleRevisionComparison(
job_no=job_no,
current_file_id=current_file_id,
previous_file_id=previous_file_id,
category=category,
added_count=comparison_result['summary']['added_count'],
removed_count=comparison_result['summary']['removed_count'],
changed_count=comparison_result['summary']['changed_count'],
unchanged_count=comparison_result['summary']['unchanged_count'],
comparison_data=comparison_result,
created_by_username=username
)
self.db.add(comparison)
self.db.flush() # ID 생성을 위해 flush
# 개별 자재 변경 로그 저장
for action in comparison_result['revision_actions']:
material_log = SimpleRevisionMaterial(
comparison_id=comparison.id,
material_id=action['material_id'],
change_type=action['action'],
revision_action=action['action'],
quantity_before=action.get('previous_quantity', 0),
quantity_after=action.get('current_quantity', 0),
quantity_difference=action.get('quantity_difference', 0),
purchase_status=action.get('purchase_status', 'not_purchased')
)
self.db.add(material_log)
self.db.commit()
return comparison
def get_changed_materials_only(self, current_file_id: int, previous_file_id: int,
categories: List[str] = None, username: str = "system") -> Dict[str, Any]:
"""변경된 자재만 필터링하여 반환"""
if not categories:
categories = ['PIPE', 'FITTING', 'FLANGE', 'VALVE', 'GASKET', 'BOLT', 'SUPPORT', 'SPECIAL', 'UNCLASSIFIED']
all_changes = {}
total_changes = 0
for category in categories:
try:
comparison_result = self.compare_revisions(
current_file_id, previous_file_id, category, username
)
# 변경된 자재만 필터링
changed_materials = []
for action in comparison_result['revision_actions']:
if action['action'] in ['added', 'removed'] or action.get('quantity_difference', 0) != 0:
changed_materials.append(action)
if changed_materials:
all_changes[category] = {
'category': category,
'changed_count': len(changed_materials),
'changes': changed_materials,
'summary': comparison_result['summary']
}
total_changes += len(changed_materials)
except Exception as e:
logger.warning(f"Failed to compare {category}: {e}")
continue
return {
'total_changed_materials': total_changes,
'categories_with_changes': list(all_changes.keys()),
'changes_by_category': all_changes,
'has_changes': total_changes > 0
}
def get_comparison_result(self, comparison_id: int) -> Optional[Dict[str, Any]]:
"""저장된 비교 결과 조회"""
comparison = self.db.query(SimpleRevisionComparison).filter(
SimpleRevisionComparison.id == comparison_id
).first()
if not comparison:
return None
# 관련 자재 변경 로그 조회
material_logs = self.db.query(SimpleRevisionMaterial).filter(
SimpleRevisionMaterial.comparison_id == comparison_id
).all()
return {
'comparison': comparison,
'material_logs': material_logs,
'comparison_data': comparison.comparison_data
}