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("", 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)