🔧 완전한 스키마 자동화 시스템 구축
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
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 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
This commit is contained in:
541
backend/app/services/pipe_revision_service.py
Normal file
541
backend/app/services/pipe_revision_service.py
Normal file
@@ -0,0 +1,541 @@
|
||||
"""
|
||||
PIPE 전용 리비전 관리 서비스
|
||||
|
||||
Cutting Plan 작성 전/후에 따른 차별화된 리비전 처리 로직
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text, and_, or_
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
File, Material, PipeCuttingPlan, PipeRevisionComparison,
|
||||
PipeRevisionChange, PipeLengthCalculation
|
||||
)
|
||||
from ..utils.pipe_utils import (
|
||||
PipeConstants, PipeDataExtractor, PipeCalculator,
|
||||
PipeComparator, PipeValidator, PipeLogger
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PipeRevisionService:
|
||||
"""PIPE 전용 리비전 관리 서비스"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def check_revision_status(self, job_no: str, new_file_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
리비전 상태 확인 및 처리 방식 결정
|
||||
|
||||
Returns:
|
||||
- revision_type: 'no_revision', 'pre_cutting_plan', 'post_cutting_plan'
|
||||
- requires_action: 처리가 필요한지 여부
|
||||
- message: 사용자에게 표시할 메시지
|
||||
"""
|
||||
try:
|
||||
# 기존 파일 확인
|
||||
previous_file = self._get_previous_file(job_no, new_file_id)
|
||||
if not previous_file:
|
||||
return {
|
||||
"revision_type": "no_revision",
|
||||
"requires_action": False,
|
||||
"message": "첫 번째 BOM 파일입니다. 새로운 Cutting Plan을 작성해주세요."
|
||||
}
|
||||
|
||||
# Cutting Plan 존재 여부 확인
|
||||
has_cutting_plan = self._has_existing_cutting_plan(job_no)
|
||||
|
||||
if not has_cutting_plan:
|
||||
# Cutting Plan 작성 전 리비전
|
||||
return {
|
||||
"revision_type": "pre_cutting_plan",
|
||||
"requires_action": True,
|
||||
"previous_file_id": previous_file.id,
|
||||
"message": "Cutting Plan 작성 전 리비전이 감지되었습니다. 새로운 BOM으로 Cutting Plan을 작성해주세요."
|
||||
}
|
||||
else:
|
||||
# Cutting Plan 작성 후 리비전
|
||||
return {
|
||||
"revision_type": "post_cutting_plan",
|
||||
"requires_action": True,
|
||||
"previous_file_id": previous_file.id,
|
||||
"message": "기존 Cutting Plan이 있는 상태에서 리비전이 감지되었습니다. 변경사항을 비교 검토해주세요."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check pipe revision status: {e}")
|
||||
return {
|
||||
"revision_type": "error",
|
||||
"requires_action": False,
|
||||
"message": f"리비전 상태 확인 중 오류가 발생했습니다: {str(e)}"
|
||||
}
|
||||
|
||||
def handle_pre_cutting_plan_revision(self, job_no: str, new_file_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Cutting Plan 작성 전 리비전 처리
|
||||
- 기존 PIPE 관련 데이터 전체 삭제
|
||||
- 새 BOM 파일로 초기화
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Processing pre-cutting-plan revision for job {job_no}")
|
||||
|
||||
# 1. 기존 PIPE 관련 데이터 삭제
|
||||
deleted_count = self._delete_existing_pipe_data(job_no)
|
||||
|
||||
# 2. 새 BOM에서 PIPE 데이터 추출
|
||||
pipe_materials = self._extract_pipe_materials_from_bom(new_file_id)
|
||||
|
||||
# 3. 처리 결과 반환
|
||||
return {
|
||||
"status": "success",
|
||||
"revision_type": "pre_cutting_plan",
|
||||
"deleted_items": deleted_count,
|
||||
"new_pipe_materials": len(pipe_materials),
|
||||
"message": f"기존 PIPE 데이터 {deleted_count}건이 삭제되었습니다. 새로운 Cutting Plan을 작성해주세요.",
|
||||
"next_action": "create_new_cutting_plan",
|
||||
"pipe_materials": pipe_materials
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle pre-cutting-plan revision: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Cutting Plan 작성 전 리비전 처리 실패: {str(e)}"
|
||||
}
|
||||
|
||||
def handle_post_cutting_plan_revision(self, job_no: str, new_file_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Cutting Plan 작성 후 리비전 처리
|
||||
- 기존 Cutting Plan과 신규 BOM 비교
|
||||
- 변경사항 상세 분석
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Processing post-cutting-plan revision for job {job_no}")
|
||||
|
||||
# 1. 기존 Cutting Plan 조회
|
||||
existing_plan = self._get_existing_cutting_plan_data(job_no)
|
||||
if not existing_plan:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "기존 Cutting Plan을 찾을 수 없습니다."
|
||||
}
|
||||
|
||||
# 2. 새 BOM에서 PIPE 데이터 추출
|
||||
new_pipe_data = self._extract_pipe_materials_from_bom(new_file_id)
|
||||
|
||||
# 3. 도면별 비교 수행
|
||||
comparison_result = self._compare_pipe_data_by_drawing(existing_plan, new_pipe_data)
|
||||
|
||||
# 4. 비교 결과 저장
|
||||
comparison_id = self._save_comparison_result(job_no, new_file_id, comparison_result)
|
||||
|
||||
# 5. 변경사항 요약
|
||||
summary = self._generate_comparison_summary(comparison_result)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"revision_type": "post_cutting_plan",
|
||||
"comparison_id": comparison_id,
|
||||
"summary": summary,
|
||||
"changed_drawings": [d for d in comparison_result if d["has_changes"]],
|
||||
"unchanged_drawings": [d for d in comparison_result if not d["has_changes"]],
|
||||
"message": f"리비전 비교가 완료되었습니다. {summary['changed_drawings_count']}개 도면에서 변경사항이 발견되었습니다.",
|
||||
"next_action": "review_changes"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle post-cutting-plan revision: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Cutting Plan 작성 후 리비전 처리 실패: {str(e)}"
|
||||
}
|
||||
|
||||
def _get_previous_file(self, job_no: str, current_file_id: int) -> Optional[File]:
|
||||
"""이전 파일 조회"""
|
||||
return self.db.query(File).filter(
|
||||
and_(
|
||||
File.job_no == job_no,
|
||||
File.id < current_file_id,
|
||||
File.is_active == True
|
||||
)
|
||||
).order_by(File.id.desc()).first()
|
||||
|
||||
def _has_existing_cutting_plan(self, job_no: str) -> bool:
|
||||
"""기존 Cutting Plan 존재 여부 확인"""
|
||||
count = self.db.query(PipeCuttingPlan).filter(
|
||||
PipeCuttingPlan.job_no == job_no
|
||||
).count()
|
||||
return count > 0
|
||||
|
||||
def _delete_existing_pipe_data(self, job_no: str) -> int:
|
||||
"""기존 PIPE 관련 데이터 삭제"""
|
||||
try:
|
||||
# Cutting Plan 데이터 삭제
|
||||
cutting_plan_count = self.db.query(PipeCuttingPlan).filter(
|
||||
PipeCuttingPlan.job_no == job_no
|
||||
).count()
|
||||
|
||||
self.db.query(PipeCuttingPlan).filter(
|
||||
PipeCuttingPlan.job_no == job_no
|
||||
).delete()
|
||||
|
||||
# Length Calculation 데이터 삭제
|
||||
self.db.query(PipeLengthCalculation).filter(
|
||||
PipeLengthCalculation.file_id.in_(
|
||||
self.db.query(File.id).filter(File.job_no == job_no)
|
||||
)
|
||||
).delete()
|
||||
|
||||
self.db.commit()
|
||||
logger.info(f"Deleted {cutting_plan_count} cutting plan records for job {job_no}")
|
||||
|
||||
return cutting_plan_count
|
||||
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Failed to delete existing pipe data: {e}")
|
||||
raise
|
||||
|
||||
def _extract_pipe_materials_from_bom(self, file_id: int) -> List[Dict[str, Any]]:
|
||||
"""BOM 파일에서 PIPE 자재 추출 (리팩토링된 유틸리티 사용)"""
|
||||
return PipeDataExtractor.extract_pipe_materials_from_file(self.db, file_id)
|
||||
|
||||
def _get_existing_cutting_plan_data(self, job_no: str) -> List[Dict[str, Any]]:
|
||||
"""기존 Cutting Plan 데이터 조회"""
|
||||
try:
|
||||
cutting_plans = self.db.query(PipeCuttingPlan).filter(
|
||||
PipeCuttingPlan.job_no == job_no
|
||||
).all()
|
||||
|
||||
plan_data = []
|
||||
for plan in cutting_plans:
|
||||
plan_data.append({
|
||||
"id": plan.id,
|
||||
"area": plan.area or "",
|
||||
"drawing_name": plan.drawing_name,
|
||||
"line_no": plan.line_no,
|
||||
"material_grade": plan.material_grade or "",
|
||||
"schedule_spec": plan.schedule_spec or "",
|
||||
"nominal_size": plan.nominal_size or "",
|
||||
"length_mm": float(plan.length_mm or 0),
|
||||
"end_preparation": plan.end_preparation or "무개선"
|
||||
})
|
||||
|
||||
logger.info(f"Retrieved {len(plan_data)} cutting plan records for job {job_no}")
|
||||
return plan_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get existing cutting plan data: {e}")
|
||||
raise
|
||||
|
||||
def _compare_pipe_data_by_drawing(self, existing_plan: List[Dict], new_pipe_data: List[Dict]) -> List[Dict[str, Any]]:
|
||||
"""도면별 PIPE 데이터 비교"""
|
||||
try:
|
||||
# 도면별로 데이터 그룹화
|
||||
existing_by_drawing = self._group_by_drawing(existing_plan)
|
||||
new_by_drawing = self._group_by_drawing(new_pipe_data)
|
||||
|
||||
# 모든 도면 목록
|
||||
all_drawings = set(existing_by_drawing.keys()) | set(new_by_drawing.keys())
|
||||
|
||||
comparison_results = []
|
||||
|
||||
for drawing_name in sorted(all_drawings):
|
||||
existing_segments = existing_by_drawing.get(drawing_name, [])
|
||||
new_segments = new_by_drawing.get(drawing_name, [])
|
||||
|
||||
# 도면별 비교 수행
|
||||
drawing_comparison = self._compare_drawing_segments(
|
||||
drawing_name, existing_segments, new_segments
|
||||
)
|
||||
|
||||
comparison_results.append(drawing_comparison)
|
||||
|
||||
return comparison_results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to compare pipe data by drawing: {e}")
|
||||
raise
|
||||
|
||||
def _group_by_drawing(self, data: List[Dict]) -> Dict[str, List[Dict]]:
|
||||
"""데이터를 도면별로 그룹화"""
|
||||
grouped = {}
|
||||
for item in data:
|
||||
drawing = item.get("drawing_name", "UNKNOWN")
|
||||
if drawing not in grouped:
|
||||
grouped[drawing] = []
|
||||
grouped[drawing].append(item)
|
||||
return grouped
|
||||
|
||||
def _compare_drawing_segments(self, drawing_name: str, existing: List[Dict], new: List[Dict]) -> Dict[str, Any]:
|
||||
"""단일 도면의 세그먼트 비교"""
|
||||
try:
|
||||
# 세그먼트 매칭 (재질, 길이, 끝단가공 기준)
|
||||
matched_pairs, added_segments, removed_segments = self._match_segments(existing, new)
|
||||
|
||||
# 변경사항 분석
|
||||
unchanged_segments = []
|
||||
modified_segments = []
|
||||
|
||||
for existing_seg, new_seg in matched_pairs:
|
||||
if self._segments_are_identical(existing_seg, new_seg):
|
||||
unchanged_segments.append({
|
||||
"change_type": "unchanged",
|
||||
"segment_data": new_seg,
|
||||
"existing_data": existing_seg
|
||||
})
|
||||
else:
|
||||
changes = self._get_segment_changes(existing_seg, new_seg)
|
||||
modified_segments.append({
|
||||
"change_type": "modified",
|
||||
"segment_data": new_seg,
|
||||
"existing_data": existing_seg,
|
||||
"changes": changes
|
||||
})
|
||||
|
||||
# 추가된 세그먼트
|
||||
added_segment_data = [
|
||||
{
|
||||
"change_type": "added",
|
||||
"segment_data": seg,
|
||||
"existing_data": None
|
||||
}
|
||||
for seg in added_segments
|
||||
]
|
||||
|
||||
# 삭제된 세그먼트
|
||||
removed_segment_data = [
|
||||
{
|
||||
"change_type": "removed",
|
||||
"segment_data": None,
|
||||
"existing_data": seg
|
||||
}
|
||||
for seg in removed_segments
|
||||
]
|
||||
|
||||
# 전체 세그먼트 목록
|
||||
all_segments = unchanged_segments + modified_segments + added_segment_data + removed_segment_data
|
||||
|
||||
# 변경사항 여부 판단
|
||||
has_changes = len(modified_segments) > 0 or len(added_segments) > 0 or len(removed_segments) > 0
|
||||
|
||||
return {
|
||||
"drawing_name": drawing_name,
|
||||
"has_changes": has_changes,
|
||||
"segments": all_segments,
|
||||
"summary": {
|
||||
"total_segments": len(all_segments),
|
||||
"unchanged_count": len(unchanged_segments),
|
||||
"modified_count": len(modified_segments),
|
||||
"added_count": len(added_segments),
|
||||
"removed_count": len(removed_segments)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to compare segments for drawing {drawing_name}: {e}")
|
||||
raise
|
||||
|
||||
def _match_segments(self, existing: List[Dict], new: List[Dict]) -> Tuple[List[Tuple], List[Dict], List[Dict]]:
|
||||
"""세그먼트 매칭 (재질, 길이 기준)"""
|
||||
matched_pairs = []
|
||||
remaining_new = new.copy()
|
||||
remaining_existing = existing.copy()
|
||||
|
||||
# 정확히 일치하는 세그먼트 찾기
|
||||
for existing_seg in existing.copy():
|
||||
for new_seg in remaining_new.copy():
|
||||
if self._segments_match_for_pairing(existing_seg, new_seg):
|
||||
matched_pairs.append((existing_seg, new_seg))
|
||||
remaining_existing.remove(existing_seg)
|
||||
remaining_new.remove(new_seg)
|
||||
break
|
||||
|
||||
# 남은 것들은 추가/삭제로 분류
|
||||
added_segments = remaining_new
|
||||
removed_segments = remaining_existing
|
||||
|
||||
return matched_pairs, added_segments, removed_segments
|
||||
|
||||
def _segments_match_for_pairing(self, seg1: Dict, seg2: Dict) -> bool:
|
||||
"""세그먼트 매칭 기준 (재질과 길이가 유사한지 확인)"""
|
||||
# 재질 비교
|
||||
material1 = seg1.get("material_grade", "").strip()
|
||||
material2 = seg2.get("material_grade", "").strip()
|
||||
|
||||
# 길이 비교 (허용 오차 1mm)
|
||||
length1 = seg1.get("length_mm", seg1.get("length", 0))
|
||||
length2 = seg2.get("length_mm", seg2.get("length", 0))
|
||||
|
||||
material_match = material1.lower() == material2.lower()
|
||||
length_match = abs(float(length1) - float(length2)) <= 1.0
|
||||
|
||||
return material_match and length_match
|
||||
|
||||
def _segments_are_identical(self, seg1: Dict, seg2: Dict) -> bool:
|
||||
"""세그먼트 완전 동일성 검사"""
|
||||
# 주요 속성들 비교
|
||||
material_match = seg1.get("material_grade", "").strip().lower() == seg2.get("material_grade", "").strip().lower()
|
||||
|
||||
length1 = seg1.get("length_mm", seg1.get("length", 0))
|
||||
length2 = seg2.get("length_mm", seg2.get("length", 0))
|
||||
length_match = abs(float(length1) - float(length2)) <= 0.1
|
||||
|
||||
end_prep1 = seg1.get("end_preparation", "무개선")
|
||||
end_prep2 = seg2.get("end_preparation", "무개선")
|
||||
end_prep_match = end_prep1 == end_prep2
|
||||
|
||||
return material_match and length_match and end_prep_match
|
||||
|
||||
def _get_segment_changes(self, existing: Dict, new: Dict) -> List[Dict[str, Any]]:
|
||||
"""세그먼트 변경사항 상세 분석"""
|
||||
changes = []
|
||||
|
||||
# 재질 변경
|
||||
old_material = existing.get("material_grade", "").strip()
|
||||
new_material = new.get("material_grade", "").strip()
|
||||
if old_material.lower() != new_material.lower():
|
||||
changes.append({
|
||||
"field": "material_grade",
|
||||
"old_value": old_material,
|
||||
"new_value": new_material
|
||||
})
|
||||
|
||||
# 길이 변경
|
||||
old_length = existing.get("length_mm", existing.get("length", 0))
|
||||
new_length = new.get("length_mm", new.get("length", 0))
|
||||
if abs(float(old_length) - float(new_length)) > 0.1:
|
||||
changes.append({
|
||||
"field": "length",
|
||||
"old_value": f"{old_length}mm",
|
||||
"new_value": f"{new_length}mm"
|
||||
})
|
||||
|
||||
# 끝단가공 변경
|
||||
old_end_prep = existing.get("end_preparation", "무개선")
|
||||
new_end_prep = new.get("end_preparation", "무개선")
|
||||
if old_end_prep != new_end_prep:
|
||||
changes.append({
|
||||
"field": "end_preparation",
|
||||
"old_value": old_end_prep,
|
||||
"new_value": new_end_prep
|
||||
})
|
||||
|
||||
return changes
|
||||
|
||||
def _save_comparison_result(self, job_no: str, new_file_id: int, comparison_result: List[Dict]) -> int:
|
||||
"""비교 결과를 데이터베이스에 저장"""
|
||||
try:
|
||||
# 이전 파일 ID 조회
|
||||
previous_file = self._get_previous_file(job_no, new_file_id)
|
||||
previous_file_id = previous_file.id if previous_file else None
|
||||
|
||||
# 통계 계산
|
||||
total_drawings = len(comparison_result)
|
||||
changed_drawings = len([d for d in comparison_result if d["has_changes"]])
|
||||
unchanged_drawings = total_drawings - changed_drawings
|
||||
|
||||
total_segments = sum(d["summary"]["total_segments"] for d in comparison_result)
|
||||
added_segments = sum(d["summary"]["added_count"] for d in comparison_result)
|
||||
removed_segments = sum(d["summary"]["removed_count"] for d in comparison_result)
|
||||
modified_segments = sum(d["summary"]["modified_count"] for d in comparison_result)
|
||||
unchanged_segments = sum(d["summary"]["unchanged_count"] for d in comparison_result)
|
||||
|
||||
# 비교 결과 저장
|
||||
comparison = PipeRevisionComparison(
|
||||
job_no=job_no,
|
||||
current_file_id=new_file_id,
|
||||
previous_cutting_plan_id=None, # 추후 구현
|
||||
total_drawings=total_drawings,
|
||||
changed_drawings=changed_drawings,
|
||||
unchanged_drawings=unchanged_drawings,
|
||||
total_segments=total_segments,
|
||||
added_segments=added_segments,
|
||||
removed_segments=removed_segments,
|
||||
modified_segments=modified_segments,
|
||||
unchanged_segments=unchanged_segments,
|
||||
created_by="system"
|
||||
)
|
||||
|
||||
self.db.add(comparison)
|
||||
self.db.flush() # ID 생성을 위해
|
||||
|
||||
# 상세 변경사항 저장
|
||||
for drawing_data in comparison_result:
|
||||
if drawing_data["has_changes"]:
|
||||
for segment in drawing_data["segments"]:
|
||||
if segment["change_type"] != "unchanged":
|
||||
change = PipeRevisionChange(
|
||||
comparison_id=comparison.id,
|
||||
drawing_name=drawing_data["drawing_name"],
|
||||
change_type=segment["change_type"]
|
||||
)
|
||||
|
||||
# 기존 데이터
|
||||
if segment["existing_data"]:
|
||||
existing = segment["existing_data"]
|
||||
change.old_line_no = existing.get("line_no")
|
||||
change.old_material_grade = existing.get("material_grade")
|
||||
change.old_schedule_spec = existing.get("schedule_spec")
|
||||
change.old_nominal_size = existing.get("nominal_size")
|
||||
change.old_length_mm = existing.get("length_mm", existing.get("length"))
|
||||
change.old_end_preparation = existing.get("end_preparation")
|
||||
|
||||
# 새 데이터
|
||||
if segment["segment_data"]:
|
||||
new_data = segment["segment_data"]
|
||||
change.new_line_no = new_data.get("line_no")
|
||||
change.new_material_grade = new_data.get("material_grade")
|
||||
change.new_schedule_spec = new_data.get("schedule_spec")
|
||||
change.new_nominal_size = new_data.get("nominal_size")
|
||||
change.new_length_mm = new_data.get("length_mm", new_data.get("length"))
|
||||
change.new_end_preparation = new_data.get("end_preparation")
|
||||
|
||||
self.db.add(change)
|
||||
|
||||
self.db.commit()
|
||||
logger.info(f"Saved comparison result with ID {comparison.id}")
|
||||
|
||||
return comparison.id
|
||||
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Failed to save comparison result: {e}")
|
||||
raise
|
||||
|
||||
def _generate_comparison_summary(self, comparison_result: List[Dict]) -> Dict[str, Any]:
|
||||
"""비교 결과 요약 생성"""
|
||||
total_drawings = len(comparison_result)
|
||||
changed_drawings = [d for d in comparison_result if d["has_changes"]]
|
||||
changed_drawings_count = len(changed_drawings)
|
||||
|
||||
total_segments = sum(d["summary"]["total_segments"] for d in comparison_result)
|
||||
added_segments = sum(d["summary"]["added_count"] for d in comparison_result)
|
||||
removed_segments = sum(d["summary"]["removed_count"] for d in comparison_result)
|
||||
modified_segments = sum(d["summary"]["modified_count"] for d in comparison_result)
|
||||
unchanged_segments = sum(d["summary"]["unchanged_count"] for d in comparison_result)
|
||||
|
||||
return {
|
||||
"total_drawings": total_drawings,
|
||||
"changed_drawings_count": changed_drawings_count,
|
||||
"unchanged_drawings_count": total_drawings - changed_drawings_count,
|
||||
"total_segments": total_segments,
|
||||
"added_segments": added_segments,
|
||||
"removed_segments": removed_segments,
|
||||
"modified_segments": modified_segments,
|
||||
"unchanged_segments": unchanged_segments,
|
||||
"change_percentage": round((changed_drawings_count / total_drawings * 100) if total_drawings > 0 else 0, 1)
|
||||
}
|
||||
|
||||
|
||||
def get_pipe_revision_service(db: Session = None) -> PipeRevisionService:
|
||||
"""PipeRevisionService 인스턴스 생성"""
|
||||
if db is None:
|
||||
db = next(get_db())
|
||||
return PipeRevisionService(db)
|
||||
Reference in New Issue
Block a user