Files
TK-BOM-Project/backend/app/services/revision_comparison_service.py
Hyungi Ahn 8f42a1054e
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
🔧 완전한 스키마 자동화 시스템 구축
 주요 기능:
- 완전한 데이터베이스 스키마 분석 및 자동 마이그레이션 시스템
- 44개 테이블 완전 지원 (운영 서버 43개 + 1개 추가)
- 누락된 테이블/컬럼 자동 감지 및 생성

🔧 해결된 스키마 문제:
- users.status 컬럼 누락 → 자동 추가
- files 테이블 4개 컬럼 누락 → 자동 추가
- materials 테이블 22개 컬럼 누락 → 자동 추가
- support_details, purchase_requests, purchase_request_items 테이블 누락 → 자동 생성
- material_purchase_tracking.description, purchase_status 컬럼 누락 → 자동 추가

🚀 자동화 도구:
- schema_analyzer.py: 코드와 DB 스키마 비교 분석
- auto_migrator.py: 자동 마이그레이션 실행
- docker_migrator.py: Docker 환경용 간편 마이그레이션
- schema_monitor.py: 실시간 스키마 모니터링

📋 리비전 관리 시스템:
- 8개 카테고리별 리비전 페이지 구현
- PIPE Cutting Plan 관리 시스템
- PIPE Issue Management 시스템
- 완전한 리비전 비교 및 추적 기능

🎯 사용법:
docker exec tk-mp-backend python3 scripts/docker_migrator.py

앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
2025-10-21 10:34:45 +09:00

225 lines
8.1 KiB
Python

"""
리비전 비교 전용 서비스
두 리비전 간의 자재 비교 및 차이점 분석
"""
from sqlalchemy.orm import Session
from sqlalchemy import text
from typing import List, Dict, Any, Optional, Tuple
from decimal import Decimal
import hashlib
from datetime import datetime
from ..models import Material, File
from ..utils.logger import get_logger
from .database_service import DatabaseService
logger = get_logger(__name__)
class RevisionComparisonService:
"""리비전 비교 전용 서비스"""
def __init__(self, db: Session):
self.db = db
self.db_service = DatabaseService(db)
def compare_revisions(
self,
current_file_id: int,
previous_file_id: int,
category_filter: Optional[str] = None
) -> Dict[str, Any]:
"""
두 리비전 간 자재 비교
Args:
current_file_id: 현재 리비전 파일 ID
previous_file_id: 이전 리비전 파일 ID
category_filter: 특정 카테고리만 비교 (선택사항)
Returns:
비교 결과 딕셔너리
"""
# 이전/현재 자재 조회
previous_materials = self._get_materials_for_comparison(previous_file_id, category_filter)
current_materials = self._get_materials_for_comparison(current_file_id, category_filter)
# 비교 수행
comparison_result = {
"comparison_date": datetime.now().isoformat(),
"current_file_id": current_file_id,
"previous_file_id": previous_file_id,
"category_filter": category_filter,
"summary": {
"previous_count": len(previous_materials),
"current_count": len(current_materials),
"unchanged": 0,
"modified": 0,
"added": 0,
"removed": 0
},
"changes": {
"unchanged": [],
"modified": [],
"added": [],
"removed": []
}
}
# 이전 자재 기준으로 비교
for key, prev_material in previous_materials.items():
if key in current_materials:
curr_material = current_materials[key]
# 자재 변경 여부 확인
if self._is_material_changed(prev_material, curr_material):
comparison_result["changes"]["modified"].append({
"key": key,
"previous": prev_material,
"current": curr_material,
"changes": self._get_material_changes(prev_material, curr_material)
})
comparison_result["summary"]["modified"] += 1
else:
comparison_result["changes"]["unchanged"].append({
"key": key,
"material": curr_material
})
comparison_result["summary"]["unchanged"] += 1
else:
# 제거된 자재
comparison_result["changes"]["removed"].append({
"key": key,
"material": prev_material
})
comparison_result["summary"]["removed"] += 1
# 신규 자재
for key, curr_material in current_materials.items():
if key not in previous_materials:
comparison_result["changes"]["added"].append({
"key": key,
"material": curr_material
})
comparison_result["summary"]["added"] += 1
return comparison_result
def get_category_comparison(
self,
current_file_id: int,
previous_file_id: int,
category: str
) -> Dict[str, Any]:
"""특정 카테고리의 리비전 비교"""
return self.compare_revisions(current_file_id, previous_file_id, category)
# PIPE 관련 메서드는 별도 처리 예정
def _get_materials_for_comparison(
self,
file_id: int,
category_filter: Optional[str] = None
) -> Dict[str, Dict]:
"""비교용 자재 데이터 조회"""
query = """
SELECT
m.id, m.original_description, m.classified_category,
m.material_grade, m.schedule, m.size_spec, m.main_nom, m.red_nom,
m.quantity, m.unit, m.length, m.drawing_name, m.line_no,
m.purchase_confirmed, m.confirmed_quantity, m.purchase_status,
m.material_hash, m.revision_status, m.brand, m.user_requirement,
m.line_number, m.is_active,
-- 비교 키 생성 (PIPE 제외)
COALESCE(m.material_hash,
CONCAT(m.original_description, '|',
COALESCE(m.material_grade, ''), '|',
COALESCE(m.size_spec, ''))) as comparison_key
FROM materials m
WHERE m.file_id = :file_id AND m.is_active = true
"""
params = {"file_id": file_id}
if category_filter:
query += " AND m.classified_category = :category"
params["category"] = category_filter
# PIPE 카테고리는 제외
query += " AND m.classified_category != 'PIPE'"
query += " ORDER BY m.line_number"
result = self.db_service.execute_query(query, params)
materials = {}
for row in result.fetchall():
row_dict = dict(row._mapping)
comparison_key = row_dict['comparison_key']
# PIPE 제외한 일반 자재 처리
materials[comparison_key] = row_dict
return materials
def _is_material_changed(self, prev_material: Dict, curr_material: Dict) -> bool:
"""자재 변경 여부 확인"""
# 주요 필드 비교
compare_fields = ['quantity', 'material_grade', 'schedule', 'size_spec',
'main_nom', 'red_nom', 'unit', 'length']
for field in compare_fields:
prev_val = prev_material.get(field)
curr_val = curr_material.get(field)
# 수치 필드는 부동소수점 오차 고려
if field in ['quantity', 'length']:
if prev_val is not None and curr_val is not None:
if abs(float(prev_val) - float(curr_val)) > 0.001:
return True
elif prev_val != curr_val:
return True
else:
if prev_val != curr_val:
return True
return False
def _get_material_changes(self, prev_material: Dict, curr_material: Dict) -> Dict[str, Any]:
"""자재 변경 내용 상세 분석"""
changes = {}
compare_fields = ['quantity', 'material_grade', 'schedule', 'size_spec',
'main_nom', 'red_nom', 'unit', 'length']
for field in compare_fields:
prev_val = prev_material.get(field)
curr_val = curr_material.get(field)
if field in ['quantity', 'length']:
if prev_val is not None and curr_val is not None:
if abs(float(prev_val) - float(curr_val)) > 0.001:
changes[field] = {
"previous": float(prev_val),
"current": float(curr_val),
"change": float(curr_val) - float(prev_val)
}
elif prev_val != curr_val:
changes[field] = {
"previous": prev_val,
"current": curr_val
}
else:
if prev_val != curr_val:
changes[field] = {
"previous": prev_val,
"current": curr_val
}
return changes