307 lines
12 KiB
Python
307 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
|
|
from updater.__version__ import VERSION
|
|
|
|
class HistoryDialog(ctk.CTkToplevel):
|
|
def __init__(self, controller, data_rows):
|
|
super().__init__()
|
|
self.controller = controller
|
|
self.author = "KH.Choi"
|
|
self.title("VOC 수집 내역")
|
|
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.meta_label = ctk.CTkLabel(
|
|
self.toolbar,
|
|
text=f"v{VERSION} | 제작 {self.author}",
|
|
font=theme_manager.get_font(11),
|
|
text_color=("gray45", "gray65")
|
|
)
|
|
self.meta_label.pack(side="right", padx=(0, 10))
|
|
|
|
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)
|