2nd commit
This commit is contained in:
parent
70db9d5d15
commit
8a9e6ec0d9
|
|
@ -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')
|
||||
|
|
@ -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,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'))
|
||||
|
|
@ -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'
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
pyqt5
|
||||
pandas
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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()
|
||||
Loading…
Reference in New Issue