2nd commit

This commit is contained in:
Envy_PC 2024-08-07 12:27:07 +09:00
parent 70db9d5d15
commit 8a9e6ec0d9
11 changed files with 526 additions and 0 deletions

0
async_tasks.py Normal file
View File

41
data/database.py Normal file
View File

@ -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')

42
data/loader.py Normal file
View File

@ -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}")

0
datas.db Normal file
View File

82
logger.py Normal file
View File

@ -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"<span style=\"color:{color};\">{msg}</span><br/>"
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'))

10
logs/application.log Normal file
View File

@ -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'

120
main.py Normal file
View File

@ -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)

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pyqt5
pandas

13
ui/fonts.py Normal file
View File

@ -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.")

128
ui/layouts.py Normal file
View File

@ -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}")

88
ui/toggleSwitch.py Normal file
View File

@ -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()