import sys
from PyQt5.QtWidgets import * # PyQt5에서 다양한 위젯들을 가져옵니다. 버튼, 레이아웃 등
from PyQt5.QtGui import * # PyQt5에서 그래픽 관련 클래스를 가져옵니다. 색상, 폰트 등
from PyQt5.QtCore import QDate, Qt # PyQt5에서 날짜와 정렬 관련 클래스를 가져옵니다.
from PyQt5 import uic # UI 파일을 로드하는 기능을 제공합니다.
import cx_Oracle as oci # Oracle 데이터베이스와 연결하는 라이브러리입니다.
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas # matplotlib 그래프를 Qt에 넣기 위한 캔버스를 생성하는 라이브러리입니다.
from matplotlib.figure import Figure # matplotlib에서 그래프를 그릴 수 있는 Figure 객체를 생성합니다.
import matplotlib.font_manager as fm # 한글 폰트를 처리하기 위한 라이브러리입니다.
import numpy as np # 수학적인 계산을 쉽게 할 수 있도록 도와주는 라이브러리입니다.
# 데이터베이스 연결 정보 (Oracle 데이터베이스에 연결)
sid = 'XE'
host = '127.0.0.1'
port = 1521
username = 'attendance'
password = '12345'
class AttendanceGraph(QWidget): # AttendanceGraph는 출석 비율을 그래프 형식으로 표시하는 클래스입니다.
def __init__(self, parent=None):
super().__init__(parent)
self.figure = Figure() # Figure 객체를 만들어서 그래프를 그릴 준비를 합니다.
self.canvas = FigureCanvas(self.figure) # 이 캔버스 위에 그래프를 그립니다.
layout = QVBoxLayout(self) # 수직 레이아웃을 만들어서 그래프를 위젯에 넣습니다.
layout.addWidget(self.canvas) # 캔버스를 레이아웃에 추가합니다.
font_path = "C:/Windows/Fonts/malgun.ttf" # 사용할 한글 폰트 파일 경로입니다.
self.font_prop = self.set_korean_font(font_path) # 한글 폰트를 설정합니다.
def set_korean_font(self, font_path): # 한글 폰트를 설정하는 메서드입니다.
try:
font = fm.FontProperties(fname=font_path) # 폰트 파일을 읽어서 FontProperties 객체로 만듭니다.
return font # 만든 폰트를 반환합니다.
except Exception as e:
print("폰트 로드 오류:", e) # 폰트를 불러오지 못했을 경우 오류를 출력합니다.
return None # 오류가 나면 None을 반환합니다.
def update_hbar_graph(self, monthly_data): # 월별 출석률 그래프를 업데이트하는 메서드입니다.
self.figure.clear() # 기존의 그래프를 지우고 새로운 그래프를 그리기 위해 초기화합니다.
ax = self.figure.add_subplot(111) # (1,1,1)형태로 1개의 서브플롯을 추가합니다. (행 1, 열 1, 위치 1)
months = [f"{i}월" for i in range(1, 13)] # "1월", "2월", ..., "12월"로 월별 이름을 만들어 리스트로 저장합니다.
attendance_rates = [0] * 12 # 12개월에 대한 출석률을 0으로 초기화합니다. (기본 출석률은 0%)
# 월별 출석 데이터를 순회하며 출석률을 계산합니다.
for month in range(1, 13):
if month in monthly_data: # 해당 월에 데이터가 있으면
data = monthly_data[month] # 해당 월의 출석 데이터를 가져옵니다.
total_o = data.get('O', 0) # 'O'는 출석을 의미합니다. 데이터가 없으면 0으로 설정합니다.
total_x = data.get('X', 0) # 'X'는 결석을 의미합니다. 데이터가 없으면 0으로 설정합니다.
total_triangles = data.get('△', 0) # '△'는 지각을 의미합니다. 데이터가 없으면 0으로 설정합니다.
adjusted_x = total_x + (total_triangles // 3) # 지각은 3번마다 결석으로 간주합니다.
total_attendance = total_o + adjusted_x + total_triangles # 전체 출석 수를 계산합니다.
if total_attendance > 0: # 출석자가 1명이라도 있으면 출석률을 계산합니다.
attendance_rates[month - 1] = (total_o / total_attendance) * 100 # 출석률을 계산하여 저장합니다.
y_pos = np.arange(len(months)) # 월별 위치를 y축에 맞게 설정합니다. (0~11)
colors = ['red' if rate < 66 else 'green' for rate in attendance_rates] # 출석률이 66% 미만이면 빨간색, 이상이면 초록색으로 표시합니다.
# 수평 막대 그래프를 그립니다. (출석률을 기준으로 월별 색상과 비율을 표시)
ax.barh(y_pos, attendance_rates, color=colors, align='center')
ax.set_yticks(y_pos) # y축의 위치를 설정합니다.
ax.set_yticklabels(months, fontproperties=self.font_prop) # 월 이름을 y축에 표시합니다.
ax.set_xlabel("출석 비율 (%)", fontproperties=self.font_prop) # x축 레이블을 "출석 비율 (%)"로 설정합니다.
ax.set_xlim(0, 100) # x축의 범위를 0에서 100까지 설정합니다.
ax.set_title("월별 출석 비율", fontproperties=self.font_prop) # 그래프 제목을 설정합니다.
self.canvas.draw() # 캔버스에 새로운 그래프를 그립니다.
class CustomCalendar(QCalendarWidget): # 사용자 정의 달력을 구현하는 클래스입니다.
def __init__(self, parent=None):
super().__init__(parent)
self.symbols = {} # 날짜별 출석 상태를 저장할 딕셔너리입니다. 예시: {QDate(2025, 3, 29): 'O'}
self.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader) # 세로 헤더를 없애고 날짜만 표시하도록 설정합니다.
self.parent = parent
self.load_attendance_data() # 출석 데이터를 불러오는 메서드를 호출합니다.
def paintCell(self, painter, rect, date): # 각 셀에 출석 상태와 시간을 그리는 메서드입니다.
super().paintCell(painter, rect, date) # 기본 셀을 먼저 그립니다.
if date in self.symbols: # 해당 날짜에 출석 상태가 있으면
symbol, time = self.symbols[date] # 출석 상태와 시간을 가져옵니다.
color_map = {'O': "blue", '△': "green", 'X': "red"} # 출석 상태에 따른 색상입니다.
painter.setPen(QColor(color_map.get(symbol, "black"))) # 출석 상태에 맞는 색상으로 설정합니다.
font = QFont("Arial", 12, QFont.Bold) # 텍스트는 굵은 글씨로 설정합니다.
painter.setFont(font)
painter.drawText(rect.adjusted(rect.width() // 3, 0, 0, 0), Qt.AlignLeft, symbol) # 셀의 왼쪽에 출석 상태를 그립니다.
if symbol != 'X': # 결석 상태가 아닌 경우에만 시간을 표시합니다.
painter.setFont(QFont("Arial", 6)) # 시간을 작은 글씨로 표시합니다.
painter.drawText(rect.adjusted(0, rect.height() // 2, 0, 0), Qt.AlignCenter, time) # 셀의 중앙에 시간을 표시합니다.
def load_attendance_data(self): # 데이터베이스에서 출석 데이터를 불러오는 메서드입니다.
try:
conn = oci.connect(f'{username}/{password}@{host}:{port}/{sid}') # Oracle DB에 연결합니다.
cursor = conn.cursor()
query = '''
SELECT ATD_DATE, STATUS, TO_CHAR(ATD_TIME, 'HH24:MI')
FROM ATTENDANCE.ATD
WHERE S_NO = 1
''' # 출석 데이터를 가져오는 SQL 쿼리입니다.
cursor.execute(query)
rows = cursor.fetchall() # 모든 출석 데이터를 가져옵니다.
status_map = {'P': 'O', 'L': '△', 'A': 'X'} # 출석 상태 코드 변환 (P: 출석, L: 지각, A: 결석)
monthly_data = {} # 월별 출석 데이터를 저장할 딕셔너리입니다.
self.symbols.clear() # 기존의 출석 상태를 초기화합니다.
for date, status, time in rows: # 데이터가 하나씩 순차적으로 처리됩니다.
qdate = QDate(date.year, date.month, date.day) # 날짜 객체를 만듭니다.
month = date.month # 날짜에서 월을 추출합니다.
# 시간을 기준으로 출석 상태를 결정합니다.
hour, minute = map(int, time.split(':')) # 시간을 시와 분으로 나눕니다.
if hour < 9:
symbol = 'O' # 오전 9시 이전은 출석으로 처리합니다.
elif 9 <= hour < 13:
symbol = '△' # 오후 1시 이전은 지각으로 처리합니다.
else:
symbol = 'X' # 오후 1시 이후는 결석으로 처리합니다.
self.symbols[qdate] = (symbol, time) # 해당 날짜에 출석 상태와 시간을 기록합니다.
if month not in monthly_data:
monthly_data[month] = {'O': 0, '△': 0, 'X': 0} # 월별 출석 데이터를 초기화합니다.
monthly_data[month][symbol] += 1 # 해당 월의 출석 상태 카운트를 증가시킵니다.
print("출결 데이터 로드 완료:", self.symbols)
self.updateCells() # 달력의 셀을 업데이트합니다.
self.parent.graph_widget.update_hbar_graph(monthly_data) # 그래프를 업데이트합니다.
except Exception as e:
print("데이터베이스 오류:", e) # 데이터베이스 오류 처리
finally:
cursor.close() # 커서 닫기
conn.close() # 데이터베이스 연결 닫기
class AttendanceApp(QMainWindow): # 출석 관리 앱의 메인 클래스입니다.
def __init__(self):
super().__init__()
uic.loadUi('./미니프젝/출석관리,통계3.ui', self) # UI 파일을 로드합니다.
self.graph_widget = AttendanceGraph(self) # 그래프 위젯을 생성합니다.
vertical_layout = self.findChild(QVBoxLayout, "verticalLayout") # UI에서 수직 레이아웃을 찾습니다.
if vertical_layout:
vertical_layout.addWidget(self.graph_widget) # 그래프 위젯을 레이아웃에 추가합니다.
vertical_layout.addStretch(1) # 레이아웃에 여유 공간을 추가합니다.
old_calendar = self.findChild(QCalendarWidget, "calendarWidget") # 기존 달력 위젯을 찾습니다.
if old_calendar:
self.custom_calendar = CustomCalendar(self) # 사용자 정의 달력을 만듭니다.
self.custom_calendar.setGeometry(old_calendar.geometry()) # 기존 달력의 위치와 크기를 복사합니다.
self.custom_calendar.setObjectName("calendarWidget") # 기존 달력과 동일한 이름을 설정합니다.
layout = old_calendar.parentWidget().layout() # 기존 달력의 부모 위젯에서 레이아웃을 찾습니다.
if layout:
layout.replaceWidget(old_calendar, self.custom_calendar) # 기존 달력을 새 달력으로 교체합니다.
old_calendar.deleteLater() # 기존 달력을 삭제합니다.
if __name__ == "__main__": # 이 부분은 프로그램 실행 시 최초로 호출되는 코드입니다.
app = QApplication(sys.argv) # QApplication 객체를 생성하여 프로그램 실행 준비를 합니다.
window = AttendanceApp() # 출석 관리 앱 창을 생성합니다.
window.show() # 창을 화면에 표시합니다.
sys.exit(app.exec_()) # 이벤트 루프를 실행하여 애플리케이션을 실행합니다.
import cx_Oracle as oci
from datetime import datetime, timedelta
import random
# 오라클 DB 접속 정보
sid = 'XE'
host = '127.0.0.1'
port = 1521
username = 'attendance'
password = '12345'
def calculate_status(atd_time):
# 출석 상태 계산 함수
# 7시~8시 사이 출석(P)으로 설정
hour = atd_time.hour
if 7 <= hour < 9:
return 'P' # 출석
else:
return 'P' # 그 외 시간대도 출석으로 설정
def random_time(current_date):
# 랜덤 시간 생성 (7시~8시)
# 출석만 나오게 설정
time = current_date.replace(hour=random.randint(7, 8), minute=random.randint(0, 59))
return time
def insert_august_attendance():
print("8월 출석 데이터 삽입 시작")
conn = oci.connect(f'{username}/{password}@{host}:{port}/{sid}')
cursor = conn.cursor()
try:
conn.begin()
# s_no = 1인 학생과 t_no = 1인 교사 확인
cursor.execute("SELECT S_NO, CLASS_NO FROM ATTENDANCE.STUDENT WHERE S_NO = 1")
student = cursor.fetchone()
cursor.execute("SELECT T_NO, CLASS_NO FROM ATTENDANCE.TEACHER WHERE T_NO = 1")
teacher = cursor.fetchone()
if not student or not teacher:
print("학생 또는 교사를 찾을 수 없습니다.")
return
s_no, student_class_no = student
t_no, teacher_class_no = teacher
if student_class_no != teacher_class_no:
print("학생과 교사의 CLASS_NO가 일치하지 않습니다.")
return
# 출석 데이터 생성 (2025년 8월 1일 ~ 8월 31일, 월~금만)
attendance_data = []
start_date = datetime(2025, 8, 1)
end_date = datetime(2025, 8, 31)
current_date = start_date
total_days = 0
total_o = 0 # 출석(동그라미)
total_l = 0 # 지각(세모)
l_dates = set() # 'L' 상태의 날짜를 저장할 집합
while current_date <= end_date:
if current_date.weekday() < 5: # 월~금만
total_days += 1
# 항상 출석 상태로 설정 (P)
status = 'P' # 출석
# 시간에 맞는 출석 상태 계산
atd_time = random_time(current_date)
status = calculate_status(atd_time) # 출석 상태 계산
attendance_data.append((s_no, current_date.strftime('%Y-%m-%d'), atd_time.strftime('%Y-%m-%d %H:%M:%S'), t_no, status))
current_date += timedelta(days=1)
# ATD 테이블에 삽입
query = '''
INSERT INTO ATTENDANCE.ATD (ATD_no, S_NO, ATD_DATE, ATD_TIME, T_NO, STATUS)
VALUES(ATTENDANCE.atd_no_seq.nextval, :1, TO_DATE(:2, 'YYYY-MM-DD'), TO_DATE(:3, 'YYYY-MM-DD HH24:MI:SS'), :4, :5)
'''
cursor.executemany(query, attendance_data)
conn.commit()
print(f"ATD 테이블에 {len(attendance_data)}개 출석 데이터 삽입 완료 (8월 한 달간, 월~금만 기록, 모두 출석(P))")
except Exception as e:
print("출석 데이터 삽입 오류 발생:", e)
conn.rollback()
finally:
cursor.close()
conn.close()
if __name__ == "__main__":
# 8월 출석 데이터 삽입
insert_august_attendance()