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 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
225 lines
8.1 KiB
Python
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
|