""" 리비전 상태 관리 서비스 리비전 진행 상태, 히스토리, 확정 등 관리 """ from sqlalchemy.orm import Session from sqlalchemy import text, desc from typing import List, Dict, Any, Optional from datetime import datetime from ..models import File, RevisionComparison, RevisionChangeLog from ..utils.logger import get_logger from .database_service import DatabaseService logger = get_logger(__name__) class RevisionStatusService: """리비전 상태 관리 서비스""" def __init__(self, db: Session): self.db = db self.db_service = DatabaseService(db) def get_revision_status(self, job_no: str, file_id: int) -> Dict[str, Any]: """ 리비전 상태 조회 Args: job_no: 작업 번호 file_id: 파일 ID Returns: 리비전 상태 정보 """ # 파일 정보 조회 current_file = self._get_file_info(file_id) if not current_file: return {"error": "파일을 찾을 수 없습니다."} # 같은 작업의 모든 파일 조회 all_files = self._get_job_files(job_no) # 리비전 히스토리 구성 revision_history = self._build_revision_history(all_files, file_id) # 현재 리비전의 처리 상태 processing_status = self._get_processing_status(file_id) return { "job_no": job_no, "current_file": current_file, "revision_history": revision_history, "processing_status": processing_status, "is_latest": revision_history.get("is_latest", False), "can_upload_new_revision": revision_history.get("can_upload_new", True), "status_summary": self._generate_status_summary(processing_status) } def get_revision_history(self, job_no: str) -> List[Dict[str, Any]]: """작업의 전체 리비전 히스토리 조회""" files = self._get_job_files(job_no) history = [] for i, file_info in enumerate(files): # 이전 파일과의 비교 정보 comparison_info = None if i > 0: prev_file = files[i-1] comparison_info = self._get_comparison_summary(file_info['id'], prev_file['id']) history.append({ "file_id": file_info['id'], "revision": file_info['revision'], "filename": file_info['original_filename'], "upload_date": file_info['upload_date'], "uploaded_by": file_info['uploaded_by'], "file_size": file_info['file_size'], "material_count": self._get_material_count(file_info['id']), "comparison_with_previous": comparison_info, "is_latest": i == 0, # 최신순 정렬이므로 첫 번째가 최신 "processing_status": self._get_processing_status(file_info['id']) }) return history def create_revision_comparison_record( self, job_no: str, current_file_id: int, previous_file_id: int, comparison_result: Dict[str, Any], created_by: str ) -> int: """리비전 비교 기록 생성""" try: comparison_record = RevisionComparison( job_no=job_no, current_file_id=current_file_id, previous_file_id=previous_file_id, comparison_result=comparison_result, summary_stats=comparison_result.get("summary", {}), created_by=created_by, is_applied=False ) self.db.add(comparison_record) self.db.commit() self.db.refresh(comparison_record) logger.info(f"Created revision comparison record: {comparison_record.id}") return comparison_record.id except Exception as e: self.db.rollback() logger.error(f"Failed to create revision comparison record: {e}") raise def apply_revision_comparison( self, comparison_id: int, applied_by: str ) -> Dict[str, Any]: """리비전 비교 결과 적용""" try: # 비교 기록 조회 comparison = self.db.query(RevisionComparison).filter( RevisionComparison.id == comparison_id ).first() if not comparison: return {"success": False, "error": "비교 기록을 찾을 수 없습니다."} if comparison.is_applied: return {"success": False, "error": "이미 적용된 비교 결과입니다."} # 적용 처리 comparison.is_applied = True comparison.applied_at = datetime.utcnow() comparison.applied_by = applied_by # 변경 로그 생성 self._create_change_logs(comparison) self.db.commit() logger.info(f"Applied revision comparison: {comparison_id}") return { "success": True, "comparison_id": comparison_id, "applied_at": comparison.applied_at.isoformat(), "applied_by": applied_by } except Exception as e: self.db.rollback() logger.error(f"Failed to apply revision comparison: {e}") return {"success": False, "error": str(e)} def get_pending_revisions(self, job_no: Optional[str] = None) -> List[Dict[str, Any]]: """대기 중인 리비전 목록 조회""" query = """ SELECT rc.id, rc.job_no, rc.current_file_id, rc.previous_file_id, rc.comparison_date, rc.created_by, rc.summary_stats, cf.original_filename as current_filename, cf.revision as current_revision, pf.original_filename as previous_filename, pf.revision as previous_revision FROM revision_comparisons rc JOIN files cf ON rc.current_file_id = cf.id LEFT JOIN files pf ON rc.previous_file_id = pf.id WHERE rc.is_applied = false """ params = {} if job_no: query += " AND rc.job_no = :job_no" params["job_no"] = job_no query += " ORDER BY rc.comparison_date DESC" result = self.db_service.execute_query(query, params) pending_revisions = [] for row in result.fetchall(): row_dict = dict(row._mapping) pending_revisions.append({ "comparison_id": row_dict['id'], "job_no": row_dict['job_no'], "current_file": { "id": row_dict['current_file_id'], "filename": row_dict['current_filename'], "revision": row_dict['current_revision'] }, "previous_file": { "id": row_dict['previous_file_id'], "filename": row_dict['previous_filename'], "revision": row_dict['previous_revision'] } if row_dict['previous_file_id'] else None, "comparison_date": row_dict['comparison_date'], "created_by": row_dict['created_by'], "summary_stats": row_dict['summary_stats'] }) return pending_revisions def _get_file_info(self, file_id: int) -> Optional[Dict[str, Any]]: """파일 정보 조회""" query = """ SELECT id, filename, original_filename, file_path, job_no, revision, bom_name, description, file_size, parsed_count, upload_date, uploaded_by, is_active FROM files WHERE id = :file_id """ result = self.db_service.execute_query(query, {"file_id": file_id}) row = result.fetchone() return dict(row._mapping) if row else None def _get_job_files(self, job_no: str) -> List[Dict[str, Any]]: """작업의 모든 파일 조회 (최신순)""" query = """ SELECT id, filename, original_filename, file_path, job_no, revision, bom_name, description, file_size, parsed_count, upload_date, uploaded_by, is_active FROM files WHERE job_no = :job_no AND is_active = true ORDER BY upload_date DESC, id DESC """ result = self.db_service.execute_query(query, {"job_no": job_no}) return [dict(row._mapping) for row in result.fetchall()] def _build_revision_history(self, all_files: List[Dict], current_file_id: int) -> Dict[str, Any]: """리비전 히스토리 구성""" current_index = None for i, file_info in enumerate(all_files): if file_info['id'] == current_file_id: current_index = i break if current_index is None: return {"error": "현재 파일을 찾을 수 없습니다."} return { "total_revisions": len(all_files), "current_position": current_index + 1, # 1-based "is_latest": current_index == 0, "is_first": current_index == len(all_files) - 1, "can_upload_new": current_index == 0, # 최신 리비전에서만 새 리비전 업로드 가능 "previous_file_id": all_files[current_index + 1]['id'] if current_index < len(all_files) - 1 else None, "next_file_id": all_files[current_index - 1]['id'] if current_index > 0 else None } def _get_processing_status(self, file_id: int) -> Dict[str, Any]: """파일의 처리 상태 조회""" # 자재별 처리 상태 통계 query = """ SELECT classified_category, COUNT(*) as total_count, SUM(CASE WHEN purchase_confirmed = true THEN 1 ELSE 0 END) as purchased_count, SUM(CASE WHEN revision_status IS NOT NULL THEN 1 ELSE 0 END) as processed_count, COUNT(DISTINCT COALESCE(revision_status, 'pending')) as status_types FROM materials WHERE file_id = :file_id AND is_active = true AND classified_category != 'PIPE' GROUP BY classified_category """ result = self.db_service.execute_query(query, {"file_id": file_id}) category_status = {} total_materials = 0 total_purchased = 0 total_processed = 0 for row in result.fetchall(): row_dict = dict(row._mapping) category = row_dict['classified_category'] category_status[category] = { "total": row_dict['total_count'], "purchased": row_dict['purchased_count'], "processed": row_dict['processed_count'], "pending": row_dict['total_count'] - row_dict['processed_count'] } total_materials += row_dict['total_count'] total_purchased += row_dict['purchased_count'] total_processed += row_dict['processed_count'] return { "file_id": file_id, "total_materials": total_materials, "total_purchased": total_purchased, "total_processed": total_processed, "pending_processing": total_materials - total_processed, "category_breakdown": category_status, "completion_percentage": (total_processed / total_materials * 100) if total_materials > 0 else 0 } def _get_comparison_summary(self, current_file_id: int, previous_file_id: int) -> Optional[Dict[str, Any]]: """비교 요약 정보 조회""" query = """ SELECT summary_stats, comparison_date, is_applied FROM revision_comparisons WHERE current_file_id = :current_file_id AND previous_file_id = :previous_file_id ORDER BY comparison_date DESC LIMIT 1 """ result = self.db_service.execute_query(query, { "current_file_id": current_file_id, "previous_file_id": previous_file_id }) row = result.fetchone() if row: row_dict = dict(row._mapping) return { "summary_stats": row_dict['summary_stats'], "comparison_date": row_dict['comparison_date'], "is_applied": row_dict['is_applied'] } return None def _get_material_count(self, file_id: int) -> int: """파일의 자재 개수 조회""" query = """ SELECT COUNT(*) as count FROM materials WHERE file_id = :file_id AND is_active = true AND classified_category != 'PIPE' """ result = self.db_service.execute_query(query, {"file_id": file_id}) row = result.fetchone() return row.count if row else 0 def _create_change_logs(self, comparison: RevisionComparison): """변경 로그 생성""" try: changes = comparison.comparison_result.get("changes", {}) # 각 변경사항에 대해 로그 생성 for change_type, change_list in changes.items(): for change_item in change_list: change_log = RevisionChangeLog( comparison_id=comparison.id, material_id=change_item.get("material", {}).get("id"), change_type=change_type, previous_data=change_item.get("previous"), current_data=change_item.get("current") or change_item.get("material"), action_taken=change_item.get("action", change_type), notes=change_item.get("reason", "") ) self.db.add(change_log) logger.info(f"Created change logs for comparison {comparison.id}") except Exception as e: logger.error(f"Failed to create change logs: {e}") raise def _generate_status_summary(self, processing_status: Dict[str, Any]) -> Dict[str, Any]: """상태 요약 생성""" total = processing_status.get("total_materials", 0) processed = processing_status.get("total_processed", 0) purchased = processing_status.get("total_purchased", 0) if total == 0: return {"status": "empty", "message": "자료가 없습니다."} completion_rate = processed / total if completion_rate >= 1.0: status = "completed" message = "모든 자재 처리 완료" elif completion_rate >= 0.8: status = "nearly_complete" message = f"처리 진행 중 ({processed}/{total})" elif completion_rate >= 0.5: status = "in_progress" message = f"처리 진행 중 ({processed}/{total})" else: status = "started" message = f"처리 시작됨 ({processed}/{total})" return { "status": status, "message": message, "completion_rate": completion_rate, "stats": { "total": total, "processed": processed, "purchased": purchased, "pending": total - processed } }