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

221 lines
9.6 KiB
Python

"""
PIPE 스냅샷 기반 Excel 내보내기 서비스
확정된 Cutting Plan의 스냅샷 데이터를 기준으로 Excel 생성
이후 리비전이 발생해도 Excel 내용은 변경되지 않음
"""
import logging
import pandas as pd
from typing import Dict, List, Optional, Any
from datetime import datetime
from io import BytesIO
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import PipeIssueSnapshot, PipeIssueSegment
from ..services.pipe_issue_snapshot_service import get_pipe_issue_snapshot_service
logger = logging.getLogger(__name__)
class PipeSnapshotExcelService:
"""PIPE 스냅샷 기반 Excel 내보내기 서비스"""
def __init__(self, db: Session):
self.db = db
self.snapshot_service = get_pipe_issue_snapshot_service(db)
def export_finalized_cutting_plan(self, job_no: str) -> Dict[str, Any]:
"""
확정된 Cutting Plan Excel 내보내기
스냅샷 데이터 기준으로 고정된 Excel 생성
"""
try:
# 1. 활성 스냅샷 확인
snapshot_info = self.snapshot_service.get_snapshot_info(job_no)
if not snapshot_info["has_snapshot"]:
return {
"success": False,
"message": "확정된 Cutting Plan이 없습니다. 먼저 Cutting Plan을 확정해주세요."
}
if not snapshot_info["is_locked"]:
return {
"success": False,
"message": "Cutting Plan이 아직 확정되지 않았습니다."
}
snapshot_id = snapshot_info["snapshot_id"]
# 2. 스냅샷 데이터 조회
segments_data = self.snapshot_service.get_snapshot_segments(snapshot_id)
if not segments_data:
return {
"success": False,
"message": "내보낼 단관 데이터가 없습니다."
}
# 3. Excel 파일 생성
excel_buffer = self._create_cutting_plan_excel(
segments_data,
snapshot_info["snapshot_name"],
job_no
)
# 4. 파일명 생성
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"PIPE_Cutting_Plan_{job_no}_{timestamp}_FINALIZED.xlsx"
return {
"success": True,
"excel_buffer": excel_buffer,
"filename": filename,
"snapshot_id": snapshot_id,
"snapshot_name": snapshot_info["snapshot_name"],
"total_segments": len(segments_data),
"message": f"확정된 Cutting Plan Excel이 생성되었습니다. (스냅샷 기준)"
}
except Exception as e:
logger.error(f"Failed to export finalized cutting plan: {e}")
return {
"success": False,
"message": f"Excel 내보내기 실패: {str(e)}"
}
def _create_cutting_plan_excel(self, segments_data: List[Dict], snapshot_name: str, job_no: str) -> BytesIO:
"""스냅샷 데이터로 Excel 파일 생성"""
# DataFrame 생성
df_data = []
for segment in segments_data:
df_data.append({
'구역': segment.get('area', ''),
'도면명': segment.get('drawing_name', ''),
'라인번호': segment.get('line_no', ''),
'재질': segment.get('material_grade', ''),
'규격': segment.get('schedule_spec', ''),
'호칭': segment.get('nominal_size', ''),
'길이(mm)': segment.get('length_mm', 0),
'끝단가공': segment.get('end_preparation', '무개선'),
'파이프정보': segment.get('material_info', '')
})
df = pd.DataFrame(df_data)
# Excel 버퍼 생성
excel_buffer = BytesIO()
with pd.ExcelWriter(excel_buffer, engine='openpyxl') as writer:
# 메인 시트 - 단관 목록
df.to_excel(writer, sheet_name='단관 목록', index=False)
# 요약 시트
summary_data = self._create_summary_data(segments_data, snapshot_name, job_no)
summary_df = pd.DataFrame(summary_data)
summary_df.to_excel(writer, sheet_name='요약', index=False)
# 구역별 시트
areas = sorted(set(segment.get('area', '') for segment in segments_data if segment.get('area')))
for area in areas:
if area: # 빈 구역 제외
area_segments = [s for s in segments_data if s.get('area') == area]
area_df_data = []
for segment in area_segments:
area_df_data.append({
'도면명': segment.get('drawing_name', ''),
'라인번호': segment.get('line_no', ''),
'재질': segment.get('material_grade', ''),
'규격': segment.get('schedule_spec', ''),
'호칭': segment.get('nominal_size', ''),
'길이(mm)': segment.get('length_mm', 0),
'끝단가공': segment.get('end_preparation', '무개선')
})
area_df = pd.DataFrame(area_df_data)
sheet_name = f'{area} 구역'
area_df.to_excel(writer, sheet_name=sheet_name, index=False)
excel_buffer.seek(0)
return excel_buffer
def _create_summary_data(self, segments_data: List[Dict], snapshot_name: str, job_no: str) -> List[Dict]:
"""요약 정보 생성"""
# 기본 통계
total_segments = len(segments_data)
total_drawings = len(set(segment.get('drawing_name', '') for segment in segments_data))
areas = sorted(set(segment.get('area', '') for segment in segments_data if segment.get('area')))
# 재질별 통계
material_stats = {}
for segment in segments_data:
material = segment.get('material_grade', 'UNKNOWN')
if material not in material_stats:
material_stats[material] = {
'count': 0,
'total_length': 0
}
material_stats[material]['count'] += 1
material_stats[material]['total_length'] += segment.get('length_mm', 0)
# 요약 데이터 구성
summary_data = [
{'항목': '작업번호', '': job_no},
{'항목': '스냅샷명', '': snapshot_name},
{'항목': '확정일시', '': datetime.now().strftime('%Y-%m-%d %H:%M:%S')},
{'항목': '총 단관 수', '': total_segments},
{'항목': '총 도면 수', '': total_drawings},
{'항목': '구역 수', '': len(areas)},
{'항목': '구역 목록', '': ', '.join(areas)},
{'항목': '', '': ''}, # 빈 줄
{'항목': '=== 재질별 통계 ===', '': ''},
]
for material, stats in material_stats.items():
summary_data.extend([
{'항목': f'{material} - 개수', '': stats['count']},
{'항목': f'{material} - 총길이(mm)', '': f"{stats['total_length']:,.1f}"},
{'항목': f'{material} - 총길이(m)', '': f"{stats['total_length']/1000:,.3f}"}
])
# 주의사항 추가
summary_data.extend([
{'항목': '', '': ''}, # 빈 줄
{'항목': '=== 주의사항 ===', '': ''},
{'항목': '⚠️ 확정된 데이터', '': '이 Excel은 Cutting Plan 확정 시점의 데이터입니다.'},
{'항목': '⚠️ 리비전 보호', '': '이후 BOM 리비전이 발생해도 이 데이터는 변경되지 않습니다.'},
{'항목': '⚠️ 수정 방법', '': '변경이 필요한 경우 수동으로 편집하거나 새로운 Cutting Plan을 작성하세요.'},
{'항목': '⚠️ 이슈 관리', '': '현장 이슈는 별도 이슈 관리 시스템을 사용하세요.'}
])
return summary_data
def check_finalization_status(self, job_no: str) -> Dict[str, Any]:
"""Cutting Plan 확정 상태 확인"""
try:
snapshot_info = self.snapshot_service.get_snapshot_info(job_no)
return {
"is_finalized": snapshot_info["has_snapshot"] and snapshot_info["is_locked"],
"can_export_finalized": snapshot_info["has_snapshot"] and snapshot_info["is_locked"],
"snapshot_info": snapshot_info if snapshot_info["has_snapshot"] else None,
"message": "확정된 Cutting Plan Excel 내보내기 가능" if snapshot_info["has_snapshot"] and snapshot_info["is_locked"] else "Cutting Plan을 먼저 확정해주세요"
}
except Exception as e:
logger.error(f"Failed to check finalization status: {e}")
return {
"is_finalized": False,
"can_export_finalized": False,
"message": f"확정 상태 확인 실패: {str(e)}"
}
def get_pipe_snapshot_excel_service(db: Session = None) -> PipeSnapshotExcelService:
"""PipeSnapshotExcelService 인스턴스 생성"""
if db is None:
db = next(get_db())
return PipeSnapshotExcelService(db)