""" 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)