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>
240 lines
9.0 KiB
Python
240 lines
9.0 KiB
Python
"""탭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("실패", "리셋 전송에 실패했습니다.")
|