feat(ui): Mastercard 팔레트 1차 적용 + 인트로 비디오 제거 (#4 부분)
Some checks failed
CI / Ruff + Test (Py3.11 + Py3.13) (3.11) (push) Failing after 10s
CI / Ruff + Test (Py3.11 + Py3.13) (3.13) (push) Failing after 10s

색감/텍스쳐 라운드:
- 50+ hex literal 갱신 → Mastercard 공식 brand 컬러로 통일.
- Bootstrap-ish 팔레트 (#2ECC71 green / #E74C3C red / #F1C40F yellow / #E67E22
  orange / #343A40 slate) → MC Red(#EB001B) / MC Yellow(#F79E1B) /
  Brand Green(#22A06B) / Near-black(#1A1A1A) / Black(#000000).
- 주요 매핑: 에러 status → MC Red, 경고 status → MC Yellow, READY 인디케이터
  → Brand Green, 오렌지 CTA → MC Red, 다크 슬레이트 버튼 → MC Near-black.
- scanvas_maker.py line ~33-47 에 팔레트 의도 주석 블록 추가.
- npx getdesign@latest add mastercard 시도는 외부 npm 코드 실행 차단으로
  건너뛰고 공개 brand 가이드라인 컬러 적용.

인트로 영상 제거:
- splash.py 삭제 (178 LOC, show_intro_splash 함수 단일 진입점).
- Design/logo_intro.mp4 삭제 (3.7 MB binary).
- scanvas_maker.py 의 호출부 13줄 제거 (~line 7044-7054).
- 효과: 첫 화면까지 12초 fade-in 인트로 제거 → 즉시 기동.

잔여 (#4 다음 라운드, multi-session):
- 단일 창 구조 (CTkToplevel 12개 통합).
- 인라인 로그 패널 → 백엔드 파일.
- VTK 임베딩 (vtkTkRenderWidget 또는 pyvistaqt).
- 3-column 레이아웃 (Sidebar/Canvas/Inspector).
- messagebox 63회 → 인라인 토스트.

검증: py_compile + AST OK. splash/show_intro_splash 호출 0건.
.gitignore 정상 (gcp-key.json/*.log/*.db/cache/__pycache__/venv 모두 ignored).

CHANGELOG.md 에 #4 1차 라운드 항목 추가.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 16:59:10 +09:00
parent 470020cf57
commit 5a44c90ea6
4 changed files with 104 additions and 231 deletions

View File

@@ -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()