1383 lines
49 KiB
Python
1383 lines
49 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
첨부파일 관리 다이얼로그 모듈
|
|
사진, PDF, HWP 등의 첨부파일을 업로드하고 관리하는 다이얼로그입니다.
|
|
"""
|
|
|
|
from typing import List
|
|
from pathlib import Path
|
|
from datetime import date
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QLabel,
|
|
QScrollArea, QGridLayout, QFrame, QPushButton, QLineEdit,
|
|
QFileDialog, QMenu, QDateEdit, QApplication, QInputDialog, QMessageBox
|
|
)
|
|
from PySide6.QtCore import Qt, Signal, QDate
|
|
from PySide6.QtGui import QPixmap, QDragEnterEvent, QDropEvent, QKeyEvent
|
|
|
|
from ui.base.base_dialog import BaseDialog
|
|
from services.storage_service import (
|
|
get_storage_service, Attachment,
|
|
SUPPORTED_IMAGE_EXTENSIONS, SUPPORTED_REPORT_EXTENSIONS,
|
|
REPORTS_DIR
|
|
)
|
|
from core.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class DropZone(QFrame):
|
|
"""드래그 앤 드롭 영역"""
|
|
|
|
files_dropped = Signal(list) # 파일 경로 리스트
|
|
|
|
def __init__(self, accept_types: str = "all", parent=None):
|
|
"""
|
|
Args:
|
|
accept_types: "all", "image", "report"
|
|
"""
|
|
super().__init__(parent)
|
|
self.accept_types = accept_types
|
|
self.setAcceptDrops(True)
|
|
self.setMinimumHeight(100)
|
|
self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setAlignment(Qt.AlignCenter)
|
|
|
|
icon_label = QLabel("📁")
|
|
icon_label.setStyleSheet("font-size: 32px;")
|
|
icon_label.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(icon_label)
|
|
|
|
if self.accept_types == "image":
|
|
text = "이미지 파일을 여기에 드롭하세요\n(jpg, png, gif, bmp, webp)\n\n💡 Ctrl+V: 클립보드에서 붙여넣기"
|
|
elif self.accept_types == "report":
|
|
text = "보고서 파일을 여기에 드롭하세요\n(pdf, hwp, doc, docx)"
|
|
else:
|
|
text = "파일을 여기에 드롭하세요"
|
|
|
|
text_label = QLabel(text)
|
|
text_label.setAlignment(Qt.AlignCenter)
|
|
text_label.setStyleSheet("color: #64748b;")
|
|
layout.addWidget(text_label)
|
|
|
|
# 기본 스타일
|
|
self._set_normal_style()
|
|
|
|
def _set_normal_style(self):
|
|
self.setStyleSheet("""
|
|
DropZone {
|
|
background-color: #1e293b;
|
|
border: 2px dashed #475569;
|
|
border-radius: 8px;
|
|
}
|
|
""")
|
|
|
|
def _set_hover_style(self):
|
|
self.setStyleSheet("""
|
|
DropZone {
|
|
background-color: #334155;
|
|
border: 2px dashed #3b82f6;
|
|
border-radius: 8px;
|
|
}
|
|
""")
|
|
|
|
def dragEnterEvent(self, event: QDragEnterEvent):
|
|
if event.mimeData().hasUrls():
|
|
# 파일 타입 검증
|
|
urls = event.mimeData().urls()
|
|
valid = False
|
|
for url in urls:
|
|
if url.isLocalFile():
|
|
ext = Path(url.toLocalFile()).suffix.lower()
|
|
if self.accept_types == "image":
|
|
valid = ext in SUPPORTED_IMAGE_EXTENSIONS
|
|
elif self.accept_types == "report":
|
|
valid = ext in SUPPORTED_REPORT_EXTENSIONS
|
|
else:
|
|
valid = ext in SUPPORTED_IMAGE_EXTENSIONS | SUPPORTED_REPORT_EXTENSIONS
|
|
if valid:
|
|
break
|
|
|
|
if valid:
|
|
event.acceptProposedAction()
|
|
self._set_hover_style()
|
|
else:
|
|
event.ignore()
|
|
|
|
def dragLeaveEvent(self, event):
|
|
self._set_normal_style()
|
|
|
|
def dropEvent(self, event: QDropEvent):
|
|
self._set_normal_style()
|
|
|
|
files = []
|
|
for url in event.mimeData().urls():
|
|
if url.isLocalFile():
|
|
file_path = url.toLocalFile()
|
|
ext = Path(file_path).suffix.lower()
|
|
|
|
valid = False
|
|
if self.accept_types == "image":
|
|
valid = ext in SUPPORTED_IMAGE_EXTENSIONS
|
|
elif self.accept_types == "report":
|
|
valid = ext in SUPPORTED_REPORT_EXTENSIONS
|
|
else:
|
|
valid = ext in SUPPORTED_IMAGE_EXTENSIONS | SUPPORTED_REPORT_EXTENSIONS
|
|
|
|
if valid:
|
|
files.append(file_path)
|
|
|
|
if files:
|
|
self.files_dropped.emit(files)
|
|
event.acceptProposedAction()
|
|
|
|
|
|
class ImageThumbnail(QFrame):
|
|
"""이미지 썸네일 위젯"""
|
|
|
|
clicked = Signal(object) # Attachment
|
|
delete_requested = Signal(object) # Attachment
|
|
open_folder_requested = Signal(object) # Attachment
|
|
rename_requested = Signal(object) # Attachment - 이름 변경 요청
|
|
|
|
def __init__(self, attachment: Attachment, parent=None):
|
|
super().__init__(parent)
|
|
self.attachment = attachment
|
|
self.setFixedSize(120, 140)
|
|
self.setCursor(Qt.PointingHandCursor)
|
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
self.customContextMenuRequested.connect(self._show_context_menu)
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(4, 4, 4, 4)
|
|
layout.setSpacing(4)
|
|
|
|
# 썸네일 이미지
|
|
self.image_label = QLabel()
|
|
self.image_label.setFixedSize(110, 100)
|
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
self.image_label.setStyleSheet("""
|
|
QLabel {
|
|
background-color: #0f172a;
|
|
border: 1px solid #334155;
|
|
border-radius: 4px;
|
|
}
|
|
""")
|
|
|
|
# 썸네일 로드
|
|
thumb_path = self.attachment.get_thumbnail_path()
|
|
if thumb_path and thumb_path.exists():
|
|
pixmap = QPixmap(str(thumb_path))
|
|
else:
|
|
full_path = self.attachment.get_full_path()
|
|
if full_path.exists():
|
|
pixmap = QPixmap(str(full_path))
|
|
else:
|
|
pixmap = QPixmap()
|
|
|
|
if not pixmap.isNull():
|
|
pixmap = pixmap.scaled(
|
|
100, 90,
|
|
Qt.KeepAspectRatio,
|
|
Qt.SmoothTransformation
|
|
)
|
|
self.image_label.setPixmap(pixmap)
|
|
|
|
layout.addWidget(self.image_label)
|
|
|
|
# 제목
|
|
title = self.attachment.title
|
|
if len(title) > 12:
|
|
title = title[:10] + "..."
|
|
|
|
title_label = QLabel(title)
|
|
title_label.setAlignment(Qt.AlignCenter)
|
|
title_label.setStyleSheet("font-size: 10px; color: #94a3b8;")
|
|
title_label.setToolTip(self.attachment.title)
|
|
layout.addWidget(title_label)
|
|
|
|
# 프레임 스타일
|
|
self.setStyleSheet("""
|
|
ImageThumbnail {
|
|
background-color: #1e293b;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
}
|
|
ImageThumbnail:hover {
|
|
border-color: #3b82f6;
|
|
}
|
|
""")
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == Qt.LeftButton:
|
|
self.clicked.emit(self.attachment)
|
|
super().mousePressEvent(event)
|
|
|
|
def _show_context_menu(self, pos):
|
|
menu = QMenu(self)
|
|
|
|
open_action = menu.addAction("📷 열기")
|
|
open_action.triggered.connect(lambda: self.clicked.emit(self.attachment))
|
|
|
|
menu.addSeparator()
|
|
|
|
rename_action = menu.addAction("✏️ 파일 이름 변경")
|
|
rename_action.triggered.connect(lambda: self.rename_requested.emit(self.attachment))
|
|
|
|
folder_action = menu.addAction("📂 폴더 위치 열기")
|
|
folder_action.triggered.connect(lambda: self.open_folder_requested.emit(self.attachment))
|
|
|
|
menu.addSeparator()
|
|
|
|
delete_action = menu.addAction("🗑 삭제")
|
|
delete_action.triggered.connect(lambda: self.delete_requested.emit(self.attachment))
|
|
|
|
menu.exec(self.mapToGlobal(pos))
|
|
|
|
|
|
class TemplateFileItem(QFrame):
|
|
"""템플릿 파일 항목 위젯 (보고서 작성 탭용)"""
|
|
|
|
clicked = Signal(str) # 파일 경로 - 클릭하면 보고서 작성 시작
|
|
file_opened = Signal(str) # 파일 경로 - 파일명 클릭 시 파일 열기
|
|
rename_requested = Signal(str) # 파일 경로 - 이름 변경 요청
|
|
|
|
def __init__(self, file_path: Path, parent=None):
|
|
super().__init__(parent)
|
|
self.file_path = file_path
|
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
self.customContextMenuRequested.connect(self._show_context_menu)
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(12, 10, 12, 10)
|
|
|
|
# 아이콘
|
|
ext = self.file_path.suffix.lower()
|
|
icon = "📝" if ext in {'.hwp', '.hwpx'} else "📄"
|
|
icon_label = QLabel(icon)
|
|
icon_label.setStyleSheet("font-size: 20px;")
|
|
layout.addWidget(icon_label)
|
|
|
|
# 파일명 (클릭 가능한 링크 스타일)
|
|
filename = self.file_path.name
|
|
self.title_label = QLabel(f'<a href="#" style="color: #60a5fa; text-decoration: none;">{filename}</a>')
|
|
self.title_label.setTextFormat(Qt.RichText)
|
|
self.title_label.setCursor(Qt.PointingHandCursor)
|
|
self.title_label.setToolTip(f"클릭하여 열기: {self.file_path}")
|
|
self.title_label.linkActivated.connect(lambda: self._open_file())
|
|
layout.addWidget(self.title_label, 1)
|
|
|
|
# 수정일
|
|
mtime = self.file_path.stat().st_mtime
|
|
from datetime import datetime
|
|
mtime_str = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d")
|
|
mtime_label = QLabel(mtime_str)
|
|
mtime_label.setStyleSheet("color: #64748b; font-size: 11px;")
|
|
layout.addWidget(mtime_label)
|
|
|
|
# 이 템플릿으로 작성 버튼
|
|
use_btn = QPushButton("✏️ 이 템플릿 사용")
|
|
use_btn.setCursor(Qt.PointingHandCursor)
|
|
use_btn.clicked.connect(lambda: self.clicked.emit(str(self.file_path)))
|
|
use_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #22c55e;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 8px 14px;
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #16a34a;
|
|
}
|
|
""")
|
|
layout.addWidget(use_btn)
|
|
|
|
def _open_file(self):
|
|
"""파일 열기"""
|
|
import os
|
|
if self.file_path.exists():
|
|
os.startfile(str(self.file_path))
|
|
self.file_opened.emit(str(self.file_path))
|
|
|
|
def _open_folder(self):
|
|
"""파일 위치 열기 (탐색기에서 파일 선택)"""
|
|
import subprocess
|
|
if self.file_path.exists():
|
|
subprocess.run(['explorer', '/select,', str(self.file_path)], check=False)
|
|
|
|
def _show_context_menu(self, pos):
|
|
"""컨텍스트 메뉴 표시"""
|
|
menu = QMenu(self)
|
|
|
|
# 파일 열기
|
|
open_action = menu.addAction("📄 파일 열기")
|
|
open_action.triggered.connect(self._open_file)
|
|
|
|
# 파일 위치 열기
|
|
folder_action = menu.addAction("📂 파일 위치 열기")
|
|
folder_action.triggered.connect(self._open_folder)
|
|
|
|
menu.addSeparator()
|
|
|
|
# 파일 이름 변경
|
|
rename_action = menu.addAction("✏️ 파일 이름 변경")
|
|
rename_action.triggered.connect(lambda: self.rename_requested.emit(str(self.file_path)))
|
|
|
|
menu.addSeparator()
|
|
|
|
# 이 템플릿으로 작성
|
|
use_action = menu.addAction("📝 이 템플릿으로 새 보고서 작성")
|
|
use_action.triggered.connect(lambda: self.clicked.emit(str(self.file_path)))
|
|
|
|
menu.exec(self.mapToGlobal(pos))
|
|
|
|
# 프레임 스타일
|
|
self.setStyleSheet("""
|
|
TemplateFileItem {
|
|
background-color: #1e293b;
|
|
border: 1px solid #334155;
|
|
border-radius: 8px;
|
|
}
|
|
TemplateFileItem:hover {
|
|
border-color: #3b82f6;
|
|
background-color: #334155;
|
|
}
|
|
""")
|
|
|
|
|
|
class ReportItem(QFrame):
|
|
"""보고서 항목 위젯"""
|
|
|
|
clicked = Signal(object) # Attachment
|
|
delete_requested = Signal(object) # Attachment
|
|
open_folder_requested = Signal(object) # Attachment
|
|
rename_requested = Signal(object) # Attachment - 이름 변경 요청
|
|
|
|
def __init__(self, attachment: Attachment, parent=None):
|
|
super().__init__(parent)
|
|
self.attachment = attachment
|
|
self.setCursor(Qt.PointingHandCursor)
|
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
self.customContextMenuRequested.connect(self._show_context_menu)
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
# 아이콘
|
|
icon = "📄" if self.attachment.file_type == 'pdf' else "📝"
|
|
icon_label = QLabel(icon)
|
|
icon_label.setStyleSheet("font-size: 20px;")
|
|
layout.addWidget(icon_label)
|
|
|
|
# 제목 (링크 스타일)
|
|
title_label = QLabel(f'<a href="#" style="color: #3b82f6; text-decoration: none;">{self.attachment.title}</a>')
|
|
title_label.setTextFormat(Qt.RichText)
|
|
title_label.setCursor(Qt.PointingHandCursor)
|
|
layout.addWidget(title_label, 1)
|
|
|
|
# 파일 크기
|
|
size_kb = self.attachment.file_size / 1024
|
|
if size_kb > 1024:
|
|
size_str = f"{size_kb / 1024:.1f} MB"
|
|
else:
|
|
size_str = f"{size_kb:.1f} KB"
|
|
|
|
size_label = QLabel(size_str)
|
|
size_label.setStyleSheet("color: #64748b; font-size: 11px;")
|
|
layout.addWidget(size_label)
|
|
|
|
# 프레임 스타일
|
|
self.setStyleSheet("""
|
|
ReportItem {
|
|
background-color: #1e293b;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
}
|
|
ReportItem:hover {
|
|
border-color: #3b82f6;
|
|
background-color: #334155;
|
|
}
|
|
""")
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == Qt.LeftButton:
|
|
self.clicked.emit(self.attachment)
|
|
super().mousePressEvent(event)
|
|
|
|
def _show_context_menu(self, pos):
|
|
menu = QMenu(self)
|
|
|
|
open_action = menu.addAction("📄 열기")
|
|
open_action.triggered.connect(lambda: self.clicked.emit(self.attachment))
|
|
|
|
menu.addSeparator()
|
|
|
|
rename_action = menu.addAction("✏️ 파일 이름 변경")
|
|
rename_action.triggered.connect(lambda: self.rename_requested.emit(self.attachment))
|
|
|
|
folder_action = menu.addAction("📂 폴더 위치 열기")
|
|
folder_action.triggered.connect(lambda: self.open_folder_requested.emit(self.attachment))
|
|
|
|
menu.addSeparator()
|
|
|
|
delete_action = menu.addAction("🗑 삭제")
|
|
delete_action.triggered.connect(lambda: self.delete_requested.emit(self.attachment))
|
|
|
|
menu.exec(self.mapToGlobal(pos))
|
|
|
|
|
|
class AttachmentDialog(BaseDialog):
|
|
"""첨부파일 관리 다이얼로그"""
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
record_type: str = "faults",
|
|
record_id: int = 0,
|
|
device_category: str = "",
|
|
record_info: dict = None # 레코드 정보 (파일명 생성용)
|
|
):
|
|
super().__init__(
|
|
parent,
|
|
title="관련자료",
|
|
width=900,
|
|
height=600,
|
|
resizable=True
|
|
)
|
|
|
|
self.record_type = record_type
|
|
self.record_id = record_id
|
|
self.device_category = device_category
|
|
self.record_info = record_info or {} # 레코드 정보 저장
|
|
self.storage = get_storage_service()
|
|
|
|
self._setup_attachment_ui()
|
|
self._set_default_titles() # 기본 제목 설정
|
|
self._load_attachments()
|
|
|
|
# 닫기 버튼만
|
|
self.add_button("닫기", self.close)
|
|
|
|
def _generate_default_folder_title(self) -> str:
|
|
"""기본 폴더 제목 생성 (레코드 정보 기반)"""
|
|
parts = []
|
|
|
|
# 편성번호
|
|
train_number = self.record_info.get("train_number", "")
|
|
if train_number:
|
|
parts.append(f"{train_number}편성")
|
|
|
|
# 호차
|
|
car_number = self.record_info.get("car_number", "")
|
|
if car_number:
|
|
parts.append(f"{car_number}호차")
|
|
|
|
# 장치분류
|
|
device_category = self.record_info.get("device_category", "")
|
|
if device_category:
|
|
parts.append(device_category)
|
|
|
|
# 고장내용 요약
|
|
fault_content = self.record_info.get("fault_content", "")
|
|
if fault_content:
|
|
summary = self._summarize_fault_content(fault_content)
|
|
if summary:
|
|
parts.append(summary)
|
|
|
|
return " ".join(parts) if parts else ""
|
|
|
|
def _summarize_fault_content(self, content: str) -> str:
|
|
"""
|
|
고장내용 요약 (NLP 도입 전 임시 메서드)
|
|
|
|
추후 NLP(자연어처리)를 도입하여 고장내용을 분석하고
|
|
핵심 키워드를 추출하는 방식으로 개선 예정
|
|
"""
|
|
# TODO: NLP 도입 시 이 메서드를 개선
|
|
if not content:
|
|
return ""
|
|
|
|
# 현재는 단순히 앞부분만 사용
|
|
max_len = 20
|
|
if len(content) <= max_len:
|
|
return content
|
|
return content[:max_len] + "..."
|
|
|
|
def _set_default_titles(self):
|
|
"""기본 제목 설정"""
|
|
# 레코드 정보가 있으면 기본 폴더명 설정
|
|
default_title = self._generate_default_folder_title()
|
|
if default_title:
|
|
self.image_title_input.setText(default_title)
|
|
self.report_title_input.setText(default_title) # 보고서 제목도 동일하게
|
|
|
|
# 발생일자가 있으면 날짜도 설정
|
|
occurrence_date = self.record_info.get("occurrence_date")
|
|
if occurrence_date:
|
|
if isinstance(occurrence_date, date):
|
|
self.image_date_edit.setDate(
|
|
QDate(occurrence_date.year, occurrence_date.month, occurrence_date.day)
|
|
)
|
|
self.report_date_edit.setDate(
|
|
QDate(occurrence_date.year, occurrence_date.month, occurrence_date.day)
|
|
)
|
|
|
|
def _setup_attachment_ui(self):
|
|
"""첨부파일 UI 설정"""
|
|
# 탭 위젯
|
|
self.tabs = QTabWidget()
|
|
self.tabs.setStyleSheet("""
|
|
QTabWidget::pane {
|
|
border: 1px solid #334155;
|
|
border-radius: 8px;
|
|
background-color: #1e293b;
|
|
}
|
|
QTabBar::tab {
|
|
background-color: #1e293b;
|
|
color: #94a3b8;
|
|
padding: 10px 20px;
|
|
border-top-left-radius: 6px;
|
|
border-top-right-radius: 6px;
|
|
}
|
|
QTabBar::tab:selected {
|
|
background-color: #334155;
|
|
color: #f8fafc;
|
|
}
|
|
""")
|
|
|
|
# 사진 탭
|
|
self.image_tab = self._create_image_tab()
|
|
self.tabs.addTab(self.image_tab, "📷 사진")
|
|
|
|
# 보고서 탭
|
|
self.report_tab = self._create_report_tab()
|
|
self.tabs.addTab(self.report_tab, "📄 보고서")
|
|
|
|
# 보고서 작성 탭
|
|
self.report_writer_tab = self._create_report_writer_tab()
|
|
self.tabs.addTab(self.report_writer_tab, "✏️ 보고서 작성")
|
|
|
|
self.content_layout.addWidget(self.tabs)
|
|
|
|
def _create_image_tab(self) -> QWidget:
|
|
"""사진 탭 생성"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
layout.setContentsMargins(10, 10, 10, 10)
|
|
layout.setSpacing(10)
|
|
|
|
# 상단 헤더 (폴더 열기 버튼)
|
|
header_widget = QWidget()
|
|
header_layout = QHBoxLayout(header_widget)
|
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
header_layout.setSpacing(8)
|
|
|
|
header_label = QLabel("📷 사진 첨부")
|
|
header_label.setStyleSheet("font-weight: bold; color: #f8fafc; font-size: 14px;")
|
|
header_layout.addWidget(header_label)
|
|
|
|
open_image_folder_btn = QPushButton("📁 사진 폴더 열기")
|
|
open_image_folder_btn.setCursor(Qt.PointingHandCursor)
|
|
open_image_folder_btn.clicked.connect(self._open_images_folder)
|
|
open_image_folder_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #475569;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 6px 12px;
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #64748b;
|
|
}
|
|
""")
|
|
header_layout.addWidget(open_image_folder_btn)
|
|
header_layout.addStretch(1)
|
|
|
|
layout.addWidget(header_widget)
|
|
|
|
# 업로드 영역
|
|
upload_widget = QWidget()
|
|
upload_layout = QHBoxLayout(upload_widget)
|
|
upload_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# 날짜 선택
|
|
self.image_date_edit = QDateEdit()
|
|
self.image_date_edit.setDate(QDate.currentDate())
|
|
self.image_date_edit.setCalendarPopup(True)
|
|
self.image_date_edit.setDisplayFormat("yyyy-MM-dd")
|
|
self.image_date_edit.setFixedWidth(140)
|
|
self.image_date_edit.setStyleSheet("""
|
|
QDateEdit {
|
|
background-color: #0f172a;
|
|
color: #f8fafc;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
padding: 8px 8px;
|
|
}
|
|
QDateEdit:focus {
|
|
border-color: #3b82f6;
|
|
}
|
|
QDateEdit::drop-down {
|
|
border: none;
|
|
width: 20px;
|
|
}
|
|
""")
|
|
upload_layout.addWidget(self.image_date_edit)
|
|
|
|
# 제목 입력
|
|
self.image_title_input = QLineEdit()
|
|
self.image_title_input.setPlaceholderText("폴더명 (예: 7편성 3호차 2위 출입문)")
|
|
self.image_title_input.setStyleSheet("""
|
|
QLineEdit {
|
|
background-color: #0f172a;
|
|
color: #f8fafc;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
}
|
|
QLineEdit:focus {
|
|
border-color: #3b82f6;
|
|
}
|
|
""")
|
|
upload_layout.addWidget(self.image_title_input, 1)
|
|
|
|
# 파일 선택 버튼
|
|
select_btn = QPushButton("파일 선택")
|
|
select_btn.setCursor(Qt.PointingHandCursor)
|
|
select_btn.clicked.connect(self._select_images)
|
|
select_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3b82f6;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 8px 16px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2563eb;
|
|
}
|
|
""")
|
|
upload_layout.addWidget(select_btn)
|
|
|
|
layout.addWidget(upload_widget)
|
|
|
|
# 드롭 영역
|
|
self.image_drop_zone = DropZone(accept_types="image")
|
|
self.image_drop_zone.files_dropped.connect(self._on_images_dropped)
|
|
layout.addWidget(self.image_drop_zone)
|
|
|
|
# 이미지 목록 (스크롤 가능)
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setStyleSheet("QScrollArea { border: none; }")
|
|
|
|
self.image_grid_widget = QWidget()
|
|
self.image_grid_layout = QGridLayout(self.image_grid_widget)
|
|
self.image_grid_layout.setSpacing(10)
|
|
self.image_grid_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
|
|
scroll.setWidget(self.image_grid_widget)
|
|
layout.addWidget(scroll, 1)
|
|
|
|
return tab
|
|
|
|
def _create_report_tab(self) -> QWidget:
|
|
"""보고서 탭 생성 (PDF/HWP 통합)"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
layout.setContentsMargins(10, 10, 10, 10)
|
|
layout.setSpacing(10)
|
|
|
|
# 상단 헤더 (폴더 열기 버튼)
|
|
header_widget = QWidget()
|
|
header_layout = QHBoxLayout(header_widget)
|
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
header_layout.setSpacing(8)
|
|
|
|
header_label = QLabel("📄 보고서 첨부")
|
|
header_label.setStyleSheet("font-weight: bold; color: #f8fafc; font-size: 14px;")
|
|
header_layout.addWidget(header_label)
|
|
|
|
open_report_folder_btn = QPushButton("📁 보고서 폴더 열기")
|
|
open_report_folder_btn.setCursor(Qt.PointingHandCursor)
|
|
open_report_folder_btn.clicked.connect(self._open_attached_reports_folder)
|
|
open_report_folder_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #475569;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 6px 12px;
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #64748b;
|
|
}
|
|
""")
|
|
header_layout.addWidget(open_report_folder_btn)
|
|
header_layout.addStretch(1)
|
|
|
|
layout.addWidget(header_widget)
|
|
|
|
# 업로드 영역 (사진 탭과 동일한 형태)
|
|
upload_widget = QWidget()
|
|
upload_layout = QHBoxLayout(upload_widget)
|
|
upload_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# 날짜 선택
|
|
self.report_date_edit = QDateEdit()
|
|
self.report_date_edit.setDate(QDate.currentDate())
|
|
self.report_date_edit.setCalendarPopup(True)
|
|
self.report_date_edit.setDisplayFormat("yyyy-MM-dd")
|
|
self.report_date_edit.setFixedWidth(140)
|
|
self.report_date_edit.setStyleSheet("""
|
|
QDateEdit {
|
|
background-color: #0f172a;
|
|
color: #f8fafc;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
padding: 8px 8px;
|
|
}
|
|
QDateEdit:focus {
|
|
border-color: #3b82f6;
|
|
}
|
|
QDateEdit::drop-down {
|
|
border: none;
|
|
width: 20px;
|
|
}
|
|
""")
|
|
upload_layout.addWidget(self.report_date_edit)
|
|
|
|
# 제목 입력
|
|
self.report_title_input = QLineEdit()
|
|
self.report_title_input.setPlaceholderText("보고서 제목 (예: 7편성 3호차 2위 출입문 닫힘 고장 동향보고)")
|
|
self.report_title_input.setStyleSheet("""
|
|
QLineEdit {
|
|
background-color: #0f172a;
|
|
color: #f8fafc;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
}
|
|
QLineEdit:focus {
|
|
border-color: #3b82f6;
|
|
}
|
|
""")
|
|
upload_layout.addWidget(self.report_title_input, 1)
|
|
|
|
# 파일 선택 버튼
|
|
select_btn = QPushButton("파일 선택")
|
|
select_btn.setCursor(Qt.PointingHandCursor)
|
|
select_btn.clicked.connect(self._select_reports)
|
|
select_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3b82f6;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 8px 16px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2563eb;
|
|
}
|
|
""")
|
|
upload_layout.addWidget(select_btn)
|
|
|
|
layout.addWidget(upload_widget)
|
|
|
|
# 드롭 영역
|
|
self.report_drop_zone = DropZone(accept_types="report")
|
|
self.report_drop_zone.files_dropped.connect(self._on_reports_dropped)
|
|
layout.addWidget(self.report_drop_zone)
|
|
|
|
# 보고서 목록 (스크롤 가능)
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setStyleSheet("QScrollArea { border: none; }")
|
|
|
|
self.report_list_widget = QWidget()
|
|
self.report_list_layout = QVBoxLayout(self.report_list_widget)
|
|
self.report_list_layout.setSpacing(8)
|
|
self.report_list_layout.setAlignment(Qt.AlignTop)
|
|
|
|
scroll.setWidget(self.report_list_widget)
|
|
layout.addWidget(scroll, 1)
|
|
|
|
return tab
|
|
|
|
def _create_report_writer_tab(self) -> QWidget:
|
|
"""보고서 작성 탭 생성 - 템플릿 목록만 표시하고 클릭 시 별도 다이얼로그 열기"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
layout.setContentsMargins(10, 10, 10, 10)
|
|
layout.setSpacing(10)
|
|
|
|
# 상단 안내 영역
|
|
header_widget = QWidget()
|
|
header_layout = QHBoxLayout(header_widget)
|
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
header_layout.setSpacing(10)
|
|
|
|
# 왼쪽: 카테고리 정보 및 안내
|
|
left_info = QWidget()
|
|
left_layout = QVBoxLayout(left_info)
|
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
left_layout.setSpacing(4)
|
|
|
|
category_name = self._get_device_category_name()
|
|
category_label = QLabel(f"📂 {category_name} 폴더의 기존 보고서")
|
|
category_label.setStyleSheet("font-weight: bold; color: #f8fafc; font-size: 14px;")
|
|
left_layout.addWidget(category_label)
|
|
|
|
info_label = QLabel("아래 목록에서 참고할 보고서를 선택하면 보고서 작성 화면이 열립니다.")
|
|
info_label.setStyleSheet("color: #94a3b8; font-size: 12px;")
|
|
left_layout.addWidget(info_label)
|
|
|
|
header_layout.addWidget(left_info, 1)
|
|
|
|
# 오른쪽: 보고서 폴더 열기 버튼
|
|
open_folder_btn = QPushButton("📁 보고서 폴더 열기")
|
|
open_folder_btn.setCursor(Qt.PointingHandCursor)
|
|
open_folder_btn.clicked.connect(self._open_reports_folder)
|
|
open_folder_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #475569;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 8px 14px;
|
|
font-size: 11px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #64748b;
|
|
}
|
|
""")
|
|
header_layout.addWidget(open_folder_btn)
|
|
|
|
layout.addWidget(header_widget)
|
|
|
|
# 템플릿 목록 스크롤
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setStyleSheet("""
|
|
QScrollArea {
|
|
border: 1px solid #334155;
|
|
border-radius: 8px;
|
|
background-color: #0f172a;
|
|
}
|
|
""")
|
|
|
|
self.template_list_widget = QWidget()
|
|
self.template_list_widget.setStyleSheet("background-color: #0f172a;")
|
|
self.template_list_layout = QVBoxLayout(self.template_list_widget)
|
|
self.template_list_layout.setContentsMargins(8, 8, 8, 8)
|
|
self.template_list_layout.setSpacing(6)
|
|
self.template_list_layout.setAlignment(Qt.AlignTop)
|
|
|
|
scroll.setWidget(self.template_list_widget)
|
|
layout.addWidget(scroll, 1)
|
|
|
|
# 템플릿 목록 로드
|
|
self._load_template_list()
|
|
|
|
return tab
|
|
|
|
def _get_device_category_name(self) -> str:
|
|
"""장치분류명 반환"""
|
|
category = self.device_category or "17.기타"
|
|
# "01.출입문" -> "출입문"
|
|
if "." in category:
|
|
return category.split(".", 1)[1]
|
|
return category
|
|
|
|
def _get_device_category_folder(self) -> Path:
|
|
"""장치분류 폴더 경로 반환"""
|
|
category = self.storage._match_category(self.device_category)
|
|
return REPORTS_DIR / category
|
|
|
|
def _load_template_list(self):
|
|
"""템플릿(기존 보고서) 목록 로드"""
|
|
layout = self.template_list_layout
|
|
|
|
# 기존 위젯 제거
|
|
while layout.count():
|
|
item = layout.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|
|
|
|
# 장치분류 폴더에서 HWP 파일 검색
|
|
category_folder = self._get_device_category_folder()
|
|
|
|
if not category_folder.exists():
|
|
no_files_label = QLabel("📁 보고서 폴더가 없습니다")
|
|
no_files_label.setStyleSheet("color: #64748b; padding: 40px; font-size: 13px;")
|
|
no_files_label.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(no_files_label)
|
|
return
|
|
|
|
# HWP, HWPX 파일 검색 (하위 폴더 포함)
|
|
hwp_files = []
|
|
for ext in ['*.hwp', '*.hwpx', '*.HWP', '*.HWPX']:
|
|
hwp_files.extend(category_folder.rglob(ext))
|
|
|
|
# 수정일 기준 정렬 (최신순)
|
|
hwp_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
|
|
|
if not hwp_files:
|
|
no_files_label = QLabel("📝 HWP 보고서가 없습니다\n\n이 폴더에 참고할 보고서 파일을 추가하세요.")
|
|
no_files_label.setStyleSheet("color: #64748b; padding: 40px; font-size: 13px;")
|
|
no_files_label.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(no_files_label)
|
|
return
|
|
|
|
for file_path in hwp_files:
|
|
item = TemplateFileItem(file_path)
|
|
# 클릭 시 보고서 작성 다이얼로그 열기
|
|
item.clicked.connect(self._on_open_report_writer)
|
|
item.rename_requested.connect(self._on_rename_template_file)
|
|
layout.addWidget(item)
|
|
|
|
def _open_reports_folder(self):
|
|
"""보고서 작성 탭 - 템플릿 보고서 폴더 열기 (storage/보고서)"""
|
|
import subprocess
|
|
|
|
if REPORTS_DIR.exists():
|
|
subprocess.run(['explorer', str(REPORTS_DIR)], check=False)
|
|
else:
|
|
# 폴더가 없으면 생성 후 열기
|
|
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
subprocess.run(['explorer', str(REPORTS_DIR)], check=False)
|
|
|
|
def _open_images_folder(self):
|
|
"""사진 탭 - 첨부된 사진 폴더 열기"""
|
|
import subprocess
|
|
from services.storage_service import STORAGE_DIR
|
|
|
|
# 현재 레코드의 사진 저장 폴더
|
|
images_folder = STORAGE_DIR / self.record_type / str(self.record_id) / "images"
|
|
|
|
if images_folder.exists():
|
|
subprocess.run(['explorer', str(images_folder)], check=False)
|
|
else:
|
|
# 폴더가 없으면 상위 레코드 폴더 열기
|
|
record_folder = STORAGE_DIR / self.record_type / str(self.record_id)
|
|
if record_folder.exists():
|
|
subprocess.run(['explorer', str(record_folder)], check=False)
|
|
else:
|
|
# 레코드 폴더도 없으면 storage 폴더 열기
|
|
subprocess.run(['explorer', str(STORAGE_DIR)], check=False)
|
|
|
|
def _open_attached_reports_folder(self):
|
|
"""보고서 탭 - 첨부된 보고서 폴더 열기"""
|
|
import subprocess
|
|
from services.storage_service import STORAGE_DIR
|
|
|
|
# 현재 레코드의 보고서 저장 폴더
|
|
reports_folder = STORAGE_DIR / self.record_type / str(self.record_id) / "reports"
|
|
|
|
if reports_folder.exists():
|
|
subprocess.run(['explorer', str(reports_folder)], check=False)
|
|
else:
|
|
# 폴더가 없으면 상위 레코드 폴더 열기
|
|
record_folder = STORAGE_DIR / self.record_type / str(self.record_id)
|
|
if record_folder.exists():
|
|
subprocess.run(['explorer', str(record_folder)], check=False)
|
|
else:
|
|
# 레코드 폴더도 없으면 storage 폴더 열기
|
|
subprocess.run(['explorer', str(STORAGE_DIR)], check=False)
|
|
|
|
def _on_open_report_writer(self, template_path: str):
|
|
"""보고서 작성 시작 - 원본/새파일을 좌우로 배치하여 한글 실행"""
|
|
from ui.dialogs.report_writer_dialog import start_report_writing
|
|
|
|
# 보고서 작성 시작 (원본: 왼쪽, 새파일: 오른쪽)
|
|
self._current_report_writer = start_report_writing(
|
|
template_path=template_path,
|
|
device_category=self.device_category,
|
|
record_info=self.record_info
|
|
)
|
|
|
|
# 다이얼로그 닫기
|
|
self.close()
|
|
|
|
def _on_report_saved(self, file_path: str):
|
|
"""보고서 저장 완료 시 호출"""
|
|
logger.info(f"보고서 저장됨: {file_path}")
|
|
# 템플릿 목록 새로고침
|
|
self._load_template_list()
|
|
|
|
def _load_attachments(self):
|
|
"""첨부파일 로드"""
|
|
# 이미지 로드
|
|
self._load_images()
|
|
|
|
# 보고서 로드
|
|
self._load_reports()
|
|
|
|
def _load_images(self):
|
|
"""이미지 목록 로드"""
|
|
# 기존 위젯 제거
|
|
while self.image_grid_layout.count():
|
|
item = self.image_grid_layout.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|
|
|
|
images = self.storage.get_attachments_by_type(
|
|
self.record_type, self.record_id, "image"
|
|
)
|
|
|
|
for i, attachment in enumerate(images):
|
|
row, col = divmod(i, 5) # 5열
|
|
thumb = ImageThumbnail(attachment)
|
|
thumb.clicked.connect(self._on_image_clicked)
|
|
thumb.delete_requested.connect(self._on_delete_attachment)
|
|
thumb.open_folder_requested.connect(self._on_open_folder)
|
|
thumb.rename_requested.connect(self._on_rename_attachment)
|
|
self.image_grid_layout.addWidget(thumb, row, col)
|
|
|
|
def _load_reports(self):
|
|
"""보고서 목록 로드 (PDF + HWP 통합)"""
|
|
layout = self.report_list_layout
|
|
|
|
# 기존 위젯 제거
|
|
while layout.count():
|
|
item = layout.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|
|
|
|
# PDF와 HWP 모두 로드
|
|
all_reports = []
|
|
for report_type in ["pdf", "hwp", "doc"]:
|
|
reports = self.storage.get_attachments_by_type(
|
|
self.record_type, self.record_id, report_type
|
|
)
|
|
all_reports.extend(reports)
|
|
|
|
# 날짜순 정렬 (최신순)
|
|
all_reports.sort(key=lambda x: x.created_at, reverse=True)
|
|
|
|
for attachment in all_reports:
|
|
item = ReportItem(attachment)
|
|
item.clicked.connect(self._on_report_clicked)
|
|
item.delete_requested.connect(self._on_delete_attachment)
|
|
item.open_folder_requested.connect(self._on_open_folder)
|
|
item.rename_requested.connect(self._on_rename_attachment)
|
|
layout.addWidget(item)
|
|
|
|
def _select_images(self):
|
|
"""이미지 파일 선택"""
|
|
files, _ = QFileDialog.getOpenFileNames(
|
|
self,
|
|
"이미지 선택",
|
|
"",
|
|
"이미지 파일 (*.jpg *.jpeg *.png *.gif *.bmp *.webp)"
|
|
)
|
|
|
|
if files:
|
|
self._upload_images(files)
|
|
|
|
def _select_reports(self):
|
|
"""보고서 파일 선택"""
|
|
files, _ = QFileDialog.getOpenFileNames(
|
|
self,
|
|
"보고서 선택",
|
|
"",
|
|
"보고서 파일 (*.pdf *.hwp *.hwpx *.doc *.docx)"
|
|
)
|
|
|
|
if files:
|
|
self._upload_reports(files)
|
|
|
|
def _on_images_dropped(self, files: List[str]):
|
|
"""이미지 드롭"""
|
|
self._upload_images(files)
|
|
|
|
def _on_reports_dropped(self, files: List[str]):
|
|
"""보고서 드롭"""
|
|
self._upload_reports(files)
|
|
|
|
def _upload_images(self, files: List[str]):
|
|
"""이미지 업로드"""
|
|
folder_title = self.image_title_input.text().strip()
|
|
if not folder_title:
|
|
folder_title = "사진"
|
|
|
|
# QDate -> date 변환
|
|
qdate = self.image_date_edit.date()
|
|
folder_date = date(qdate.year(), qdate.month(), qdate.day())
|
|
|
|
self.storage.save_images(
|
|
file_paths=files,
|
|
record_type=self.record_type,
|
|
record_id=self.record_id,
|
|
folder_title=folder_title,
|
|
folder_date=folder_date
|
|
)
|
|
|
|
self.image_title_input.clear()
|
|
self._load_images()
|
|
|
|
def _upload_reports(self, files: List[str]):
|
|
"""보고서 업로드"""
|
|
title = self.report_title_input.text().strip()
|
|
qdate = self.report_date_edit.date()
|
|
|
|
# QDate -> date 변환
|
|
report_date = date(qdate.year(), qdate.month(), qdate.day())
|
|
|
|
# 장치분류는 레코드 정보에서 가져옴
|
|
category = self.device_category or "17.기타"
|
|
|
|
for file_path in files:
|
|
file_title = title or Path(file_path).stem
|
|
self.storage.save_report(
|
|
file_path=file_path,
|
|
record_type=self.record_type,
|
|
record_id=self.record_id,
|
|
device_category=category,
|
|
report_title=file_title,
|
|
report_date=report_date
|
|
)
|
|
|
|
self.report_title_input.clear()
|
|
self._load_reports()
|
|
|
|
def _on_image_clicked(self, attachment: Attachment):
|
|
"""이미지 클릭 - 뷰어 열기"""
|
|
from ui.dialogs.image_viewer_dialog import ImageViewerDialog
|
|
|
|
# 현재 이미지 목록
|
|
images = self.storage.get_attachments_by_type(
|
|
self.record_type, self.record_id, "image"
|
|
)
|
|
|
|
# 현재 이미지 인덱스 찾기
|
|
current_index = 0
|
|
for i, img in enumerate(images):
|
|
if img.id == attachment.id:
|
|
current_index = i
|
|
break
|
|
|
|
dialog = ImageViewerDialog(
|
|
self,
|
|
images,
|
|
current_index,
|
|
record_info=self.record_info
|
|
)
|
|
dialog.exec()
|
|
|
|
# 저장 후 목록 새로고침
|
|
self._load_images()
|
|
|
|
def _on_report_clicked(self, attachment: Attachment):
|
|
"""보고서 클릭 - 파일 열기"""
|
|
self.storage.open_file(attachment)
|
|
|
|
def _on_delete_attachment(self, attachment: Attachment):
|
|
"""첨부파일 삭제"""
|
|
result = self.storage.delete_attachment(
|
|
self.record_type, self.record_id, attachment.id
|
|
)
|
|
|
|
if result:
|
|
self._load_attachments()
|
|
|
|
def _on_open_folder(self, attachment: Attachment):
|
|
"""폴더 위치 열기"""
|
|
import subprocess
|
|
|
|
full_path = attachment.get_full_path()
|
|
folder_path = full_path.parent
|
|
|
|
if folder_path.exists():
|
|
# Windows에서 탐색기로 폴더 열기 (파일 선택 상태로)
|
|
subprocess.run(['explorer', '/select,', str(full_path)], check=False)
|
|
|
|
def _on_rename_attachment(self, attachment: Attachment):
|
|
"""첨부파일 이름 변경 (사진, 보고서)"""
|
|
full_path = attachment.get_full_path()
|
|
if not full_path.exists():
|
|
logger.warning(f"파일이 존재하지 않습니다: {full_path}")
|
|
return
|
|
|
|
old_name = full_path.stem # 확장자 제외 파일명
|
|
ext = full_path.suffix # 확장자
|
|
|
|
# 새 이름 입력받기
|
|
new_name, ok = QInputDialog.getText(
|
|
self,
|
|
"파일 이름 변경",
|
|
f"새 파일 이름을 입력하세요:\n(확장자 {ext}는 자동으로 추가됩니다)",
|
|
text=old_name
|
|
)
|
|
|
|
if not ok or not new_name.strip():
|
|
return
|
|
|
|
new_name = new_name.strip()
|
|
|
|
# 새 경로 생성
|
|
new_path = full_path.parent / f"{new_name}{ext}"
|
|
|
|
if new_path.exists():
|
|
QMessageBox.warning(self, "오류", f"'{new_name}{ext}' 파일이 이미 존재합니다.")
|
|
return
|
|
|
|
try:
|
|
# 파일 이름 변경
|
|
full_path.rename(new_path)
|
|
|
|
# 썸네일도 있으면 이름 변경
|
|
thumb_path = attachment.get_thumbnail_path()
|
|
if thumb_path and thumb_path.exists():
|
|
new_thumb_name = f"{new_name}_thumb{thumb_path.suffix}"
|
|
new_thumb_path = thumb_path.parent / new_thumb_name
|
|
thumb_path.rename(new_thumb_path)
|
|
|
|
# attachment 객체 업데이트 및 DB 저장
|
|
attachment.filename = f"{new_name}{ext}"
|
|
attachment.title = new_name
|
|
get_storage_service().update_attachment(attachment)
|
|
|
|
logger.info(f"파일 이름 변경: {old_name}{ext} -> {new_name}{ext}")
|
|
|
|
# 목록 새로고침
|
|
self._load_images()
|
|
self._load_reports()
|
|
|
|
except Exception as e:
|
|
logger.error(f"파일 이름 변경 실패: {e}")
|
|
QMessageBox.warning(self, "오류", f"파일 이름 변경에 실패했습니다:\n{e}")
|
|
|
|
def _on_rename_template_file(self, file_path: str):
|
|
"""템플릿 파일 이름 변경 (보고서 작성 탭)"""
|
|
path = Path(file_path)
|
|
if not path.exists():
|
|
logger.warning(f"파일이 존재하지 않습니다: {path}")
|
|
return
|
|
|
|
old_name = path.stem # 확장자 제외 파일명
|
|
ext = path.suffix # 확장자
|
|
|
|
# 새 이름 입력받기
|
|
new_name, ok = QInputDialog.getText(
|
|
self,
|
|
"파일 이름 변경",
|
|
f"새 파일 이름을 입력하세요:\n(확장자 {ext}는 자동으로 추가됩니다)",
|
|
text=old_name
|
|
)
|
|
|
|
if not ok or not new_name.strip():
|
|
return
|
|
|
|
new_name = new_name.strip()
|
|
|
|
# 새 경로 생성
|
|
new_path = path.parent / f"{new_name}{ext}"
|
|
|
|
if new_path.exists():
|
|
QMessageBox.warning(self, "오류", f"'{new_name}{ext}' 파일이 이미 존재합니다.")
|
|
return
|
|
|
|
try:
|
|
# 파일 이름 변경
|
|
path.rename(new_path)
|
|
logger.info(f"템플릿 파일 이름 변경: {old_name}{ext} -> {new_name}{ext}")
|
|
|
|
# 목록 새로고침
|
|
self._load_template_list()
|
|
|
|
except Exception as e:
|
|
logger.error(f"파일 이름 변경 실패: {e}")
|
|
QMessageBox.warning(self, "오류", f"파일 이름 변경에 실패했습니다:\n{e}")
|
|
|
|
def keyPressEvent(self, event: QKeyEvent):
|
|
"""키 이벤트 - Ctrl+V로 클립보드 이미지 붙여넣기"""
|
|
if event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_V:
|
|
# 사진 탭이 활성화되어 있을 때만
|
|
if self.tabs.currentWidget() == self.image_tab:
|
|
if self._paste_image_from_clipboard():
|
|
event.accept()
|
|
return
|
|
|
|
super().keyPressEvent(event)
|
|
|
|
def _paste_image_from_clipboard(self) -> bool:
|
|
"""클립보드에서 이미지 붙여넣기
|
|
|
|
Returns:
|
|
성공 여부
|
|
"""
|
|
import tempfile
|
|
from datetime import datetime
|
|
|
|
clipboard = QApplication.clipboard()
|
|
mime_data = clipboard.mimeData()
|
|
|
|
# 이미지가 있는지 확인
|
|
if mime_data.hasImage():
|
|
image = clipboard.image()
|
|
if image.isNull():
|
|
return False
|
|
|
|
# 임시 파일로 저장
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
temp_dir = Path(tempfile.gettempdir())
|
|
temp_file = temp_dir / f"clipboard_{timestamp}.png"
|
|
|
|
# QImage를 파일로 저장
|
|
if image.save(str(temp_file), "PNG"):
|
|
# 업로드
|
|
self._upload_images([str(temp_file)])
|
|
|
|
# 임시 파일 삭제 시도 (실패해도 무시)
|
|
try:
|
|
temp_file.unlink()
|
|
except Exception:
|
|
pass
|
|
|
|
logger.info("클립보드에서 이미지 붙여넣기 완료")
|
|
return True
|
|
|
|
# 파일 URL이 있는 경우 (파일 복사 후 붙여넣기)
|
|
elif mime_data.hasUrls():
|
|
files = []
|
|
for url in mime_data.urls():
|
|
if url.isLocalFile():
|
|
file_path = url.toLocalFile()
|
|
ext = Path(file_path).suffix.lower()
|
|
if ext in SUPPORTED_IMAGE_EXTENSIONS:
|
|
files.append(file_path)
|
|
|
|
if files:
|
|
self._upload_images(files)
|
|
logger.info("클립보드에서 %d개 이미지 파일 붙여넣기 완료", len(files))
|
|
return True
|
|
|
|
return False
|
|
|