From 4e6b2460664aa826745d43e7c0b28d6a93933d34 Mon Sep 17 00:00:00 2001 From: KWON Date: Mon, 21 Jul 2025 17:36:12 +0900 Subject: [PATCH] =?UTF-8?q?POS=EC=97=90=EC=84=9C=20=EC=82=BD=EC=9E=85?= =?UTF-8?q?=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20GUI=EB=A1=9C?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94?= =?UTF-8?q?=20=ED=88=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pos_view_gui.py | 195 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 lib/pos_view_gui.py diff --git a/lib/pos_view_gui.py b/lib/pos_view_gui.py new file mode 100644 index 0000000..0897885 --- /dev/null +++ b/lib/pos_view_gui.py @@ -0,0 +1,195 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import customtkinter as ctk +import tkinter.font as tkFont +from tkinter import messagebox, ttk +from tkcalendar import DateEntry +from datetime import datetime, timedelta +from sqlalchemy import select, func, between +from conf import db_schema, db + +# Windows DPI Awareness 설정 (윈도우 전용) +if sys.platform == "win32": + import ctypes + try: + ctypes.windll.shcore.SetProcessDpiAwareness(1) # SYSTEM_AWARE = 1 + except Exception: + pass + +pos_table = db_schema.pos +engine = db.engine + +class PosViewGUI(ctk.CTk): + def __init__(self): + super().__init__() + + self.title("POS 데이터 조회") + self.geometry("900x500") + self.configure(fg_color="#f0f0f0") # 배경색 맞춤 + + ctk.set_appearance_mode("light") + ctk.set_default_color_theme("blue") + + # 폰트 세팅 - NanumGothic이 없으면 Arial 대체 + try: + self.label_font = ("NanumGothic", 13) + except Exception: + self.label_font = ("Arial", 13) + + # Treeview 스타일 설정 (ttk 스타일) + style = ttk.Style(self) + style.theme_use('default') + style.configure("Treeview", + font=("NanumGothic", 12), + rowheight=30) # 높이 조절로 글씨 깨짐 방지 + style.configure("Treeview.Heading", + font=("NanumGothic", 13, "bold")) + + # --- 위젯 배치 --- + + # 날짜 범위 + ctk.CTkLabel(self, text="시작일:", anchor="w", font=self.label_font, fg_color="#f0f0f0")\ + .grid(row=0, column=0, padx=10, pady=5, sticky="e") + self.start_date_entry = DateEntry(self, width=12, background='darkblue', foreground='white') + self.start_date_entry.grid(row=0, column=1, padx=10, pady=5, sticky="w") + + ctk.CTkLabel(self, text="종료일:", anchor="w", font=self.label_font, fg_color="#f0f0f0")\ + .grid(row=0, column=2, padx=10, pady=5, sticky="e") + self.end_date_entry = DateEntry(self, width=12, background='darkblue', foreground='white') + self.end_date_entry.grid(row=0, column=3, padx=10, pady=5, sticky="w") + + # 대분류 + ctk.CTkLabel(self, text="대분류 :", anchor="w", font=self.label_font, fg_color="#f0f0f0")\ + .grid(row=1, column=0, padx=10, pady=5, sticky="e") + self.ca01_combo = ctk.CTkComboBox(self, values=["전체"], width=180) + self.ca01_combo.grid(row=1, column=1, padx=10, pady=5, sticky="w") + self.ca01_combo.configure(command=self.on_ca01_selected) + + # 소분류 + ctk.CTkLabel(self, text="소분류 :", anchor="w", font=self.label_font, fg_color="#f0f0f0")\ + .grid(row=1, column=2, padx=10, pady=5, sticky="e") + self.ca03_combo = ctk.CTkComboBox(self, values=["전체"], width=180) + self.ca03_combo.grid(row=1, column=3, padx=10, pady=5, sticky="w") + + # 상품명 + ctk.CTkLabel(self, text="상품명 :", anchor="w", font=self.label_font, fg_color="#f0f0f0")\ + .grid(row=2, column=0, padx=10, pady=5, sticky="e") + self.name_entry = ctk.CTkEntry(self, width=280) + self.name_entry.grid(row=2, column=1, columnspan=3, padx=10, pady=5, sticky="w") + + # 조회 버튼 + self.search_btn = ctk.CTkButton(self, text="조회", command=self.search, + fg_color="#0d6efd", hover_color="#0b5ed7", text_color="white") + self.search_btn.grid(row=3, column=0, columnspan=4, pady=10) + + # 결과 Treeview + self.DISPLAY_COLUMNS = ['ca01', 'ca02', 'ca03', 'name', 'qty', 'tot_amount', 'tot_discount', 'actual_amount'] + self.COLUMN_LABELS = { + 'ca01': '대분류', + 'ca02': '중분류', + 'ca03': '소분류', + 'name': '상품명', + 'qty': '수량', + 'tot_amount': '총매출액', + 'tot_discount': '총할인액', + 'actual_amount': '실매출액' + } + + self.tree = ttk.Treeview(self, columns=self.DISPLAY_COLUMNS, show='headings', height=15) + for col in self.DISPLAY_COLUMNS: + self.tree.heading(col, text=self.COLUMN_LABELS[col]) + self.tree.column(col, width=120, anchor='center') + self.tree.grid(row=4, column=0, columnspan=4, padx=10, pady=10, sticky="nsew") + + # 그리드 가중치 설정 (창 크기에 따라 트리뷰 확장) + self.grid_rowconfigure(4, weight=1) + for col_index in range(4): + self.grid_columnconfigure(col_index, weight=1) + + # 날짜 기본값 설정 (전날부터 7일 전까지) + end_date = datetime.today().date() - timedelta(days=1) + start_date = end_date - timedelta(days=6) + self.start_date_entry.set_date(start_date) + self.end_date_entry.set_date(end_date) + + # 초기 대분류, 소분류 콤보박스 값 불러오기 + self.load_ca01_options() + + def on_ca01_selected(self, value): + # print("대분류 선택됨:", value) 디버깅용 + self.load_ca03_options() + + def load_ca01_options(self): + start_date = self.start_date_entry.get_date() + end_date = self.end_date_entry.get_date() + with engine.connect() as conn: + stmt = select(pos_table.c.ca01).where( + between(pos_table.c.date, start_date, end_date) + ).distinct().order_by(pos_table.c.ca01) + result = conn.execute(stmt) + ca01_list = [row[0] for row in result.fetchall()] + self.ca01_combo.configure(values=['전체'] + ca01_list) + self.ca01_combo.set('전체') + self.load_ca03_options() + + def load_ca03_options(self): + start_date = self.start_date_entry.get_date() + end_date = self.end_date_entry.get_date() + ca01_val = self.ca01_combo.get() + with engine.connect() as conn: + stmt = select(pos_table.c.ca03).where( + between(pos_table.c.date, start_date, end_date) + ) + if ca01_val != '전체': + stmt = stmt.where(pos_table.c.ca01 == ca01_val) + stmt = stmt.distinct().order_by(pos_table.c.ca03) + result = conn.execute(stmt) + ca03_list = [row[0] for row in result.fetchall()] + self.ca03_combo.configure(values=['전체'] + ca03_list) + self.ca03_combo.set('전체') # 항상 기본값으로 초기화 + + def search(self): + start_date = self.start_date_entry.get_date() + end_date = self.end_date_entry.get_date() + ca01_val = self.ca01_combo.get() + ca03_val = self.ca03_combo.get() + name_val = self.name_entry.get().strip() + + conditions = [between(pos_table.c.date, start_date, end_date)] + if ca01_val != '전체': + conditions.append(pos_table.c.ca01 == ca01_val) + if ca03_val != '전체': + conditions.append(pos_table.c.ca03 == ca03_val) + if name_val: + conditions.append(pos_table.c.name.like(f"%{name_val}%")) + + with engine.connect() as conn: + stmt = select( + pos_table.c.ca01, + pos_table.c.ca02, + pos_table.c.ca03, + pos_table.c.name, + func.sum(pos_table.c.qty).label("qty"), + func.sum(pos_table.c.tot_amount).label("tot_amount"), + func.sum(pos_table.c.tot_discount).label("tot_discount"), + func.sum(pos_table.c.actual_amount).label("actual_amount") + ).where(*conditions).group_by(pos_table.c.barcode).order_by(pos_table.c.ca01, pos_table.c.ca03) + + result = conn.execute(stmt).mappings().all() + + self.tree.delete(*self.tree.get_children()) + for row in result: + values = tuple(row[col] for col in self.DISPLAY_COLUMNS) + self.tree.insert('', 'end', values=values) + +if __name__ == "__main__": + try: + import tkcalendar + except ImportError: + print("tkcalendar가 설치되어 있지 않습니다. 'pip install tkcalendar'로 설치해주세요.") + sys.exit(1) + + app = PosViewGUI() + app.mainloop()