347 lines
11 KiB
Python
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
|