Files
TK-BOM-Project/backend/app/services/enhanced_revision_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

489 lines
19 KiB
Python

"""
강화된 리비전 관리 서비스
구매 상태 기반 리비전 비교 및 처리
"""
from sqlalchemy.orm import Session
from sqlalchemy import text, and_, or_
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 EnhancedRevisionService:
"""강화된 리비전 관리 서비스"""
def __init__(self, db: Session):
self.db = db
self.db_service = DatabaseService(db)
def compare_revisions_with_purchase_status(
self,
job_no: str,
current_file_id: int,
previous_file_id: Optional[int] = None
) -> Dict[str, Any]:
"""
구매 상태를 고려한 리비전 비교
Args:
job_no: 작업 번호
current_file_id: 현재 파일 ID
previous_file_id: 이전 파일 ID (None이면 자동 탐지)
Returns:
비교 결과 딕셔너리
"""
if not previous_file_id:
previous_file_id = self._get_previous_file_id(job_no, current_file_id)
if not previous_file_id:
return self._handle_first_revision(current_file_id)
# 이전 리비전 자재 조회 (구매 상태 포함)
previous_materials = self._get_materials_with_purchase_status(previous_file_id)
# 현재 리비전 자재 조회
current_materials = self._get_materials_with_purchase_status(current_file_id)
# 자재별 비교 수행
comparison_result = self._perform_detailed_comparison(
previous_materials, current_materials, job_no
)
return comparison_result
def _get_materials_with_purchase_status(self, file_id: int) -> 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,
-- PIPE 자재 특별 키 생성
CASE
WHEN m.classified_category = 'PIPE' THEN
CONCAT(m.drawing_name, '|', m.line_no, '|', COALESCE(m.length, 0))
ELSE
m.material_hash
END as comparison_key
FROM materials m
WHERE m.file_id = :file_id
ORDER BY m.line_number
"""
result = self.db_service.execute_query(query, {"file_id": file_id})
materials = {}
for row in result.fetchall():
row_dict = dict(row._mapping)
comparison_key = row_dict['comparison_key']
# PIPE 자재의 경우 도면-라인넘버별로 길이 합산
if row_dict['classified_category'] == 'PIPE':
if comparison_key in materials:
# 기존 자재에 길이 합산
materials[comparison_key]['quantity'] += row_dict['quantity']
materials[comparison_key]['total_length'] = (
materials[comparison_key].get('total_length', 0) +
(row_dict['length'] or 0) * row_dict['quantity']
)
else:
row_dict['total_length'] = (row_dict['length'] or 0) * row_dict['quantity']
materials[comparison_key] = row_dict
else:
materials[comparison_key] = row_dict
return materials
def _perform_detailed_comparison(
self,
previous_materials: Dict[str, Dict],
current_materials: Dict[str, Dict],
job_no: str
) -> Dict[str, Any]:
"""상세 비교 수행"""
result = {
"job_no": job_no,
"comparison_date": datetime.now().isoformat(),
"summary": {
"total_previous": len(previous_materials),
"total_current": len(current_materials),
"purchased_maintained": 0,
"purchased_increased": 0,
"purchased_decreased": 0,
"unpurchased_maintained": 0,
"unpurchased_increased": 0,
"unpurchased_decreased": 0,
"new_materials": 0,
"deleted_materials": 0
},
"changes": {
"purchased_materials": {
"maintained": [],
"additional_purchase_needed": [],
"excess_inventory": []
},
"unpurchased_materials": {
"maintained": [],
"quantity_updated": [],
"quantity_reduced": []
},
"new_materials": [],
"deleted_materials": []
}
}
# 이전 자재 기준으로 비교
for key, prev_material in previous_materials.items():
if key in current_materials:
curr_material = current_materials[key]
change_info = self._analyze_material_change(prev_material, curr_material)
if prev_material.get('purchase_confirmed', False):
# 구매 완료된 자재 처리
self._process_purchased_material_change(result, change_info, prev_material, curr_material)
else:
# 구매 미완료 자재 처리
self._process_unpurchased_material_change(result, change_info, prev_material, curr_material)
else:
# 삭제된 자재
result["changes"]["deleted_materials"].append({
"material": prev_material,
"reason": "removed_from_new_revision"
})
result["summary"]["deleted_materials"] += 1
# 신규 자재 처리
for key, curr_material in current_materials.items():
if key not in previous_materials:
result["changes"]["new_materials"].append({
"material": curr_material,
"action": "new_material_added"
})
result["summary"]["new_materials"] += 1
return result
def _analyze_material_change(self, prev_material: Dict, curr_material: Dict) -> Dict:
"""자재 변경 사항 분석"""
prev_qty = float(prev_material.get('quantity', 0))
curr_qty = float(curr_material.get('quantity', 0))
# PIPE 자재의 경우 총 길이로 비교
if prev_material.get('classified_category') == 'PIPE':
prev_total = prev_material.get('total_length', 0)
curr_total = curr_material.get('total_length', 0)
return {
"quantity_change": curr_qty - prev_qty,
"length_change": curr_total - prev_total,
"change_type": "length_based" if abs(curr_total - prev_total) > 0.01 else "no_change"
}
else:
return {
"quantity_change": curr_qty - prev_qty,
"change_type": "increased" if curr_qty > prev_qty else "decreased" if curr_qty < prev_qty else "no_change"
}
def _process_purchased_material_change(
self,
result: Dict,
change_info: Dict,
prev_material: Dict,
curr_material: Dict
):
"""구매 완료 자재 변경 처리"""
if change_info["change_type"] == "no_change":
result["changes"]["purchased_materials"]["maintained"].append({
"material": curr_material,
"action": "maintain_inventory"
})
result["summary"]["purchased_maintained"] += 1
elif change_info["change_type"] == "increased":
additional_qty = change_info["quantity_change"]
result["changes"]["purchased_materials"]["additional_purchase_needed"].append({
"material": curr_material,
"previous_quantity": prev_material.get('quantity'),
"current_quantity": curr_material.get('quantity'),
"additional_needed": additional_qty,
"action": "additional_purchase_required"
})
result["summary"]["purchased_increased"] += 1
else: # decreased
excess_qty = abs(change_info["quantity_change"])
result["changes"]["purchased_materials"]["excess_inventory"].append({
"material": curr_material,
"previous_quantity": prev_material.get('quantity'),
"current_quantity": curr_material.get('quantity'),
"excess_quantity": excess_qty,
"action": "mark_as_excess_inventory"
})
result["summary"]["purchased_decreased"] += 1
def _process_unpurchased_material_change(
self,
result: Dict,
change_info: Dict,
prev_material: Dict,
curr_material: Dict
):
"""구매 미완료 자재 변경 처리"""
if change_info["change_type"] == "no_change":
result["changes"]["unpurchased_materials"]["maintained"].append({
"material": curr_material,
"action": "maintain_purchase_pending"
})
result["summary"]["unpurchased_maintained"] += 1
elif change_info["change_type"] == "increased":
result["changes"]["unpurchased_materials"]["quantity_updated"].append({
"material": curr_material,
"previous_quantity": prev_material.get('quantity'),
"current_quantity": curr_material.get('quantity'),
"quantity_change": change_info["quantity_change"],
"action": "update_purchase_quantity"
})
result["summary"]["unpurchased_increased"] += 1
else: # decreased
result["changes"]["unpurchased_materials"]["quantity_reduced"].append({
"material": curr_material,
"previous_quantity": prev_material.get('quantity'),
"current_quantity": curr_material.get('quantity'),
"quantity_change": change_info["quantity_change"],
"action": "reduce_purchase_quantity"
})
result["summary"]["unpurchased_decreased"] += 1
def _get_previous_file_id(self, job_no: str, current_file_id: int) -> Optional[int]:
"""이전 파일 ID 자동 탐지"""
query = """
SELECT id, revision
FROM files
WHERE job_no = :job_no AND id != :current_file_id AND is_active = true
ORDER BY upload_date DESC
LIMIT 1
"""
result = self.db_service.execute_query(query, {
"job_no": job_no,
"current_file_id": current_file_id
})
row = result.fetchone()
return row.id if row else None
def _handle_first_revision(self, current_file_id: int) -> Dict[str, Any]:
"""첫 번째 리비전 처리"""
materials = self._get_materials_with_purchase_status(current_file_id)
return {
"job_no": None,
"comparison_date": datetime.now().isoformat(),
"is_first_revision": True,
"summary": {
"total_materials": len(materials),
"all_new": True
},
"changes": {
"new_materials": [{"material": mat, "action": "first_revision"} for mat in materials.values()]
}
}
def apply_revision_changes(self, comparison_result: Dict, current_file_id: int) -> Dict[str, Any]:
"""리비전 변경사항을 DB에 적용"""
try:
# 각 변경사항별로 DB 업데이트
updates_applied = {
"purchased_materials": 0,
"unpurchased_materials": 0,
"new_materials": 0,
"deleted_materials": 0
}
changes = comparison_result.get("changes", {})
# 구매 완료 자재 처리
purchased = changes.get("purchased_materials", {})
for category, materials in purchased.items():
for item in materials:
material = item["material"]
action = item["action"]
if action == "additional_purchase_required":
self._mark_additional_purchase_needed(material, item)
elif action == "mark_as_excess_inventory":
self._mark_excess_inventory(material, item)
updates_applied["purchased_materials"] += 1
# 구매 미완료 자재 처리
unpurchased = changes.get("unpurchased_materials", {})
for category, materials in unpurchased.items():
for item in materials:
material = item["material"]
action = item["action"]
if action == "update_purchase_quantity":
self._update_purchase_quantity(material, item)
elif action == "reduce_purchase_quantity":
self._reduce_purchase_quantity(material, item)
updates_applied["unpurchased_materials"] += 1
# 신규 자재 처리
for item in changes.get("new_materials", []):
self._mark_new_material(item["material"])
updates_applied["new_materials"] += 1
# 삭제된 자재 처리
for item in changes.get("deleted_materials", []):
self._mark_deleted_material(item["material"])
updates_applied["deleted_materials"] += 1
self.db.commit()
return {
"success": True,
"updates_applied": updates_applied,
"message": "리비전 변경사항이 성공적으로 적용되었습니다."
}
except Exception as e:
self.db.rollback()
logger.error(f"Failed to apply revision changes: {e}")
return {
"success": False,
"error": str(e),
"message": "리비전 변경사항 적용 중 오류가 발생했습니다."
}
def _mark_additional_purchase_needed(self, material: Dict, change_info: Dict):
"""추가 구매 필요 표시"""
update_query = """
UPDATE materials
SET revision_status = 'additional_purchase_needed',
notes = CONCAT(COALESCE(notes, ''),
'\n추가 구매 필요: ', :additional_qty, ' ', unit)
WHERE id = :material_id
"""
self.db_service.execute_query(update_query, {
"material_id": material["id"],
"additional_qty": change_info["additional_needed"]
})
def _mark_excess_inventory(self, material: Dict, change_info: Dict):
"""잉여 재고 표시"""
update_query = """
UPDATE materials
SET revision_status = 'excess_inventory',
notes = CONCAT(COALESCE(notes, ''),
'\n잉여 재고: ', :excess_qty, ' ', unit)
WHERE id = :material_id
"""
self.db_service.execute_query(update_query, {
"material_id": material["id"],
"excess_qty": change_info["excess_quantity"]
})
def _update_purchase_quantity(self, material: Dict, change_info: Dict):
"""구매 수량 업데이트"""
update_query = """
UPDATE materials
SET quantity = :new_quantity,
revision_status = 'quantity_updated',
notes = CONCAT(COALESCE(notes, ''),
'\n수량 변경: ', :prev_qty, '', :new_qty)
WHERE id = :material_id
"""
self.db_service.execute_query(update_query, {
"material_id": material["id"],
"new_quantity": change_info["current_quantity"],
"prev_qty": change_info["previous_quantity"],
"new_qty": change_info["current_quantity"]
})
def _reduce_purchase_quantity(self, material: Dict, change_info: Dict):
"""구매 수량 감소"""
if change_info["current_quantity"] <= 0:
# 수량이 0 이하면 삭제 표시
update_query = """
UPDATE materials
SET revision_status = 'deleted',
is_active = false,
notes = CONCAT(COALESCE(notes, ''), '\n리비전에서 삭제됨')
WHERE id = :material_id
"""
else:
# 수량만 감소
update_query = """
UPDATE materials
SET quantity = :new_quantity,
revision_status = 'quantity_reduced',
notes = CONCAT(COALESCE(notes, ''),
'\n수량 감소: ', :prev_qty, '', :new_qty)
WHERE id = :material_id
"""
self.db_service.execute_query(update_query, {
"material_id": material["id"],
"new_quantity": change_info.get("current_quantity", 0),
"prev_qty": change_info.get("previous_quantity", 0),
"new_qty": change_info.get("current_quantity", 0)
})
def _mark_new_material(self, material: Dict):
"""신규 자재 표시"""
update_query = """
UPDATE materials
SET revision_status = 'new_in_revision'
WHERE id = :material_id
"""
self.db_service.execute_query(update_query, {
"material_id": material["id"]
})
def _mark_deleted_material(self, material: Dict):
"""삭제된 자재 표시 (이전 리비전에서)"""
update_query = """
UPDATE materials
SET revision_status = 'removed_in_new_revision',
notes = CONCAT(COALESCE(notes, ''), '\n신규 리비전에서 제거됨')
WHERE id = :material_id
"""
self.db_service.execute_query(update_query, {
"material_id": material["id"]
})