feat(ui): #4 핵심 — 인라인 로그 제거 + InlinePanel 12 popup 인라인화
사용자 명시 요청 (#4 잔여 핵심): "기존 구조에 지도 아래에 있는 로그는 백엔드로 빼고, 프로세스를 클릭할 때마다 새로운 창이 뜨는 것이 아니라 한 화면에서 바로 구동되게끔 적용". 1. 인라인 로그 패널 완전 제거: - self.textbox CTkTextbox 위젯 제거 (이전 라운드 80px 축소 → 본 라운드 완전 삭제). - main_frame layout: row 0 weight 3→1 (지도 전체 차지), row 1 (로그) 제거, status_bar row 2→1. - self.log() 동작 변경: textbox.insert 대신 백엔드 logger.info (logs/scanvas.log RotatingFileHandler 5MB×5). status_text 가 짧은 미리보기 (≤80자) 즉시 표시. - 효과: 메인 캔버스 영역 ~25% 확대 + GUI 메인 thread 부담 감소. 2. harness/inline_panel.py 신규 (231 LOC): - InlinePanel: ctk.CTkToplevel 호환 인라인 오버레이 (CTkFrame 상속). - API 호환: title/geometry/transient/grab_set/protocol/wait_window/destroy + iconbitmap/wm_* no-op. - 핵심 트릭: tk.Misc.wait_window(self) 가 Frame 에서도 동작 (widget destruction 대기) — wait_window 호출 5곳 (T1/T6/T7/T8/T10) 그대로 유지 가능. - 다중 패널 z-order (_z_counter + lift), main_frame 95% cap, MC Red 타이틀 바. 3. 12 ctk.CTkToplevel → InlinePanel 일괄 치환: - T1 (DXF 레이어), T2 (구조물 빌드), T3 (빌드 진행), T6 (상세도면), T7 (치수), T8 (계획선 고도), T9 (TIN core), T10 (렌더 옵션), T11 (Blender 결과), T12 (AI 렌더 결과) — 10 main popups. - T4 (렌더 sub-옵션), T5 (VLM 결과) — T3 자식 popups. - 2 replace_all 패턴: ctk.CTkToplevel(self) → InlinePanel(self), ctk.CTkToplevel(win) → InlinePanel(win). - 결과: 12 popups 모두 main 창 안 floating frame 으로 렌더, 별도 OS 창 안 뜸. 사용자가 ALT-TAB 으로 창 사이 오갈 필요 없음. 검증: - py_compile + AST OK (scanvas_maker, perf, crash_logger, inline_panel 4개). - ruff check All checks passed (0 errors). - import smoke test: scanvas_maker import 성공, InlinePanel 가 진짜 harness 클래스로 로드. - self.textbox 잔존 refs: 0. CTkToplevel refs: 3 (모두 import fallback/주석). InlinePanel refs: 15 (12 호출지 + import). 잔여 (#4 next round, multi-session): - InlinePanel 실 GUI 워크플로 검증 (사용자 도면 로드 후 T1~T12 한 번씩). - VTK 임베딩 (pv.Plotter().show() 6곳 → pyvistaqt.QtInteractor). - messagebox 63회 → 인라인 토스트. - Inspector 패널 영구 컬럼 (3-column 레이아웃). - 메인 thread 블로킹 작업 worker thread 분리. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,20 @@ except ImportError:
|
||||
def set_perf_log(fn): # type: ignore[no-redef]
|
||||
pass
|
||||
|
||||
# InlinePanel (#4) — CTkToplevel 호환 인라인 오버레이. 별도 OS 창 안 띄우고
|
||||
# main_frame 안 floating frame 으로 렌더. 실패 시 폴백으로 CTkToplevel 사용.
|
||||
try:
|
||||
from harness.inline_panel import InlinePanel
|
||||
except ImportError:
|
||||
InlinePanel = ctk.CTkToplevel # type: ignore[assignment,misc]
|
||||
|
||||
# 크래시 로거의 일반 로그 채널 — self.log() 가 인라인 textbox 대신 백엔드 파일에
|
||||
# 흘리도록 하기 위해 (#4 "로그는 백엔드로"). harness.logger 의 get_logger 와 별개.
|
||||
try:
|
||||
from harness.crash_logger import get_logger as _get_crash_logger
|
||||
except ImportError:
|
||||
_get_crash_logger = None # type: ignore[assignment]
|
||||
|
||||
# 구조물 상세도면 치수 파서
|
||||
try:
|
||||
from detail_parser import DetailParser, dimensions_to_structure_params
|
||||
@@ -600,8 +614,8 @@ class SCanvasApp(ctk.CTk):
|
||||
self.main_frame = ctk.CTkFrame(self, corner_radius=15, fg_color="transparent")
|
||||
self.main_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
|
||||
self.main_frame.grid_columnconfigure(0, weight=1)
|
||||
self.main_frame.grid_rowconfigure(0, weight=3) # 지도 (넓게)
|
||||
self.main_frame.grid_rowconfigure(1, weight=1) # 로그 (좁게)
|
||||
# 피드백 #4: 인라인 로그 패널 제거. row 0 (지도/캔버스) 전체, row 1 (status_bar) 만 남음.
|
||||
self.main_frame.grid_rowconfigure(0, weight=1) # 지도/캔버스 (전체)
|
||||
|
||||
# 1. 지도 (상단 — 넓게). Light/Dark 테마별 배경 쌍 — tkintermapview
|
||||
# 타일 주변의 얇은 padding 에 이 색이 보임.
|
||||
@@ -620,15 +634,13 @@ class SCanvasApp(ctk.CTk):
|
||||
self.map_view.set_zoom(6)
|
||||
self.map_view.set_position(36.5, 127.5)
|
||||
|
||||
# 2. 로그 (하단 — 스크롤 가능. 피드백 #4: 인라인 로그 비중 축소, 백엔드 파일이
|
||||
# 주 기록처. height 120 → 80 으로 캔버스 영역 확보.
|
||||
# 파일 로그: %LOCALAPPDATA%\\S-CANVAS\\scanvas_harness.log + logs/scanvas.log)
|
||||
self.textbox = ctk.CTkTextbox(self.main_frame, height=80, font=ctk.CTkFont(family="Consolas", size=12), border_width=1)
|
||||
self.textbox.grid(row=1, column=0, padx=0, pady=0, sticky="nsew")
|
||||
# 2. 로그 패널 — 피드백 #4: **제거** (인라인 → 백엔드 파일).
|
||||
# 파일 위치: %LOCALAPPDATA%\\S-CANVAS\\scanvas_harness.log + logs/scanvas.log.
|
||||
# self.log() 호출은 그대로 유지하되 logger 로 흘러가고 status_text 만 갱신.
|
||||
|
||||
# 3. 하단 상태 바
|
||||
# 3. 하단 상태 바 — 로그 제거로 row 2 → row 1
|
||||
self.status_bar = ctk.CTkFrame(self.main_frame, height=28, fg_color="transparent")
|
||||
self.status_bar.grid(row=2, column=0, sticky="ew", pady=(5, 0))
|
||||
self.status_bar.grid(row=1, column=0, sticky="ew", pady=(5, 0))
|
||||
|
||||
self.status_indicator = ctk.CTkLabel(self.status_bar, text="● READY", text_color="#22A06B", font=ctk.CTkFont(size=12, weight="bold"))
|
||||
self.status_indicator.pack(side="left", padx=10)
|
||||
@@ -747,11 +759,20 @@ class SCanvasApp(ctk.CTk):
|
||||
print(f"[Warning] 윈도우 아이콘 설정 실패: {e}")
|
||||
|
||||
def log(self, message):
|
||||
timestamp = datetime.datetime.now().strftime("[%H:%M:%S]")
|
||||
def _update():
|
||||
self.textbox.insert("end", f"{timestamp} {message}\n")
|
||||
self.textbox.see("end")
|
||||
self.after(0, _update)
|
||||
"""피드백 #4: 인라인 로그 패널 제거 후, 모든 메시지는 백엔드 파일로.
|
||||
|
||||
- 백엔드: %LOCALAPPDATA%\\S-CANVAS\\scanvas_harness.log + logs/scanvas.log
|
||||
(RotatingFileHandler 5MB×5).
|
||||
- 사용자 즉시 피드백: status_bar 의 status_text 가 짧은 미리보기로 갱신
|
||||
(긴 메시지는 80자에서 잘림 + …).
|
||||
- 매 호출이 textbox.insert 으로 GUI 메인 thread 부담 주던 패턴 제거됨.
|
||||
"""
|
||||
if _get_crash_logger is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
_get_crash_logger().info(message)
|
||||
if hasattr(self, "status_text"):
|
||||
short = message if len(message) <= 80 else message[:77] + "…"
|
||||
self.after(0, lambda s=short: self.status_text.configure(text=s))
|
||||
|
||||
def start_progress(self, label: str | None = None) -> None:
|
||||
"""진행률 인디케이터 표시 + indeterminate animation 시작.
|
||||
@@ -848,7 +869,7 @@ class SCanvasApp(ctk.CTk):
|
||||
option_list = list(type_options.values())
|
||||
|
||||
# 팝업 창
|
||||
win = ctk.CTkToplevel(self)
|
||||
win = InlinePanel(self)
|
||||
win.title("S-CANVAS: DXF 레이어 분류")
|
||||
win.geometry("900x650")
|
||||
win.grab_set()
|
||||
@@ -1501,7 +1522,7 @@ class SCanvasApp(ctk.CTk):
|
||||
"완료하여 구조물 레이어를 등록해 주세요.")
|
||||
return
|
||||
|
||||
win = ctk.CTkToplevel(self)
|
||||
win = InlinePanel(self)
|
||||
win.title("S-CANVAS: 구조물 상세 3D 빌드 (템플릿 기반)")
|
||||
win.geometry("1100x650")
|
||||
win.grab_set()
|
||||
@@ -1678,7 +1699,7 @@ class SCanvasApp(ctk.CTk):
|
||||
messagebox.showerror("오류", f"템플릿을 찾을 수 없습니다: {tid}")
|
||||
return
|
||||
|
||||
win = ctk.CTkToplevel(self)
|
||||
win = InlinePanel(self)
|
||||
win.title(f"구조물 검토: {name}")
|
||||
win.geometry("760x820")
|
||||
win.grab_set()
|
||||
@@ -1971,7 +1992,7 @@ class SCanvasApp(ctk.CTk):
|
||||
return
|
||||
|
||||
# 옵션 다이얼로그 — 시간대 + 투명배경 + 샘플
|
||||
opt_win = ctk.CTkToplevel(win)
|
||||
opt_win = InlinePanel(win)
|
||||
opt_win.title("Blender Cycles 렌더 옵션")
|
||||
opt_win.geometry("420x340")
|
||||
opt_win.transient(win); opt_win.grab_set()
|
||||
@@ -2126,7 +2147,7 @@ class SCanvasApp(ctk.CTk):
|
||||
|
||||
def _show_diff_dialog(diff: dict):
|
||||
"""Gemini가 반환한 diff를 테이블로 보여주고 사용자가 체크한 항목만 적용."""
|
||||
dwin = ctk.CTkToplevel(win)
|
||||
dwin = InlinePanel(win)
|
||||
dwin.title(f"AI 검증 결과: {name}")
|
||||
dwin.geometry("820x680")
|
||||
dwin.grab_set()
|
||||
@@ -2448,7 +2469,7 @@ class SCanvasApp(ctk.CTk):
|
||||
messagebox.showerror("오류", "detail_parser 모듈을 찾을 수 없습니다.")
|
||||
return
|
||||
|
||||
win = ctk.CTkToplevel(self)
|
||||
win = InlinePanel(self)
|
||||
win.title("S-CANVAS: 구조물 상세도면 추가")
|
||||
win.geometry("950x600")
|
||||
win.grab_set()
|
||||
@@ -2568,7 +2589,7 @@ class SCanvasApp(ctk.CTk):
|
||||
"""
|
||||
info = self.structure_registry[layer_name]
|
||||
|
||||
win = ctk.CTkToplevel(self)
|
||||
win = InlinePanel(self)
|
||||
win.title(f"치수 확인: {info['name']}")
|
||||
win.geometry("650x500")
|
||||
win.grab_set()
|
||||
@@ -2805,7 +2826,7 @@ class SCanvasApp(ctk.CTk):
|
||||
except Exception as e:
|
||||
self.log(f" 마커 표시 오류: {e}")
|
||||
|
||||
win = ctk.CTkToplevel(self)
|
||||
win = InlinePanel(self)
|
||||
win.title("S-CANVAS: 계획선 고도 설정")
|
||||
win.geometry("1280x560")
|
||||
win.grab_set()
|
||||
@@ -4707,7 +4728,7 @@ class SCanvasApp(ctk.CTk):
|
||||
pts_abs = pts_zero + origin
|
||||
x0p, y0p, x1p, y1p = [float(v) for v in self.projected_bounds]
|
||||
|
||||
win = ctk.CTkToplevel(self)
|
||||
win = InlinePanel(self)
|
||||
win.title("🎯 TIN 이용 범위 선택")
|
||||
win.geometry("1100x920")
|
||||
win.minsize(900, 640)
|
||||
@@ -6622,7 +6643,7 @@ class SCanvasApp(ctk.CTk):
|
||||
engine = self.render_engine.get()
|
||||
|
||||
# 시간대 + 출력 화질 선택 (Step 4 실행 시)
|
||||
time_win = ctk.CTkToplevel(self)
|
||||
time_win = InlinePanel(self)
|
||||
time_win.title("렌더링 옵션")
|
||||
time_win.geometry("380x360")
|
||||
time_win.grab_set()
|
||||
@@ -6982,7 +7003,7 @@ class SCanvasApp(ctk.CTk):
|
||||
bg.convert("RGBA"), pil_img
|
||||
).convert("RGB")
|
||||
|
||||
win = ctk.CTkToplevel(self)
|
||||
win = InlinePanel(self)
|
||||
win.title(f"🎨 Blender 렌더 결과 - {Path(image_path).name}")
|
||||
|
||||
sw = self.winfo_screenwidth(); sh = self.winfo_screenheight()
|
||||
@@ -7055,7 +7076,7 @@ class SCanvasApp(ctk.CTk):
|
||||
try:
|
||||
from PIL import ImageTk
|
||||
|
||||
win = ctk.CTkToplevel(self)
|
||||
win = InlinePanel(self)
|
||||
win.title("S-CANVAS: AI 렌더링 결과")
|
||||
win.geometry("820x860")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user