VOC_Monitor/app/view/dialogs/history_dialog.py

298 lines
12 KiB
Python

import customtkinter as ctk
from view.theme import theme_manager
from datetime import datetime
from view.components.date_range_selector import DateRangeSelector
class HistoryDialog(ctk.CTkToplevel):
def __init__(self, controller, data_rows):
super().__init__()
self.controller = controller
self.author = " by KH.Choi"
self.title("VOC 수집 내역" + self.author)
self.geometry("1200x700") # 너비 확장
self.all_data = data_rows # 필터링용 원본 데이터
# --- 상단: 툴바 (필터 + 테마 + 새로고침) ---
self.toolbar = ctk.CTkFrame(self, height=60, fg_color="transparent")
self.toolbar.pack(fill="x", padx=20, pady=(20, 10))
# 1. 날짜 필터
ctk.CTkLabel(self.toolbar, text="기간:", font=theme_manager.get_font(14, "bold")).pack(side="left", padx=(0, 10))
self.date_selector = DateRangeSelector(
self.toolbar,
on_change_callback=lambda start, end: self.apply_filter(),
fg_color="transparent"
)
self.date_selector.pack(side="left", padx=(0, 20))
# 2. 검색
ctk.CTkLabel(self.toolbar, text="검색:", font=theme_manager.get_font(14, "bold")).pack(side="left", padx=(0, 10))
self.search_entry = ctk.CTkEntry(self.toolbar, width=100, placeholder_text="제목, 내용, 부서 검색...", font=theme_manager.get_font(12))
self.search_entry.pack(side="left")
self.search_entry.bind("<KeyRelease>", lambda e: self.apply_filter())
# 3. 우측 버튼 그룹
self.btn_settings = ctk.CTkButton(self.toolbar, text="⚙️ 설정", width=100, command=self.open_settings, font=theme_manager.get_font(12))
self.btn_settings.pack(side="right", padx=5)
self.btn_theme = ctk.CTkButton(self.toolbar, text="테마 변경", width=100, command=self.toggle_theme, font=theme_manager.get_font(12))
self.btn_theme.pack(side="right", padx=5)
self.btn_refresh = ctk.CTkButton(self.toolbar, text="새로고침", width=100, command=self.refresh_and_update, font=theme_manager.get_font(12))
self.btn_refresh.pack(side="right", padx=5)
# 통계 버튼 추가
self.btn_statistics = ctk.CTkButton(
self.toolbar,
text="📊 통계 보기",
width=110,
command=self.open_statistics,
font=theme_manager.get_font(12),
fg_color="#2563eb",
hover_color="#1d4ed8"
)
self.btn_statistics.pack(side="right", padx=5)
# --- 메인: 리스트 (헤더 + 스크롤 목록) ---
# 1. 헤더 (Grid 사용)
self.header_frame = ctk.CTkFrame(self, height=40)
self.header_frame.pack(fill="x", padx=20, pady=(0, 5))
# 컬럼 정의: [표시명, 가중치(0=고정폭), 고정폭크기]
# 가중치가 있으면 고정폭 무시하고 비율대로 늘어남
self.columns = [
("확인", 0, 90), # 0
("번호", 0, 90), # 1
("공개", 0, 50), # 2 [New]
("채널", 0, 60), # 3 [New]
("상태", 0, 70), # 4
("제목 (클릭시 상세)", 1, 0), # 5 (가변)
("부서", 0, 120), # 6
("작성자", 0, 70), # 7
("접수일자", 0, 100), # 8 [Rename]
("링크", 0, 50) # 9
]
# 헤더 그리기
for i, (name, weight, width) in enumerate(self.columns):
self.header_frame.columnconfigure(i, weight=weight) # 가변 여부 설정
lbl = ctk.CTkLabel(self.header_frame, text=name, font=theme_manager.get_font(13, "bold"))
if weight == 0:
lbl.configure(width=width)
lbl.grid(row=0, column=i, sticky="ew", pady=10, padx=2)
# 2. 스크롤 목록 (rows)
self.list_scroll = ctk.CTkScrollableFrame(self)
self.list_scroll.pack(fill="both", expand=True, padx=20, pady=(0, 20))
# 스크롤 내부 컬럼 설정 (헤더와 동일하게)
for i, (name, weight, width) in enumerate(self.columns):
self.list_scroll.columnconfigure(i, weight=weight)
# 목록 아이템들을 저장할 리스트 (삭제/갱신용)
self.list_items = []
# 데이터 초기 삽입
self.update_data(data_rows)
def toggle_theme(self):
new_mode = theme_manager.toggle_theme()
def open_settings(self):
"""설정 창 열기"""
self.controller.open_settings()
def open_statistics(self):
"""통계 분석 창 열기"""
self.controller.open_statistics()
def refresh_and_update(self):
"""새로고침: DB에서 최신 데이터를 다시 불러옵니다."""
# 컨트롤러에서 포맷팅된 최신 데이터 가져오기
new_data = self.controller.get_all_posts_formatted()
self.update_data(new_data)
def refresh_data_only(self):
"""현재 필터 상태를 유지하면서 데이터만 갱신"""
self.refresh_and_update()
def update_data(self, rows):
"""데이터 갱신 및 UI 다시 그리기"""
self.all_data = rows
self.apply_filter()
def apply_filter(self):
"""검색어 및 날짜 필터링 및 목록 렌더링"""
# 기존 목록 삭제
for item in self.list_items:
item.destroy()
self.list_items.clear()
query = self.search_entry.get().lower()
# 날짜 범위 가져오기
start_date, end_date = self.date_selector.get_date_range()
# 필터링
filtered = []
for r in self.all_data:
# 1. 검색어 필터
if query:
if not (query in r['title'].lower() or
query in r['department'].lower() or
query in r['writer'].lower()):
continue
# 2. 날짜 필터
try:
# 날짜 문자열 파싱 (YYYY-MM-DD 또는 YYYY-MM-DD HH:MM:SS 형식)
date_str = r.get('date', '')
if not date_str:
continue
# 날짜 부분만 추출 (시간 정보 제거)
if ' ' in date_str:
date_part = date_str.split(' ')[0]
else:
date_part = date_str
post_date = datetime.strptime(date_part, "%Y-%m-%d").date()
# 날짜 범위 체크
if not (start_date <= post_date <= end_date):
continue
except Exception as e:
# 날짜 파싱 실패 시 해당 항목 제외
print(f"날짜 파싱 오류 (ID: {r.get('id', 'Unknown')}): {e}")
continue
filtered.append(r)
# 정렬 (ID 역순)
try:
filtered.sort(key=lambda x: int(x['id']), reverse=True)
except:
filtered.sort(key=lambda x: str(x['id']), reverse=True)
# 렌더링 (최대 100개)
for row_data in filtered[:100]:
self._create_row_item(row_data)
def _create_row_item(self, m):
"""개별 행(Card) 생성"""
# 배경색 교차 등은 복잡하므로 단순 통일 혹은 hover 효과만
row_frame = ctk.CTkFrame(self.list_scroll, fg_color=("gray95", "gray25"), corner_radius=5)
row_frame.pack(fill="x", pady=2)
# Grid 설정 (헤더와 동일하게 동기화)
for i, (name, weight, width) in enumerate(self.columns):
row_frame.columnconfigure(i, weight=weight)
# 1. 확인 여부 & 시간
checked_at = m.get('checked_at')
if not checked_at:
check_text = "미확인"
check_color = "red" if theme_manager.current_theme == "Light" else "#FF5555"
font_size = 12
is_bold = "bold"
else:
# 타임스탬프 포맷팅: (2/13)13:24
try:
dt = datetime.strptime(str(checked_at).split('.')[0], "%Y-%m-%d %H:%M:%S")
time_str = dt.strftime("(%m/%d)%H:%M")
except:
time_str = str(checked_at)
check_text = f"확인\n{time_str}"
check_color = "gray"
font_size = 11
is_bold = "normal"
lbl_check = ctk.CTkLabel(row_frame, text=check_text, text_color=check_color,
font=theme_manager.get_font(font_size, is_bold))
if self.columns[0][1] == 0: lbl_check.configure(width=self.columns[0][2])
lbl_check.grid(row=0, column=0, pady=5)
# 2. 번호
lbl_id = ctk.CTkLabel(row_frame, text=str(m['id']), font=theme_manager.get_font(12))
if self.columns[1][1] == 0: lbl_id.configure(width=self.columns[1][2])
lbl_id.grid(row=0, column=1, pady=5)
# 3. 공개 (New)
is_pub = m.get('is_public')
pub_text = "공개" if is_pub == 1 else "비공개"
pub_color = None if is_pub == 1 else "gray"
# text_color가 None이면 기본색 사용
if pub_color:
lbl_pub = ctk.CTkLabel(row_frame, text=pub_text, text_color=pub_color, font=theme_manager.get_font(11))
else:
lbl_pub = ctk.CTkLabel(row_frame, text=pub_text, font=theme_manager.get_font(11))
if self.columns[2][1] == 0: lbl_pub.configure(width=self.columns[2][2])
lbl_pub.grid(row=0, column=2, pady=5)
# 4. 채널 (New)
raw_ch = m.get('channel', '')
ch_text = raw_ch if raw_ch else "-"
lbl_ch = ctk.CTkLabel(row_frame, text=ch_text, font=theme_manager.get_font(11))
if self.columns[3][1] == 0: lbl_ch.configure(width=self.columns[3][2])
lbl_ch.grid(row=0, column=3, pady=5)
# 5. 상태
status = m['status']
s_color = "green" if "완료" in status else "#FF9900" if "접수" in status else "gray"
lbl_st = ctk.CTkLabel(row_frame, text=status, text_color=s_color, font=theme_manager.get_font(12, "bold"))
if self.columns[4][1] == 0: lbl_st.configure(width=self.columns[4][2])
lbl_st.grid(row=0, column=4, pady=5)
# 6. 제목 (가변, 클릭 가능)
title_btn = ctk.CTkButton(
row_frame,
text=m['title'],
font=theme_manager.get_font(12),
anchor="w",
fg_color="transparent",
text_color=("black", "white"),
hover_color=("gray85", "gray30"),
command=lambda id=m['id']: self.controller.request_detail_popup(id)
)
title_btn.grid(row=0, column=5, sticky="ew", pady=5, padx=5)
# 7. 부서
lbl_dept = ctk.CTkLabel(row_frame, text=m['department'], font=theme_manager.get_font(11))
if self.columns[6][1] == 0: lbl_dept.configure(width=self.columns[6][2])
lbl_dept.grid(row=0, column=6, pady=5)
# 8. 작성자
mask_writer = m.get('writer') or ""
if len(mask_writer) > 1: mask_writer = mask_writer[0] + "*" + mask_writer[-1] # 간단 마스킹
lbl_w = ctk.CTkLabel(row_frame, text=mask_writer, font=theme_manager.get_font(11))
if self.columns[7][1] == 0: lbl_w.configure(width=self.columns[7][2])
lbl_w.grid(row=0, column=7, pady=5)
# 9. 접수일자
date = m.get('date', '')
lbl_date = ctk.CTkLabel(row_frame, text=date, font=theme_manager.get_font(11))
if self.columns[8][1] == 0: lbl_date.configure(width=self.columns[8][2])
lbl_date.grid(row=0, column=8, pady=5)
# 10. 링크
web_btn = ctk.CTkButton(
row_frame,
text="이동",
width=40,
height=24,
font=theme_manager.get_font(11),
fg_color="transparent",
border_width=1,
text_color=("gray20", "gray80"),
command=lambda id=m['id']: self.controller.open_in_browser(id)
)
# 링크 컬럼 너비에 맞춰 중앙 정렬 등을 위해 Frame이나 wrapper 쓸 수도 있으나, 여기선 grid center
web_btn.grid(row=0, column=9, padx=5)
self.list_items.append(row_frame)