# -*- coding: utf-8 -*- """ 보고서 작성 모듈 원본 파일과 새 파일을 좌우로 배치하여 한글 프로그램을 실행합니다. 파일 수정 여부를 MD5 해시로 확인하여 미수정 파일은 자동 삭제합니다. """ import os import shutil import hashlib import ctypes import subprocess import time from pathlib import Path from datetime import datetime from typing import List, Tuple from PySide6.QtCore import Signal, QObject, QThread from services.storage_service import REPORTS_DIR from core.logger import get_logger logger = get_logger(__name__) # Windows API try: import win32gui import win32con WIN32_AVAILABLE = True except ImportError: WIN32_AVAILABLE = False logger.warning("pywin32가 설치되어 있지 않습니다.") def calculate_file_hash(file_path: Path) -> str: """파일의 MD5 해시 계산""" if not file_path.exists(): return "" hash_md5 = hashlib.md5() with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() def get_screen_size() -> Tuple[int, int]: """화면 크기 가져오기""" user32 = ctypes.windll.user32 width = user32.GetSystemMetrics(0) height = user32.GetSystemMetrics(1) return width, height def open_file(file_path: str): """파일 열기 (시스템 기본 프로그램)""" if Path(file_path).exists(): os.startfile(file_path) def get_hwp_windows() -> List[int]: """현재 열린 한글 창 목록 가져오기""" if not WIN32_AVAILABLE: return [] windows = [] def enum_callback(hwnd, results): if win32gui.IsWindowVisible(hwnd): class_name = win32gui.GetClassName(hwnd) title = win32gui.GetWindowText(hwnd) # 한글 창 클래스명 패턴 if any(x in class_name for x in ['Hwp', 'HNC', 'Hnc']) or \ any(x in title for x in ['.hwp', '.HWP', '한글', '한컴']): results.append(hwnd) return True try: win32gui.EnumWindows(enum_callback, windows) except Exception: pass return windows def move_window(hwnd: int, x: int, y: int, width: int, height: int): """창 위치 및 크기 조정""" if not WIN32_AVAILABLE: return try: # 최대화 상태 해제 win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) time.sleep(0.1) # 위치 및 크기 조정 win32gui.SetWindowPos( hwnd, win32con.HWND_TOP, x, y, width, height, win32con.SWP_SHOWWINDOW ) except Exception as e: logger.error(f"창 이동 실패: {e}") class WindowArrangeThread(QThread): """창 배치를 위한 백그라운드 스레드""" finished = Signal() def __init__(self, before_windows: List[int], parent=None): super().__init__(parent) self.before_windows = set(before_windows) self.screen_width, self.screen_height = get_screen_size() def run(self): """새로 열린 창 2개를 찾아서 좌우로 배치""" half_width = self.screen_width // 2 # 최대 10초 동안 새 창 2개가 열릴 때까지 대기 new_windows = [] for _ in range(20): # 0.5초 * 20 = 10초 time.sleep(0.5) current_windows = get_hwp_windows() new_windows = [w for w in current_windows if w not in self.before_windows] if len(new_windows) >= 2: break # 찾은 창들 배치 if len(new_windows) >= 2: # 첫 번째 창 (먼저 열린 것) -> 왼쪽 move_window(new_windows[0], 0, 0, half_width, self.screen_height) time.sleep(0.2) # 두 번째 창 (나중에 열린 것) -> 오른쪽 move_window(new_windows[1], half_width, 0, half_width, self.screen_height) logger.info(f"창 배치 완료: 왼쪽={new_windows[0]}, 오른쪽={new_windows[1]}") elif len(new_windows) == 1: # 창이 하나만 열린 경우 (같은 프로세스에서 2개 탭으로 열릴 수 있음) logger.warning("한글 창이 1개만 감지됨. 탭으로 열렸을 수 있습니다.") else: logger.warning("새로 열린 한글 창을 찾지 못했습니다.") self.finished.emit() class ReportWriter(QObject): """보고서 작성기 원본 파일은 화면 왼쪽 절반에, 새 파일은 오른쪽 절반에 배치하여 두 개의 한글 프로그램을 동시에 실행합니다. """ # 보고서 작성 완료 시그널 report_saved = Signal(str) def __init__( self, template_path: str, device_category: str = "", record_info: dict = None ): super().__init__() self.template_path = template_path self.device_category = device_category self.record_info = record_info or {} self._current_new_report_path: Path = None self._original_hash: str = "" self._arrange_thread: WindowArrangeThread = None def _get_category_folder(self) -> Path: """장치분류 폴더 경로 반환""" category = self._match_category(self.device_category) return REPORTS_DIR / category def _match_category(self, device_category: str) -> str: """장치분류 매칭""" from services.storage_service import REPORT_CATEGORIES if not device_category: return "17.기타" for cat in REPORT_CATEGORIES: if cat == device_category: return cat try: num = int(device_category.lstrip('0')) for cat in REPORT_CATEGORIES: cat_num = int(cat.split('.')[0]) if cat_num == num: return cat except ValueError: pass for cat in REPORT_CATEGORIES: if device_category in cat: return cat return "17.기타" def _generate_new_report_filename(self) -> str: """새 보고서 파일명 생성""" today_str = datetime.now().strftime("%Y%m%d") train_no = self.record_info.get("train_no", "") train_number = self.record_info.get("train_number", "") car_number = self.record_info.get("car_number", "") fault_content = self.record_info.get("fault_content", "") parts = [f"({today_str})1호선"] if train_no: parts.append(f"제{train_no}열차") if train_number: parts.append(f"{train_number}편성") if car_number: parts.append(f"{car_number}호차") if fault_content: summary = fault_content[:30].strip() parts.append(summary) template_name = Path(self.template_path).stem.lower() if "동향" in template_name: parts.append("동향보고") elif "조치결과" in template_name: parts.append("조치결과보고") elif "분석" in template_name: parts.append("분석보고") else: parts.append("동향보고") filename = " ".join(parts) invalid_chars = '<>:"/\\|?*' for char in invalid_chars: filename = filename.replace(char, '') return filename def start_writing(self) -> bool: """보고서 작성 시작 - 두 개의 한글 창을 좌우로 배치""" if not self.template_path or not Path(self.template_path).exists(): logger.error("템플릿 파일이 존재하지 않습니다.") return False # 파일명 생성 new_filename = self._generate_new_report_filename() if not new_filename.lower().endswith(('.hwp', '.hwpx')): new_filename += '.hwp' # 저장 경로 category_folder = self._get_category_folder() category_folder.mkdir(parents=True, exist_ok=True) new_file_path = category_folder / new_filename # 파일 복사 try: shutil.copy2(self.template_path, new_file_path) self._current_new_report_path = new_file_path self._original_hash = calculate_file_hash(new_file_path) logger.info(f"새 보고서 파일 생성: {new_file_path}") except Exception as e: logger.error(f"파일 복사 실패: {e}") return False # 현재 열린 한글 창 목록 저장 (나중에 새 창 구분용) before_windows = get_hwp_windows() # 원본 파일 열기 subprocess.Popen(['cmd', '/c', 'start', '', self.template_path], shell=False) # 약간 대기 후 새 파일 열기 time.sleep(0.5) subprocess.Popen(['cmd', '/c', 'start', '', str(new_file_path)], shell=False) # 백그라운드에서 창 배치 self._arrange_thread = WindowArrangeThread(before_windows) self._arrange_thread.start() return True def is_file_modified(self) -> bool: """파일이 수정되었는지 확인 (MD5 해시 비교)""" if not self._current_new_report_path or not self._current_new_report_path.exists(): return False if not self._original_hash: return True current_hash = calculate_file_hash(self._current_new_report_path) return current_hash != self._original_hash def cleanup_if_unmodified(self) -> bool: """수정되지 않은 파일 삭제. 삭제되면 True 반환""" if self._current_new_report_path and self._current_new_report_path.exists(): if not self.is_file_modified(): try: self._current_new_report_path.unlink() logger.info(f"미수정 파일 삭제: {self._current_new_report_path}") return True except Exception as e: logger.error(f"파일 삭제 실패: {e}") return False def get_new_file_path(self) -> Path: """새 파일 경로 반환""" return self._current_new_report_path def start_report_writing( template_path: str, device_category: str = "", record_info: dict = None ) -> ReportWriter: """보고서 작성 시작 (간편 함수) 원본 파일과 새 파일을 좌우로 배치하여 한글을 실행합니다. Args: template_path: 템플릿 파일 경로 device_category: 장치분류 record_info: 레코드 정보 (파일명 생성용) Returns: ReportWriter 인스턴스 (나중에 수정 여부 확인용) """ writer = ReportWriter( template_path=template_path, device_category=device_category, record_info=record_info ) writer.start_writing() return writer