handOver2/ui/dialogs/report_writer_dialog.py

347 lines
11 KiB
Python

# -*- 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