diff --git a/async_tasks.py b/async_tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/data/database.py b/data/database.py new file mode 100644 index 0000000..f08b0d1 --- /dev/null +++ b/data/database.py @@ -0,0 +1,41 @@ +import pandas as pd +import sqlite3 + +class DataLoader: + def __init__(self, db_path='datas.db'): + self.db_path = db_path + + def load_data_from_excel(self, excel_path): + # 엑셀 파일 읽기 + df = pd.read_excel(excel_path) + + # 데이터베이스 연결 + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 테이블 생성 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS CircuitBreakers ( + id INTEGER PRIMARY KEY, + symbol TEXT, + english_name TEXT, + korean_name TEXT, + function_description TEXT, + position TEXT + ) + ''') + + # 데이터 삽입 + for _, row in df.iterrows(): + cursor.execute(''' + INSERT INTO CircuitBreakers (id, symbol, english_name, korean_name, function_description, position) + VALUES (?, ?, ?, ?, ?, ?) + ''', (row['순서'], row['기호'], row['원어'], row['명칭'], row['기능설명'], row['위치'], row['도입년월'], row['도입명칭'], row['제작사'])) + + # 데이터베이스 저장 및 닫기 + conn.commit() + conn.close() + +# # 예제 사용 +# loader = DataLoader() +# loader.load_data_from_excel('path_to_excel_file.xlsx') diff --git a/data/loader.py b/data/loader.py new file mode 100644 index 0000000..7a3be41 --- /dev/null +++ b/data/loader.py @@ -0,0 +1,42 @@ +import pandas as pd +import sqlite3 +from logger import default_logger + +class DataLoader: + def __init__(self, db_path='datas.db'): + self.db_path = db_path + + def load_data_from_excel(self, excel_path): + try: + # 엑셀 파일 읽기 + df = pd.read_excel(excel_path) + + # 데이터베이스 연결 + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 테이블 생성 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS CircuitBreakers ( + id INTEGER PRIMARY KEY, + symbol TEXT, + english_name TEXT, + korean_name TEXT, + function_description TEXT, + remarks TEXT + ) + ''') + + # 데이터 삽입 + for _, row in df.iterrows(): + cursor.execute(''' + INSERT INTO CircuitBreakers (id, symbol, english_name, korean_name, function_description, remarks) + VALUES (?, ?, ?, ?, ?, ?) + ''', (row['순서'], row['기호'], row['원어'], row['명칭'], row['기능설명'], row['비고'])) + + # 데이터베이스 저장 및 닫기 + conn.commit() + conn.close() + default_logger.info(f"Data loaded successfully from {excel_path}") + except Exception as e: + default_logger.error(f"Error loading data from Excel: {e}") diff --git a/datas.db b/datas.db new file mode 100644 index 0000000..e69de29 diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..a6964e6 --- /dev/null +++ b/logger.py @@ -0,0 +1,82 @@ +import logging +import os +from logging.handlers import RotatingFileHandler +from PyQt5.QtCore import pyqtSignal, QObject + +def setup_logger(name, log_file, level=logging.DEBUG, max_bytes=10*1024*1024, backup_count=5): + """로거 설정을 위한 함수 + 다양한 로그 레벨을 지원하며, 파일 및 콘솔에 로그를 출력합니다. + + Args: + name (str): 로거의 이름. + log_file (str): 로그 파일의 경로. + level (int): 로그 레벨 (DEBUG, INFO, WARNING, ERROR, CRITICAL). + max_bytes (int): 로그 파일의 최대 크기 (바이트). + backup_count (int): 백업할 로그 파일의 개수. + + Returns: + logging.Logger: 설정된 로거 객체. + """ + formatter = logging.Formatter('%(asctime)s - %(filename)s:%(lineno)d - %(name)s - %(levelname)s - %(message)s') + + # RotatingFileHandler를 사용하여 로그 파일 설정 + handler = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count, encoding='utf-8') + handler.setFormatter(formatter) + + logger = logging.getLogger(name) + logger.setLevel(level) + logger.addHandler(handler) + + # 콘솔 로그 출력을 위한 핸들러가 이미 추가되었는지 확인 + if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers): + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + console_handler.setLevel(level) + logger.addHandler(console_handler) + + return logger + +class QTextEditLogger(logging.Handler, QObject): + appendHtml = pyqtSignal(str) # HTML 메시지를 전달할 시그널 정의 + scrollToBottom = pyqtSignal() # 스크롤을 최하단으로 이동시키는 시그널 + + def __init__(self): + logging.Handler.__init__(self) + QObject.__init__(self) + + def emit(self, record): + msg = self.format(record) # 로그 레코드를 문자열로 포매팅 + + if record.levelno == logging.DEBUG: + color = "black" + elif record.levelno == logging.INFO: + color = "grey" + elif record.levelno == logging.WARNING: + color = "orange" + elif record.levelno == logging.ERROR: + color = "red" + elif record.levelno == logging.CRITICAL: + color = "purple" + else: + color = "black" + + # HTML 스타일을 적용한 메시지 생성 + message = f"{msg}
" + self.appendHtml.emit(message) # HTML 메시지로 변경 + self.scrollToBottom.emit() # 스크롤 시그널 발생 + + def close(self): + # 핸들러 종료 시 필요한 작업 구현 + self.flush() + logging.Handler.close(self) + + def flush(self): + # 커스텀 flush 구현 + pass + +# 로거 인스턴스 설정 +log_directory = "logs" +if not os.path.exists(log_directory): + os.makedirs(log_directory) + +default_logger = setup_logger('default_logger', os.path.join(log_directory, 'application.log')) diff --git a/logs/application.log b/logs/application.log new file mode 100644 index 0000000..145ef53 --- /dev/null +++ b/logs/application.log @@ -0,0 +1,10 @@ +2024-07-27 23:25:30,116 - layouts.py:100 - default_logger - ERROR - Error updating table: Execution failed on sql 'SELECT * FROM CircuitBreakers': no such table: CircuitBreakers +2024-07-27 23:29:59,925 - layouts.py:107 - default_logger - ERROR - Error updating table: Execution failed on sql 'SELECT * FROM CircuitBreakers': no such table: CircuitBreakers +2024-07-27 23:30:37,827 - layouts.py:107 - default_logger - ERROR - Error updating table: Execution failed on sql 'SELECT * FROM CircuitBreakers': no such table: CircuitBreakers +2024-07-27 23:30:39,975 - layouts.py:37 - default_logger - INFO - Toggle 항상위 turned on. +2024-07-27 23:35:13,967 - layouts.py:107 - default_logger - ERROR - Error updating table: Execution failed on sql 'SELECT * FROM CircuitBreakers': no such table: CircuitBreakers +2024-07-27 23:38:58,838 - main.py:136 - default_logger - ERROR - Unexpected error: name 'sys' is not defined +2024-07-27 23:39:08,890 - main.py:72 - default_logger - ERROR - Error adding layouts: arguments did not match any overloaded call: + addWidget(self, w: Optional[QWidget]): argument 1 has unexpected type 'ToggleLayout' + addWidget(self, a0: Optional[QWidget], row: int, column: int, alignment: Union[Qt.Alignment, Qt.AlignmentFlag] = Qt.Alignment()): argument 1 has unexpected type 'ToggleLayout' + addWidget(self, a0: Optional[QWidget], row: int, column: int, rowSpan: int, columnSpan: int, alignment: Union[Qt.Alignment, Qt.AlignmentFlag] = Qt.Alignment()): argument 1 has unexpected type 'ToggleLayout' diff --git a/main.py b/main.py new file mode 100644 index 0000000..ba3a169 --- /dev/null +++ b/main.py @@ -0,0 +1,120 @@ +# main.py +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QAction, QFileDialog, QMenuBar +from ui.layouts import QHLayout, QVLayout, TableLayout, LogLayout, SearchTextLayout, ToggleLayout +from ui.fonts import set_font +from data.loader import DataLoader +from logger import default_logger + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + try: + self.initUI() + except Exception as e: + default_logger.error(f"Error initializing UI: {e}") + sys.exit(1) + + def initUI(self): + self.setGeometry(100, 100, 450, 900) + self.setWindowTitle('전동차 데이터 관리 프로그램') + + # 중앙에 배치 + self.center() + + # 메인 위젯과 레이아웃 설정 + central_widget = QWidget() + layout = QVBoxLayout() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + + # 레이아웃 추가 + self.addLayouts(layout) + + # 메뉴 설정 + self.createMenuBar() + + def addLayouts(self, layout): + try: + settings_layout = QHLayout() + settings_layout.addLayout(ToggleLayout("항상위")) + settings_layout.addLayout(ToggleLayout("검색히스토리 저장")) + layout.addLayout(settings_layout) + + search_layout = SearchTextLayout() + layout.addLayout(search_layout) + + categories_layout = QHLayout() + categories = [ + "전체", "차체", "ATC/ATO", "냉난방", "출입문", "도입단계", + "제동", "추진장치", "SIV", "공기", "화재감지", "CCTV", "기타장치" + ] + for category in categories: + toggle = ToggleLayout(category) + categories_layout.addLayout(toggle) + layout.addLayout(categories_layout) + + self.table_layout = TableLayout() + layout.addLayout(self.table_layout) + + self.log_layout = LogLayout() + layout.addLayout(self.log_layout) + except Exception as e: + default_logger.error(f"Error adding layouts: {e}") + + def center(self): + try: + frame_geom = self.frameGeometry() + screen = QApplication.desktop().screenNumber(QApplication.desktop().cursor().pos()) + center_point = QApplication.desktop().screenGeometry(screen).center() + frame_geom.moveCenter(center_point) + self.move(frame_geom.topLeft()) + except Exception as e: + default_logger.error(f"Error centering window: {e}") + + def createMenuBar(self): + try: + menu_bar = QMenuBar(self) + self.setMenuBar(menu_bar) + + file_menu = menu_bar.addMenu('File') + + load_action = QAction('데이터로드', self) + load_action.triggered.connect(self.load_data) + file_menu.addAction(load_action) + + delete_action = QAction('데이터삭제', self) + delete_action.triggered.connect(self.delete_data) + file_menu.addAction(delete_action) + except Exception as e: + default_logger.error(f"Error creating menu bar: {e}") + + def load_data(self): + try: + options = QFileDialog.Options() + file_path, _ = QFileDialog.getOpenFileName(self, "엑셀 파일 선택", "", "엑셀 파일 (*.xlsx; *.xls);;All Files (*)", options=options) + if file_path: + loader = DataLoader() + loader.load_data_from_excel(file_path) + self.table_layout.update_table() + except Exception as e: + default_logger.error(f"Error loading data: {e}") + + def delete_data(self): + try: + if os.path.exists('datas.db'): + os.remove('datas.db') + self.table_layout.clear_table() + except Exception as e: + default_logger.error(f"Error deleting data: {e}") + +if __name__ == '__main__': + try: + app = QApplication(sys.argv) + set_font(app) + main_win = MainWindow() + main_win.show() + sys.exit(app.exec_()) + except Exception as e: + default_logger.error(f"Unexpected error: {e}") + sys.exit(1) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..99b43d8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyqt5 +pandas \ No newline at end of file diff --git a/ui/fonts.py b/ui/fonts.py new file mode 100644 index 0000000..aab7355 --- /dev/null +++ b/ui/fonts.py @@ -0,0 +1,13 @@ +from PyQt5.QtGui import QFontDatabase, QFont +import os + +def set_font(app): + font_path = 'fonts/mainFont.ttf' + if os.path.exists(font_path): + font_id = QFontDatabase.addApplicationFont(font_path) + if font_id != -1: + families = QFontDatabase.applicationFontFamilies(font_id) + if families: + app.setFont(QFont(families[0])) + else: + print("Custom font not found, using default font.") diff --git a/ui/layouts.py b/ui/layouts.py new file mode 100644 index 0000000..0343e9f --- /dev/null +++ b/ui/layouts.py @@ -0,0 +1,128 @@ +# ui/layouts.py +from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QCheckBox, QLineEdit, QTableWidget, QPlainTextEdit, QTableWidgetItem, QHeaderView +import sqlite3 +import pandas as pd +from logger import default_logger +from ui.toggleSwitch import ToggleSwitch + +class QHLayout(QHBoxLayout): + def __init__(self): + super().__init__() + +class QVLayout(QVBoxLayout): + def __init__(self): + super().__init__() + +class ToggleLayout(QHBoxLayout): + def __init__(self, label_text, initial_state=False): + super().__init__() + self.label = QLabel(label_text) + self.toggle_switch = ToggleSwitch() + self.toggle_switch.setState(initial_state) + + if label_text == "전체": + self.label.setStyleSheet("font-weight: bold; font-size: 14px;") + self.toggle_switch.setStyleSheet("font-weight: bold; font-size: 14px;") + + self.addWidget(self.label) + self.addWidget(self.toggle_switch) + + self.toggle_switch.clicked.connect(self.on_toggle) + + def is_toggled(self): + return self.toggle_switch.isChecked() + + def on_toggle(self, checked): + state = "on" if checked else "off" + default_logger.info(f"Toggle {self.label.text()} turned {state}.") + +class SearchTextLayout(QHBoxLayout): + def __init__(self): + super().__init__() + self.label = QLabel('검색') + self.addWidget(self.label) + self.search_field = QLineEdit() + self.addWidget(self.search_field) + + self.search_history = [] + + self.search_field.textChanged.connect(self.search) + + def search(self): + search_text = self.search_field.text() + default_logger.info(f"Searching for: {search_text}") + if search_text and search_text not in self.search_history: + self.search_history.append(search_text) + self.async_search(search_text) + + def async_search(self, text): + pass + +class LogLayout(QVBoxLayout): + def __init__(self): + super().__init__() + self.label = QLabel('로그') + self.addWidget(self.label) + self.log_area = QPlainTextEdit() + self.log_area.setReadOnly(True) + self.addWidget(self.log_area) + + def log_message(self, message): + self.log_area.appendPlainText(message) + +class TableLayout(QVBoxLayout): + def __init__(self): + super().__init__() + self.table = QTableWidget() + self.addWidget(self.table) + self.init_table() + + def init_table(self): + try: + self.table.setColumnCount(6) + self.table.setHorizontalHeaderLabels(['순서', '기호', '원어', '명칭', '기능설명', '비고']) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.table.setSelectionBehavior(QTableWidget.SelectRows) + self.update_table() + except Exception as e: + default_logger.error(f"Error initializing table: {e}") + + def update_table(self): + try: + conn = sqlite3.connect('datas.db') + query = "SELECT * FROM CircuitBreakers" + result = pd.read_sql_query(query, conn) + conn.close() + + self.table.setRowCount(len(result)) + for i, row in result.iterrows(): + for j, value in enumerate(row): + item = QTableWidgetItem(str(value)) + item.setFlags(item.flags() ^ Qt.ItemIsEditable) + self.table.setItem(i, j, item) + + self.table.cellClicked.connect(self.on_cell_click) + self.table.cellDoubleClicked.connect(self.on_cell_double_click) + except Exception as e: + default_logger.error(f"Error updating table: {e}") + + def on_cell_click(self, row, column): + try: + default_logger.info(f"Cell clicked: ({row}, {column})") + for col in range(self.table.columnCount()): + self.table.item(row, col).setBackground(Qt.yellow) + except Exception as e: + default_logger.error(f"Error on cell click: {e}") + + def on_cell_double_click(self, row, column): + try: + default_logger.info(f"Cell double clicked: ({row}, {column})") + except Exception as e: + default_logger.error(f"Error on cell double click: {e}") + + def clear_table(self): + try: + self.table.setRowCount(0) + default_logger.info("Table cleared.") + except Exception as e: + default_logger.error(f"Error clearing table: {e}") diff --git a/ui/toggleSwitch.py b/ui/toggleSwitch.py new file mode 100644 index 0000000..b10d13e --- /dev/null +++ b/ui/toggleSwitch.py @@ -0,0 +1,88 @@ +from PyQt5.QtCore import Qt, QRect, QPropertyAnimation, pyqtProperty, pyqtSignal, QPoint +from PyQt5.QtGui import QPainter, QColor +from PyQt5.QtWidgets import QWidget +import logging + +# 로거 인스턴스 가져오기 +logger = logging.getLogger('default_logger') + +class ToggleSwitch(QWidget): + clicked = pyqtSignal(bool) + + def __init__(self, parent=None): + super(ToggleSwitch, self).__init__(parent) + self.setFixedSize(40, 20) + self._checked = False + self._circle_color_checked = QColor('red') + self._circle_color_unchecked = QColor('gray') + self._background_color = QColor('white') + self._circle_pos = QPoint(0, 0) # Circle's initial position. + self.animation = QPropertyAnimation(self, b"circle_pos") + self.animation.setDuration(250) + + self._init_position() + + @pyqtProperty(QPoint) + def circle_pos(self): + return self._circle_pos + + @circle_pos.setter + def circle_pos(self, pos): + self._circle_pos = pos + self.update() + + def _init_position(self): + if self._checked: + self._circle_pos.setX(10) + else: + self._circle_pos.setX(0) + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self._checked = not self._checked + self.clicked.emit(self._checked) + self._update_animation() + self.update() + super(ToggleSwitch, self).mousePressEvent(event) + + def _update_animation(self): + if self._checked: + self.animation.setStartValue(QPoint(0, 0)) + self.animation.setEndValue(QPoint(20, 0)) + else: + self.animation.setStartValue(QPoint(20, 0)) + self.animation.setEndValue(QPoint(0, 0)) + self.animation.start() + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + painter.setPen(Qt.NoPen) + painter.setBrush(self._background_color) + painter.drawRoundedRect(QRect(0, 0, 40, 20), 10, 10) + + circle_color = self._circle_color_checked if self._checked else self._circle_color_unchecked + + painter.setBrush(circle_color) + painter.drawEllipse(self._circle_pos.x(), self._circle_pos.y(), 20, 20) + + def setChecked(self, checked): + if self._checked != checked: + self._checked = checked + self._update_animation() + self.update() + + def isChecked(self): + return self._checked + + def setState(self, state): + """ToggleSwitch의 상태를 설정합니다. + + Args: + state (bool): True로 설정하면 스위치를 체크 상태로, False로 설정하면 언체크 상태로 변경합니다. + """ + if self._checked != state: + self._checked = state + self._update_animation() + self.clicked.emit(self._checked) + self.update() \ No newline at end of file