Initial commit: Dabit Time Manager project

Python-based time management application with UDP discovery,
TCP protocol communication, time sync, and drift monitoring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
insulee
2026-02-10 11:10:55 +09:00
commit 3c14e1e401
27 changed files with 2240 additions and 0 deletions

239
gui/discovery_panel.py Normal file
View File

@@ -0,0 +1,239 @@
"""탭1: 컨트롤러 검색/IP변경 패널"""
import tkinter as tk
from tkinter import ttk, messagebox
import threading
from models.controller import Controller
from network.udp_discovery import get_network_interfaces, search_controllers, send_sett, send_reset
class DiscoveryPanel(ttk.Frame):
"""컨트롤러 검색 및 설정 변경 패널."""
def __init__(self, parent, shared_controllers: list[Controller]):
super().__init__(parent)
self._controllers = shared_controllers
self._searching = False
self._build_ui()
def _build_ui(self):
# --- 상단: NIC 선택 + 검색 ---
top = ttk.LabelFrame(self, text="컨트롤러 검색", padding=5)
top.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(top, text="네트워크:").grid(row=0, column=0, padx=(0, 5))
self._nic_var = tk.StringVar()
self._nic_combo = ttk.Combobox(top, textvariable=self._nic_var, state="readonly", width=20)
self._nic_combo.grid(row=0, column=1, padx=(0, 10))
self._btn_search = ttk.Button(top, text="검색", command=self._on_search)
self._btn_search.grid(row=0, column=2, padx=(0, 5))
self._search_status = ttk.Label(top, text="")
self._search_status.grid(row=0, column=3, padx=5)
self._refresh_nics()
# --- 중간: 컨트롤러 목록 ---
mid = ttk.LabelFrame(self, text="검색된 컨트롤러", padding=5)
mid.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
cols = ("mac", "ip", "port", "name", "subnet", "gateway")
self._tree = ttk.Treeview(mid, columns=cols, show="headings", height=8)
self._tree.heading("mac", text="MAC")
self._tree.heading("ip", text="IP")
self._tree.heading("port", text="포트")
self._tree.heading("name", text="이름")
self._tree.heading("subnet", text="서브넷")
self._tree.heading("gateway", text="게이트웨이")
self._tree.column("mac", width=140)
self._tree.column("ip", width=120)
self._tree.column("port", width=60)
self._tree.column("name", width=120)
self._tree.column("subnet", width=120)
self._tree.column("gateway", width=120)
scroll = ttk.Scrollbar(mid, orient=tk.VERTICAL, command=self._tree.yview)
self._tree.configure(yscrollcommand=scroll.set)
self._tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scroll.pack(side=tk.RIGHT, fill=tk.Y)
self._tree.bind("<<TreeviewSelect>>", self._on_select)
# --- 로그 영역 ---
log_frame = ttk.LabelFrame(self, text="검색 로그", padding=5)
log_frame.pack(fill=tk.X, padx=5, pady=(0, 5))
self._log = tk.Text(log_frame, height=5, state=tk.DISABLED, wrap=tk.WORD,
font=("Consolas", 9))
log_scroll = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self._log.yview)
self._log.configure(yscrollcommand=log_scroll.set)
self._log.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
log_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# --- 하단: 설정 변경 ---
bot = ttk.LabelFrame(self, text="설정 변경", padding=5)
bot.pack(fill=tk.X, padx=5, pady=5)
fields = [
("IP:", "ip_var", 15),
("포트:", "port_var", 6),
("이름:", "name_var", 15),
("서브넷:", "subnet_var", 15),
("게이트웨이:", "gw_var", 15),
]
self._edit_vars = {}
for i, (label, var_name, width) in enumerate(fields):
ttk.Label(bot, text=label).grid(row=i // 3, column=(i % 3) * 2, padx=2, sticky=tk.E)
var = tk.StringVar()
self._edit_vars[var_name] = var
ttk.Entry(bot, textvariable=var, width=width).grid(
row=i // 3, column=(i % 3) * 2 + 1, padx=(0, 10), pady=2
)
btn_frame = ttk.Frame(bot)
btn_frame.grid(row=2, column=0, columnspan=6, pady=5)
ttk.Button(btn_frame, text="적용", command=self._on_apply).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="리셋", command=self._on_reset).pack(side=tk.LEFT, padx=5)
def _log_message(self, msg: str):
self._log.config(state=tk.NORMAL)
self._log.insert(tk.END, msg + "\n")
self._log.see(tk.END)
self._log.config(state=tk.DISABLED)
def _refresh_nics(self):
nics = get_network_interfaces()
self._nic_list = nics
self._nic_combo["values"] = [f"{name} ({ip})" for ip, name in nics]
if nics:
self._nic_combo.current(0)
def _get_bind_ip(self) -> str:
idx = self._nic_combo.current()
if idx >= 0 and idx < len(self._nic_list):
return self._nic_list[idx][0]
return "0.0.0.0"
def _on_search(self):
if self._searching:
return
self._searching = True
self._btn_search.config(state=tk.DISABLED)
self._search_status.config(text="검색중...")
self._tree.delete(*self._tree.get_children())
self._controllers.clear()
# 로그 초기화
self._log.config(state=tk.NORMAL)
self._log.delete("1.0", tk.END)
self._log.config(state=tk.DISABLED)
bind_ip = self._get_bind_ip()
self._log_message(f"검색 시작 (NIC: {bind_ip})")
# 검색 로그를 GUI로 전달하는 큐
self._log_queue: list[str] = []
threading.Thread(target=self._search_thread, args=(bind_ip,), daemon=True).start()
self._poll_logs()
def _search_thread(self, bind_ip: str):
def on_log(msg: str):
self._log_queue.append(msg)
results = search_controllers(bind_ip=bind_ip, timeout=3.0, on_log=on_log)
self._search_results = results
self._search_finished = True
def _poll_logs(self):
"""백그라운드 스레드의 로그를 GUI에 표시."""
while self._log_queue:
msg = self._log_queue.pop(0)
self._log_message(msg)
if hasattr(self, "_search_finished") and self._search_finished:
self._search_finished = False
self._search_done(self._search_results)
return
self.after(100, self._poll_logs)
def _search_done(self, results: list[Controller]):
self._searching = False
self._btn_search.config(state=tk.NORMAL)
self._controllers.clear()
self._controllers.extend(results)
for ctrl in results:
self._tree.insert("", tk.END, values=(
ctrl.mac, ctrl.ip, ctrl.port, ctrl.name, ctrl.subnet, ctrl.gateway
))
self._search_status.config(text=f"{len(results)}개 발견")
def _on_select(self, event):
sel = self._tree.selection()
if not sel:
return
values = self._tree.item(sel[0], "values")
# mac, ip, port, name, subnet, gateway
self._edit_vars["ip_var"].set(values[1])
self._edit_vars["port_var"].set(values[2])
self._edit_vars["name_var"].set(values[3])
self._edit_vars["subnet_var"].set(values[4])
self._edit_vars["gw_var"].set(values[5])
def _get_selected_controller(self) -> Controller | None:
sel = self._tree.selection()
if not sel:
messagebox.showwarning("선택 필요", "컨트롤러를 선택하세요.")
return None
values = self._tree.item(sel[0], "values")
mac = values[0]
for c in self._controllers:
if c.mac == mac:
return c
return None
def _on_apply(self):
ctrl = self._get_selected_controller()
if not ctrl:
return
ctrl.ip = self._edit_vars["ip_var"].get().strip()
try:
ctrl.port = int(self._edit_vars["port_var"].get().strip())
except ValueError:
messagebox.showerror("오류", "포트는 숫자여야 합니다.")
return
ctrl.name = self._edit_vars["name_var"].get().strip()
ctrl.subnet = self._edit_vars["subnet_var"].get().strip()
ctrl.gateway = self._edit_vars["gw_var"].get().strip()
bind_ip = self._get_bind_ip()
ok = send_sett(ctrl, bind_ip)
if ok:
messagebox.showinfo("성공", "설정이 전송되었습니다.\n적용을 위해 리셋하세요.")
sel = self._tree.selection()
if sel:
self._tree.item(sel[0], values=(
ctrl.mac, ctrl.ip, ctrl.port, ctrl.name, ctrl.subnet, ctrl.gateway
))
else:
messagebox.showerror("실패", "설정 전송에 실패했습니다.")
def _on_reset(self):
ctrl = self._get_selected_controller()
if not ctrl:
return
if not messagebox.askyesno("확인", f"{ctrl.mac} 컨트롤러를 리셋하시겠습니까?"):
return
bind_ip = self._get_bind_ip()
ok = send_reset(ctrl.mac, bind_ip)
if ok:
messagebox.showinfo("성공", "리셋 명령이 전송되었습니다.")
else:
messagebox.showerror("실패", "리셋 전송에 실패했습니다.")