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 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
221 lines
9.6 KiB
Python
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)
|