diff --git a/CHANGELOG.md b/CHANGELOG.md index c365d47..6138579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,53 @@ --- +## 2026-05-08 (후속 — UI 1차) + +### [feat] Mastercard 팔레트 1차 적용 + 인트로 비디오 제거 (#4 부분) + +> **사용자 피드백 #4 부분 진행**: 색감/텍스쳐 위주의 1차 라운드. 단일 창 구조 (CTkToplevel 12개 통합, VTK 임베딩 등) 는 multi-session 작업이라 별도 라운드. 본 commit 은 색채 정체성 + 인트로 제거에 한정. + +#### Mastercard 디자인 토큰 매핑 (10색) +- `#2ECC71` (Bootstrap green) → `#22A06B` Brand Green (READY 인디케이터, 12곳). +- `#E74C3C` (Bootstrap red) → `#EB001B` Mastercard Red (에러 status, 14곳). +- `#F1C40F` (Bootstrap yellow) → `#F79E1B` Mastercard Yellow (경고 status, 7곳). +- `#27AE60` / `#1E8449` (CTA 그린/hover) → `#22A06B` / `#1B8454` (5+3곳). +- `#E67E22` / `#BA6116` (오렌지 CTA/hover) → `#EB001B` / `#A30013` MC Red (3+1곳) — 주요 액션 버튼이 MC 레드로 통일. +- `#343A40` / `#212529` (다크 슬레이트 버튼/hover) → `#1A1A1A` / `#000000` MC Near-black (1+1곳). +- `#2b2b2b` (CTk 기본 다크 캔버스) → `#1A1A1A` (1곳, 다크 모드 일관성). + +총 50+ hex literal 갱신. Mastercard 공식 brand 가이드라인 (공개) 기반 — `npx getdesign@latest add mastercard` 시도는 외부 npm 코드 실행 차단으로 건너뛰고 공개 팔레트 적용. + +#### 디자인 의도 +- **PRIMARY (MC Red `#EB001B`)**: 주 CTA + 에러 상태 일관 적용. 위험 신호와 액션 강조 단일 색. +- **ACCENT (MC Yellow `#F79E1B`)**: 경고/노란 status 일관 — Step3 진행 인디케이터 등. +- **SUCCESS (Brand Green `#22A06B`)**: READY/완료 인디케이터. MC 자체 그린 토큰 없음 → brand-friendly 톤 선정. +- **DARK (`#1A1A1A`)**: 다크 모드 페이지/카드 bg, 다크 버튼 — pure black 직전. 텍스트 가독성 ↑. +- 팔레트 문서화 블록: `scanvas_maker.py` line ~33-47 (`Mastercard 디자인 시스템 팔레트` 주석). + +#### 인트로 영상 제거 +- `splash.py` 삭제 (178 LOC) — `show_intro_splash` 함수 단일 진입점, 더 이상 호출 없음. +- `Design/logo_intro.mp4` 삭제 (3.7 MB). +- `scanvas_maker.py` 의 호출부 제거 (line ~7044-7054 13줄): try/except + show_intro_splash + import. +- 효과: 메인 앱 즉시 기동, 첫 화면까지 12초 fade-in 사라짐 → "느리게 느껴짐" 피드백 일부 완화. + +#### 잔여 (#4 다음 라운드) +- **단일 창 구조**: CTkToplevel 12개 (T1~T12) 인스펙터 패널로 통합. +- **인라인 로그 패널 제거**: `main_frame.row=1` `CTkTextbox` → harness_log_path() 백엔드 파일. +- **VTK 임베딩**: `pv.Plotter().show()` 6개 호출지 → `vtkmodules.tk.vtkTkRenderWidget` 또는 `pyvistaqt.QtInteractor`. +- **3-column 레이아웃**: Sidebar(240) / Main Canvas(flex) / Inspector(340). +- **messagebox 63회** → 인라인 토스트/배너 (위험한 askyesno 4건만 모달 유지). + +이는 multi-session 작업이라 본 라운드 범위 외. UI_REDESIGN_PLAN.md §3 참조. + +#### 검증 +- `python -m py_compile scanvas_maker.py harness/perf.py harness/crash_logger.py` 통과. +- AST parse OK. +- `splash`/`show_intro_splash` 호출 잔존 검사 — 0건 (주석 블록의 1회 'splash' 언급 제외). +- `git ls-files | grep -E '...gcp.*key|\\.log$|\\.db$|\\.bak|venv|__pycache__|cache/'` — 0건. .gitignore 정상 동작. + +--- + ## 2026-05-08 ### [merge] Gitea s-canvas 원격(raw upload, 184185c)과 로컬 lint+Phase 0 history 통합 diff --git a/Design/logo_intro.mp4 b/Design/logo_intro.mp4 deleted file mode 100644 index 5ce1a80..0000000 Binary files a/Design/logo_intro.mp4 and /dev/null differ diff --git a/scanvas_maker.py b/scanvas_maker.py index 10e72a7..1b69fd9 100644 --- a/scanvas_maker.py +++ b/scanvas_maker.py @@ -30,6 +30,22 @@ from PIL import Image, ImageDraw, ImageFilter import tkintermapview from matplotlib.colors import LinearSegmentedColormap +# ===== Mastercard 디자인 시스템 팔레트 (피드백 #4 — UI/UX 재설계) ===== +# 본 색상은 코드 전반에 hex literal 로 직접 적용된다 (constants import 부담 회피). +# 본 주석 블록은 디자인 의도 문서화용. UI_REDESIGN_PLAN.md 참조. +# +# PRIMARY #EB001B Mastercard Red — 주 CTA, 에러 상태 +# PRIMARY' #A30013 Red Dark — CTA hover/active +# ACCENT #F79E1B Mastercard Yellow — 보조 강조, 경고 (Step3 노란 status 등) +# FOCUS #FF5F00 Overlap Orange — 포커스 / focus accent (예약) +# SUCCESS #22A06B Brand-friendly Green — 성공 / READY 인디케이터 +# SUCCESS' #1B8454 Green Dark — 성공 hover +# DARK #1A1A1A Near-black — 다크 모드 페이지/버튼 bg +# BLACK #000000 Pure Black — 텍스트, 다크 버튼 hover +# BORDER_L #E0E0E0 Light Border — 라이트 모드 보더 +# BORDER_D #333333 Dark Border — 다크 모드 보더 (현 코드는 #3F3F3F 유지) +# 인트로 splash 비디오는 본 라운드에서 제거됨 (피드백 #4). + # 지형(TIN) 컬러맵 — **파란색 금지** (피드백 #3: 물과 헷갈림). # 어두운 토양 → 밝은 모래/건조 톤 → 능선 광택. matplotlib "terrain" 대체. _TIN_EARTH_CMAP = LinearSegmentedColormap.from_list( @@ -470,14 +486,14 @@ class SCanvasApp(ctk.CTk): self.btn_step4 = ctk.CTkButton( self.sidebar_frame, text="4. AI 렌더링", command=self.btn_ai_render_callback, height=40, - fg_color="#E67E22", hover_color="#BA6116", + fg_color="#EB001B", hover_color="#A30013", font=ctk.CTkFont(weight="bold")) self.btn_step4.grid(row=_next_row(), column=0, pady=(6, 6), sticky="ew", **pad) self.btn_struct_build = ctk.CTkButton( self.sidebar_frame, text="구조물 상세 3D 빌드", command=self._open_structure_template_dialog, height=32, - fg_color="#27AE60", hover_color="#1E8449", text_color="white", + fg_color="#22A06B", hover_color="#1B8454", text_color="white", font=ctk.CTkFont(size=12, weight="bold")) self.btn_struct_build.grid(row=_next_row(), column=0, pady=(3, 0), sticky="ew", **pad) @@ -494,8 +510,8 @@ class SCanvasApp(ctk.CTk): self.btn_reopen_3d = ctk.CTkButton( self.sidebar_frame, text="🗔 3D 뷰 다시 열기", command=self._reopen_3d_preview, height=30, - fg_color=("#343A40", "#2C3E50"), - hover_color=("#212529", "#34495E"), + fg_color=("#1A1A1A", "#2C3E50"), + hover_color=("#000000", "#34495E"), text_color="#FFFFFF", font=ctk.CTkFont(size=11, weight="bold")) self.btn_reopen_3d.grid(row=_next_row(), column=0, pady=(6, 0), sticky="ew", **pad) @@ -591,7 +607,7 @@ class SCanvasApp(ctk.CTk): # 타일 주변의 얇은 padding 에 이 색이 보임. self.map_frame = ctk.CTkFrame( self.main_frame, corner_radius=12, - fg_color=("#FFFFFF", "#2b2b2b"), + fg_color=("#FFFFFF", "#1A1A1A"), border_width=1, border_color=("#DEE2E6", "#3F3F3F"), ) self.map_frame.grid(row=0, column=0, padx=0, pady=(0, 8), sticky="nsew") @@ -612,7 +628,7 @@ class SCanvasApp(ctk.CTk): 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_indicator = ctk.CTkLabel(self.status_bar, text="● READY", text_color="#2ECC71", font=ctk.CTkFont(size=12, weight="bold")) + 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) self.status_text = ctk.CTkLabel(self.status_bar, text="지형 데이터를 로드해 주세요.", font=ctk.CTkFont(size=12)) @@ -739,7 +755,7 @@ class SCanvasApp(ctk.CTk): except Exception: pass - def set_status(self, text, indicator_color="#2ECC71"): + def set_status(self, text, indicator_color="#22A06B"): def _update(): self.status_text.configure(text=text) self.status_indicator.configure(text_color=indicator_color) @@ -767,7 +783,7 @@ class SCanvasApp(ctk.CTk): "cofferdam_downstream": {"name_ko": "하류 가물막이", "render_mode": "wall_extrude", "color": "#8D9CA6"}, "building": {"name_ko": "건축물/가설건물", "render_mode": "box_extrude", "color": "#BDC3C7"}, "temp_facility": {"name_ko": "가설부지/야적장", "render_mode": "surface_overlay", "color": "#E8DACC"}, - "boundary": {"name_ko": "경계선 (참고용)", "render_mode": "line_only", "color": "#E74C3C"}, + "boundary": {"name_ko": "경계선 (참고용)", "render_mode": "line_only", "color": "#EB001B"}, "ignore": {"name_ko": "무시 (사용 안 함)", "render_mode": "none", "color": "#CCCCCC"}, } except Exception: @@ -854,7 +870,7 @@ class SCanvasApp(ctk.CTk): z_marker = "★ " if has_z else " " name_text = f"{z_marker}{layer_name}" - name_color = "#2ECC71" if guessed == "terrain" else None + name_color = "#22A06B" if guessed == "terrain" else None lbl = ctk.CTkLabel(scroll_frame, text=name_text, font=ctk.CTkFont(size=12), anchor="w", width=320, text_color=name_color) lbl.grid(row=i, column=0, padx=(5, 3), pady=2, sticky="w") @@ -1519,7 +1535,7 @@ class SCanvasApp(ctk.CTk): status_var.set(f"빌드됨 ({len(info['template_meshes'])}개)") status_label = ctk.CTkLabel(list_frame, textvariable=status_var, font=ctk.CTkFont(size=10), - text_color="#2ECC71") + text_color="#22A06B") status_label.grid(row=ri, column=4, padx=3, pady=3, sticky="w") # 작업 버튼 @@ -1599,7 +1615,7 @@ class SCanvasApp(ctk.CTk): self.show_3d_preview(textured=False) ctk.CTkButton(bottom, text="TIN에 구조물 반영 + 3D 보기", width=200, - fg_color="#E67E22", hover_color="#D35400", + fg_color="#EB001B", hover_color="#D35400", text_color="white", font=ctk.CTkFont(size=11, weight="bold"), command=_apply_structures_to_tin @@ -2081,8 +2097,8 @@ class SCanvasApp(ctk.CTk): score_f = float(score) except (TypeError, ValueError): score_f = 0.0 - score_color = ("#27AE60" if score_f >= 0.85 else - "#F39C12" if score_f >= 0.6 else "#E74C3C") + score_color = ("#22A06B" if score_f >= 0.85 else + "#F39C12" if score_f >= 0.6 else "#EB001B") hdr = ctk.CTkFrame(dwin, fg_color="transparent") hdr.pack(fill="x", padx=15, pady=(12, 4)) @@ -2144,10 +2160,10 @@ class SCanvasApp(ctk.CTk): if excess: ctk.CTkLabel(scroll, text="참고: 모델에만 있는 요소 (적용 아님)", font=ctk.CTkFont(size=12, weight="bold"), - text_color="#E74C3C", anchor="w").pack(fill="x", pady=(10, 2)) + text_color="#EB001B", anchor="w").pack(fill="x", pady=(10, 2)) for note in excess: ctk.CTkLabel(scroll, text=f"• {note}", font=ctk.CTkFont(size=10), - text_color="#E67E22", wraplength=760, justify="left", + text_color="#EB001B", wraplength=760, justify="left", anchor="w").pack(fill="x", padx=8, pady=1) valves_incorrect = diff.get("valves_incorrect", []) or [] @@ -2204,7 +2220,7 @@ class SCanvasApp(ctk.CTk): fg_color="transparent", border_width=1, command=dwin.destroy).pack(side="right", padx=4) ctk.CTkButton(btns, text="✓ 선택 항목 적용 + 재빌드", width=220, - fg_color="#27AE60", hover_color="#1E8449", + fg_color="#22A06B", hover_color="#1B8454", font=ctk.CTkFont(size=11, weight="bold"), command=_apply_and_rebuild).pack(side="right", padx=4) # 생성된 비교 이미지 경로 안내 @@ -2306,7 +2322,7 @@ class SCanvasApp(ctk.CTk): fg_color="transparent", border_width=1, command=win.destroy).pack(side="right", padx=3) ctk.CTkButton(bottom, text="✓ 확정 (레지스트리 저장)", width=190, - fg_color="#27AE60", hover_color="#1E8449", + fg_color="#22A06B", hover_color="#1B8454", text_color="white", font=ctk.CTkFont(size=11, weight="bold"), command=_do_confirm).pack(side="right", padx=3) @@ -2436,7 +2452,7 @@ class SCanvasApp(ctk.CTk): # 파싱 결과 표시 result_var = ctk.StringVar(value=self._format_detail_params(info.get("detail_params"))) result_label = ctk.CTkLabel(list_frame, textvariable=result_var, - font=ctk.CTkFont(size=10), text_color="#2ECC71") + font=ctk.CTkFont(size=10), text_color="#22A06B") result_label.grid(row=ri, column=4, padx=5, pady=3, sticky="w") # 파일 선택 버튼 @@ -2543,7 +2559,7 @@ class SCanvasApp(ctk.CTk): ).grid(row=di, column=1, padx=5, pady=1, sticky="w") ctk.CTkLabel(detail_frame, text=f"{dim.value:.3f} {dim.unit}", font=ctk.CTkFont(size=10) ).grid(row=di, column=2, padx=5, pady=1, sticky="w") - conf_color = "#2ECC71" if dim.confidence >= 0.9 else "#F1C40F" if dim.confidence >= 0.8 else "#E74C3C" + conf_color = "#22A06B" if dim.confidence >= 0.9 else "#F79E1B" if dim.confidence >= 0.8 else "#EB001B" ctk.CTkLabel(detail_frame, text=f"{dim.confidence:.0%}", font=ctk.CTkFont(size=10), text_color=conf_color ).grid(row=di, column=3, padx=5, pady=1, sticky="w") @@ -4108,7 +4124,7 @@ class SCanvasApp(ctk.CTk): self.dxf_path = file_path self.log(f">>> [Step 1] DXF 로드: {os.path.basename(file_path)}") - self.set_status("DXF 분석 중...", "#F1C40F") + self.set_status("DXF 분석 중...", "#F79E1B") # 진단 로그 초기화 (세션 시작) self._diag(f"=== Step 1 시작: {file_path} ===", reset=True) @@ -4187,13 +4203,13 @@ class SCanvasApp(ctk.CTk): self.update_map_view_to_mesh() reg_n = len(self.structure_registry) - self.set_status(f"TIN 생성 완료 · 구조물 등록 {reg_n}개", "#2ECC71") + self.set_status(f"TIN 생성 완료 · 구조물 등록 {reg_n}개", "#22A06B") self.btn_step2.configure(fg_color=["#3a7ebf", "#1f538d"], border_width=0) self.show_3d_preview(textured=False) except Exception as e: self.log(f"오류: {e!s}") - self.set_status("오류 발생", "#E74C3C") + self.set_status("오류 발생", "#EB001B") messagebox.showerror("오류", f"TIN 생성 중 문제가 발생했습니다:\n{e}") def create_tin_from_dxf(self, filepath, terrain_layers=None): @@ -4747,7 +4763,7 @@ class SCanvasApp(ctk.CTk): bx0, by0, bx1, by1 = state["bbox"] state["core_rect"] = _MplRect( (bx0, by0), bx1 - bx0, by1 - by0, - fill=False, edgecolor="#E74C3C", linewidth=2.2, label="정밀 TIN core") + fill=False, edgecolor="#EB001B", linewidth=2.2, label="정밀 TIN core") ax.add_patch(state["core_rect"]) # 통계 갱신 in_core = ((pts_abs[:, 0] >= bx0) & (pts_abs[:, 0] <= bx1) @@ -4805,8 +4821,8 @@ class SCanvasApp(ctk.CTk): _clear_live() drag["live_rect"] = _MplRect( (bx0, by0), bx1 - bx0, by1 - by0, - fill=True, facecolor="#E74C3C", alpha=0.18, - edgecolor="#E74C3C", linewidth=2.0) + fill=True, facecolor="#EB001B", alpha=0.18, + edgecolor="#EB001B", linewidth=2.0) ax.add_patch(drag["live_rect"]) stat_lbl.configure(text=( f"드래그 중 {bx1-bx0:.0f}×{by1-by0:.0f} m " @@ -4918,7 +4934,7 @@ class SCanvasApp(ctk.CTk): text="✅ 선택 결과 제출 (이 범위를 정밀 TIN core 로 확정)", command=_on_confirm, height=56, - fg_color="#2ECC71", hover_color="#27AE60", + fg_color="#22A06B", hover_color="#22A06B", text_color="white", font=ctk.CTkFont(size=16, weight="bold")) submit_btn.pack(side="top", fill="x", padx=10, pady=10) @@ -5036,7 +5052,7 @@ class SCanvasApp(ctk.CTk): feather_m = max(150.0, dem_buffer_m * 0.2) src_crs = self.crs_option.get() - self.set_status("DEM으로 TIN 확장 중...", "#F1C40F") + self.set_status("DEM으로 TIN 확장 중...", "#F79E1B") self.log(f">>> [Step 1.5] DEM으로 TIN 확장 (buffer={dem_buffer_m:.0f}m, feather={feather_m:.0f}m)...") # [1.5-CORE] "TIN 이용 범위" 가 설정되어 있으면 **3-zone 블렌드** 선행. @@ -5102,7 +5118,7 @@ class SCanvasApp(ctk.CTk): self.log(f" [DEM 확장] {result.n_points}개 정점, {result.n_faces}개 삼각형") except Exception as e: self.log(f" [Step 1.5] DEM 확장 실패: {e}") - self.set_status("DEM 확장 실패", "#E74C3C") + self.set_status("DEM 확장 실패", "#EB001B") messagebox.showerror("오류", f"DEM 확장 실패:\n{e}") return @@ -5113,7 +5129,7 @@ class SCanvasApp(ctk.CTk): # UV 매핑/텍스처 초기화 — 다음 Step 2에서 재생성 self.total_mesh = None - self.set_status("Step 1.5 완료 — 위성지도 결합 준비", "#2ECC71") + self.set_status("Step 1.5 완료 — 위성지도 결합 준비", "#22A06B") self.btn_step2.configure(fg_color=["#3a7ebf", "#1f538d"], border_width=0) self.show_3d_preview(textured=False) @@ -5349,7 +5365,7 @@ class SCanvasApp(ctk.CTk): messagebox.showwarning("주의", "먼저 TIN을 생성해야 합니다.") return - self.set_status("위성 이미지 다운로드 중...", "#F1C40F") + self.set_status("위성 이미지 다운로드 중...", "#F79E1B") source_name = self.tile_source_option.get() self.log(f">>> [Step 2] 위성 타일 다운로드 ({source_name})...") @@ -5457,14 +5473,14 @@ class SCanvasApp(ctk.CTk): self._uv_mapping_params = None self.log("텍스처 UV 매핑 완료.") - self.set_status("위성지도 결합 완료", "#2ECC71") + self.set_status("위성지도 결합 완료", "#22A06B") self.btn_step3.configure(fg_color=["#3a7ebf", "#1f538d"], border_width=0) self.show_3d_preview(textured=True, texture_obj=texture) except Exception as e: self.log(f"결합 실패: {e}") - self.set_status("결합 실패", "#E74C3C") + self.set_status("결합 실패", "#EB001B") messagebox.showerror("오류", f"위성지도 결합 중 오류 발생:\n{e}") def _download_xyz_tiles(self, url_template, min_lat, min_lon, max_lat, max_lon, zoom=17): @@ -5855,7 +5871,7 @@ class SCanvasApp(ctk.CTk): self.log(" ◆ 좌클릭+드래그: 회전 | 휠: 줌 | 우클릭+드래그: 이동") self.log(" ◆ 하단 화면비 버튼 클릭 → 창 크기/캡처 비율 즉시 잠금") self.log(" ◆ 원하는 뷰가 잡히면 Enter 키(또는 q)를 누르거나 창을 닫으세요") - self.set_status("뷰포인트를 선택하세요 (Enter로 확정)", "#F1C40F") + self.set_status("뷰포인트를 선택하세요 (Enter로 확정)", "#F79E1B") try: # 인터랙티브 3D 뷰어 열기 — 사용자가 자유롭게 회전/줌 @@ -5863,7 +5879,7 @@ class SCanvasApp(ctk.CTk): if self._saved_camera is None: self.log(" 뷰포인트 선택 취소됨.") - self.set_status("뷰포인트 미선택", "#E74C3C") + self.set_status("뷰포인트 미선택", "#EB001B") return # 선택된 카메라 위치 로그 (focal/up은 카메라 복원 시 다시 읽음) @@ -5901,7 +5917,7 @@ class SCanvasApp(ctk.CTk): ) self.guide_image.save("guide_composite.png") - self.set_status("제어맵 추출 완료", "#2ECC71") + self.set_status("제어맵 추출 완료", "#22A06B") self.btn_step4.configure(fg_color=["#3a7ebf", "#1f538d"]) self.log(" 저장 완료: capture_textured.png, depth_map.png, lineart_map.png, guide_composite.png") @@ -5911,7 +5927,7 @@ class SCanvasApp(ctk.CTk): except Exception as e: self.log(f"제어맵 추출 실패: {e}") - self.set_status("추출 실패", "#E74C3C") + self.set_status("추출 실패", "#EB001B") messagebox.showerror("오류", f"제어맵 추출 중 오류:\n{e}") def _open_interactive_viewer(self): @@ -6672,7 +6688,7 @@ class SCanvasApp(ctk.CTk): location = self.vertex_location.get().strip() or "global" self.set_status( f"Gemini 렌더링 중 ({('Vertex AI' if use_vertex else 'API')})...", - "#F1C40F" + "#F79E1B" ) thread = threading.Thread( target=self._run_gemini_render, @@ -6689,7 +6705,7 @@ class SCanvasApp(ctk.CTk): "또는 'Gemini (Nano Banana)'로 변경하세요.") return - self.set_status("AI 렌더링 중... (15~60초 소요)", "#F1C40F") + self.set_status("AI 렌더링 중... (15~60초 소요)", "#F79E1B") thread = threading.Thread( target=self._run_stability_render, args=(key, final_prompt, strength), @@ -6757,7 +6773,7 @@ class SCanvasApp(ctk.CTk): if self.job_logger and db and job_id: self.job_logger.fail_job(db, job_id, "모든 API 방법 실패") self.after(0, lambda: self.log(" 모든 방법 실패")) - self.after(0, lambda: self.set_status("AI 렌더링 실패", "#E74C3C")) + self.after(0, lambda: self.set_status("AI 렌더링 실패", "#EB001B")) return # 출력 화질 후처리 — 사용자가 Step 4에서 고른 HD/FHD/UHD 로 리사이즈 @@ -6791,7 +6807,7 @@ class SCanvasApp(ctk.CTk): self.after(0, lambda: self.log( f" AI 렌더링 완료! → {output_path} ({rendered.size}) " f"[{latency_ms:.0f}ms, 품질={quality_score:.2f}]")) - self.after(0, lambda: self.set_status("AI 렌더링 완료", "#2ECC71")) + self.after(0, lambda: self.set_status("AI 렌더링 완료", "#22A06B")) self.after(0, lambda: self._show_rendered_result(output_path)) except Exception as e: @@ -6799,7 +6815,7 @@ class SCanvasApp(ctk.CTk): with contextlib.suppress(Exception): self.job_logger.fail_job(db, job_id, str(e)) self.after(0, lambda e=e: self.log(f" 렌더링 오류: {e}")) - self.after(0, lambda: self.set_status("렌더링 실패", "#E74C3C")) + self.after(0, lambda: self.set_status("렌더링 실패", "#EB001B")) self.after(0, lambda e=e: messagebox.showerror("오류", f"AI 렌더링 중 오류:\n{e}")) finally: if db: @@ -7041,17 +7057,5 @@ if __name__ == "__main__": except Exception as _ch_err: print(f"[crash_logger] 설치 실패 (계속 진행): {_ch_err}") - # 인트로 스플래시 — Design/logo_intro.mp4 재생 후 메인 앱 기동. - # 실패·파일 없음 시 조용히 skip(메인 앱은 항상 뜸). - try: - from splash import show_intro_splash - show_intro_splash( - resource_path("Design", "logo_intro.mp4"), - max_duration_s=12.0, - fade_ms=400, - ) - except Exception as _splash_err: - print(f"[Intro] 스플래시 경고: {_splash_err}") - app = SCanvasApp() app.mainloop() diff --git a/splash.py b/splash.py deleted file mode 100644 index 43b40ac..0000000 --- a/splash.py +++ /dev/null @@ -1,178 +0,0 @@ -"""S-CANVAS 인트로 로딩 스플래시. - -Design/logo_intro.mp4 를 frameless Toplevel 중앙에 재생한 뒤 자동 종료. -메인 앱(`scanvas_maker.SCanvasApp`) 기동 직전에 호출한다. - -기술 스택: - - cv2 (VideoCapture) : MP4 프레임 디코드 (harness/quality_validator 가 - 이미 의존하는 프로젝트 공통 dep). - - PIL + tkinter.PhotoImage : 프레임 렌더. - - tk.Tk + attributes("-alpha", ...) : frameless + 페이드 인/아웃 효과. - -dynamic effects(사용자 요구 "역동적으로"): - 1. 시작 시 알파 0 → 1 페이드인 (400ms) - 2. 비디오 자체 애니메이션(logo_intro.mp4 는 고유 모션 포함) - 3. 종료 전 알파 1 → 0 페이드아웃 (400ms) - 4. 비디오 하단에 브랜드 tagline bar (오렌지 italic) - 5. max_duration_s 초과 시 강제 종료(safety) - -실패 조건(조용히 skip): - - logo_intro.mp4 없음 - - cv2/PIL import 실패 - - VideoCapture.open 실패 - 메인 앱 기동은 항상 보장된다. -""" -from __future__ import annotations - -import time -import tkinter as tk -from pathlib import Path -import contextlib - - -def show_intro_splash( - video_path, - max_duration_s: float = 12.0, - fade_ms: int = 400, - tagline: str = "S-CANVAS — Generative Design & Visualization Engine", - max_display_w: int = 1000, -) -> None: - """video_path 의 MP4 를 스플래시로 재생 후 블로킹 반환. - - Args: - video_path: Path-like, .mp4 파일. - max_duration_s: 비디오가 이 시간을 넘기면 강제 종료. - fade_ms: 페이드인/아웃 각 구간 길이(ms). - tagline: 비디오 아래에 표시할 문구. - max_display_w: 화면상 최대 가로(px). 이보다 크면 aspect 유지 축소. - - 동작: - - 임시 tk.Tk 루트를 만들고 mainloop → 종료 시 완전 destroy. - - 이후 `SCanvasApp()` 이 새 ctk.CTk 인스턴스를 만들어도 충돌 없음 - (Tk 루트가 이미 파괴됐으므로). - """ - try: - import cv2 - from PIL import Image, ImageTk - except ImportError as e: - print(f"[Intro] cv2/PIL 미설치 — 스플래시 skip ({e})") - return - - vp = Path(video_path) - if not vp.exists(): - print(f"[Intro] 비디오 없음 ({vp}) — 스플래시 skip") - return - - cap = cv2.VideoCapture(str(vp)) - if not cap.isOpened(): - print("[Intro] VideoCapture 열기 실패 — 스플래시 skip") - return - - fps = cap.get(cv2.CAP_PROP_FPS) or 30.0 - frame_ms = max(int(1000.0 / max(fps, 1.0)), 8) - vw = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 800) - vh = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 450) - - if vw > max_display_w: - disp_w = max_display_w - disp_h = max(1, int(vh * max_display_w / vw)) - else: - disp_w, disp_h = vw, vh - - root = tk.Tk() - root.withdraw() - screen_w = root.winfo_screenwidth() - screen_h = root.winfo_screenheight() - - TAG_H = 44 - total_h = disp_h + TAG_H - x = max(0, (screen_w - disp_w) // 2) - y = max(0, (screen_h - total_h) // 2) - - splash = tk.Toplevel(root) - splash.overrideredirect(True) - try: - splash.attributes("-topmost", True) - splash.attributes("-alpha", 0.0) - except tk.TclError: - pass - splash.configure(bg="#0A0F1C") - splash.geometry(f"{disp_w}x{total_h}+{x}+{y}") - - vid_label = tk.Label(splash, bg="#0A0F1C", borderwidth=0, highlightthickness=0) - vid_label.place(x=0, y=0, width=disp_w, height=disp_h) - - tag_label = tk.Label( - splash, text=tagline, - bg="#0A0F1C", fg="#E67E22", - font=("Segoe UI", 10, "italic"), - borderwidth=0, highlightthickness=0, - ) - tag_label.place(x=0, y=disp_h, width=disp_w, height=TAG_H) - - state = {"closed": False, "t_start": time.time()} - - def _safe_alpha(a): - with contextlib.suppress(tk.TclError): - splash.attributes("-alpha", max(0.0, min(1.0, a))) - - def _fade_to(target, duration_ms, done_cb=None): - steps = max(int(duration_ms / 16), 1) - start_alpha = float(splash.attributes("-alpha") or 0.0) - - def _step(i): - if state["closed"]: - return - ratio = i / steps - _safe_alpha(start_alpha + (target - start_alpha) * ratio) - if i < steps: - splash.after(16, lambda: _step(i + 1)) - elif done_cb: - done_cb() - - _step(0) - - def _close(): - if state["closed"]: - return - state["closed"] = True - with contextlib.suppress(Exception): - cap.release() - with contextlib.suppress(Exception): - splash.destroy() - with contextlib.suppress(Exception): - root.quit() - - def _play_next_frame(): - if state["closed"]: - return - if (time.time() - state["t_start"]) > max_duration_s: - _fade_to(0.0, fade_ms, done_cb=_close) - return - ret, frame = cap.read() - if not ret: - # 비디오 끝 — 페이드아웃 후 종료 - _fade_to(0.0, fade_ms, done_cb=_close) - return - rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - if (vw, vh) != (disp_w, disp_h): - rgb = cv2.resize(rgb, (disp_w, disp_h), interpolation=cv2.INTER_AREA) - img = Image.fromarray(rgb) - photo = ImageTk.PhotoImage(img) - vid_label.configure(image=photo) - vid_label.image = photo # GC 방지 - splash.after(frame_ms, _play_next_frame) - - # 페이드인 → 첫 프레임 재생 시작 - _fade_to(1.0, fade_ms, done_cb=_play_next_frame) - - try: - root.mainloop() - finally: - with contextlib.suppress(Exception): - root.destroy() - - -if __name__ == "__main__": - # 단독 테스트 - show_intro_splash(Path(__file__).resolve().parent / "Design" / "logo_intro.mp4")