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