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

422 lines
16 KiB
Python

"""
리비전 상태 관리 서비스
리비전 진행 상태, 히스토리, 확정 등 관리
"""
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
}
}