""" 웰컴 모달 컴포넌트 - YouTube 튜토리얼 영상 팝업 첫 방문 시 자동으로 표시, "다시 보지 않기" 기능 지원 localStorage 키: 'welcome_modal_dismissed' (게임 상태와 독립적) """ import gradio as gr class WelcomeModal: """ 웰컴 모달 - YouTube 튜토리얼 영상 표시 - 첫 방문 시 자동 표시 - "다시 보지 않기" 클릭 시 localStorage에 저장 - 게임 초기화와 독립적으로 설정 유지 """ # YouTube 영상 ID YOUTUBE_VIDEO_ID = "YbCf6x0B3fU" # localStorage 키 (게임 상태와 별도) STORAGE_KEY = "welcome_modal_dismissed" HTML_TEMPLATE = """ ${value && value.visible ? `
` : ''} """ CSS_TEMPLATE = """ /* 웰컴 모달 - 전역 스타일에 영향 없도록 스코핑 */ .welcome-modal-overlay { position: fixed !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; width: 100vw !important; height: 100vh !important; background-color: rgba(0, 0, 0, 0.7) !important; backdrop-filter: blur(4px); z-index: 9999 !important; display: flex !important; justify-content: center !important; align-items: center !important; padding: 20px; box-sizing: border-box; font-family: inherit; } .welcome-modal-container { width: 100%; max-width: 1280px; max-height: 90vh; overflow-y: auto; position: relative; animation: welcomeModalFadeIn 0.3s ease-out; font-family: inherit; border-radius: 20px; } @keyframes welcomeModalFadeIn { from { opacity: 0; transform: scale(0.9) translateY(-20px); } to { opacity: 1; transform: scale(1) translateY(0); } } .welcome-modal-close { position: absolute; top: 16px; right: 16px; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #ffffff; transition: all 0.2s ease; z-index: 10; padding: 0; font-family: inherit; } .welcome-modal-close:hover { background: rgba(255, 255, 255, 0.2); transform: scale(1.1); } .welcome-modal-content { padding: 32px; text-align: center; font-family: inherit; } .welcome-modal-title { font-size: 60px; font-weight: 700; color: #ffffff; margin: 0 0 24px 0; font-family: inherit; } .welcome-modal-subtitle { font-size: 16px; color: rgba(255, 255, 255, 0.7); margin: 0 0 24px 0; font-family: inherit; } .welcome-modal-video { position: relative; width: 100%; padding-bottom: 56.25%; /* 16:9 aspect ratio */ border-radius: 16px; overflow: hidden; background: #000; margin-bottom: 24px; } .welcome-modal-video iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .welcome-modal-actions { display: flex; justify-content: center; gap: 12px; align-items: center; } .welcome-modal-btn { padding: 14px 32px; border-radius: 20px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; border: none; min-width: 200px; font-family: inherit; } .welcome-modal-btn-primary { background: #4db8ff !important; color: #ffffff !important; } .welcome-modal-btn-primary:hover { transform: translateY(-2px); } .welcome-modal-btn-secondary { background: transparent; color: rgba(255, 255, 255, 0.6); border: 1px solid rgba(255, 255, 255, 0.2); } .welcome-modal-btn-secondary:hover { background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.9); } /* Mobile responsive */ @media (max-width: 600px) { .welcome-modal-container { max-width: 100%; border-radius: 16px; } .welcome-modal-content { padding: 24px 16px; } .welcome-modal-title { font-size: 22px; } .welcome-modal-btn { width: 100%; min-width: unset; } } """ JS_ON_LOAD = """ const STORAGE_KEY = 'welcome_modal_dismissed'; const videoId = props.value?.videoId || 'YbCf6x0B3fU'; // 페이지 로드 시 localStorage 체크 const isDismissed = localStorage.getItem(STORAGE_KEY) === 'true'; // "다시 보지 않기"를 선택하지 않은 경우에만 모달 표시 if (!isDismissed) { // 폰트가 완전히 로드된 후 모달 표시 document.fonts.ready.then(() => { // 폰트 로드 후 약간의 딜레이 추가 (렌더링 안정화) setTimeout(() => { props.value = { visible: true, videoId: videoId }; }, 100); }); } // 클릭 이벤트 핸들러 element.addEventListener('click', (e) => { // 배경 클릭으로 닫기 if (e.target.dataset.welcomeOverlay === 'true') { props.value = { visible: false, videoId: props.value?.videoId }; } // X 버튼 또는 "Start Playing" 버튼 클릭 if (e.target.closest('[data-welcome-close="true"]')) { props.value = { visible: false, videoId: props.value?.videoId }; } // "Don't show again" 버튼 클릭 if (e.target.closest('[data-welcome-dismiss="true"]')) { localStorage.setItem(STORAGE_KEY, 'true'); props.value = { visible: false, videoId: props.value?.videoId }; } }); // ESC 키로 닫기 const handleKeydown = (e) => { if (e.key === 'Escape' && props.value && props.value.visible) { props.value = { visible: false, videoId: props.value?.videoId }; } }; document.addEventListener('keydown', handleKeydown); """ def __init__(self, video_id: str = None): """ 웰컴 모달 초기화 Args: video_id: YouTube 영상 ID (기본값: YbCf6x0B3fU) """ self.video_id = video_id or self.YOUTUBE_VIDEO_ID self.component = None def render(self) -> gr.HTML: """ 웰컴 모달 컴포넌트 렌더링 Returns: gr.HTML: 렌더링된 모달 컴포넌트 """ # 초기값은 visible: False로 설정 (JS에서 로드 후 표시) self.component = gr.HTML( value={"visible": False, "videoId": self.video_id}, html_template=self.HTML_TEMPLATE, css_template=self.CSS_TEMPLATE, js_on_load=self.JS_ON_LOAD, elem_id="welcome-modal", ) return self.component @staticmethod def show(video_id: str = "YbCf6x0B3fU") -> dict: """모달 열기""" return {"visible": True, "videoId": video_id} @staticmethod def hide() -> dict: """모달 닫기""" return {"visible": False, "videoId": ""}