'use client'; import { useEffect, useRef, useState } from 'react'; import { SplashScreen } from '@/components/ui/splash-screen'; import { SPLASH_SHOWN_STORAGE_KEY } from '@/lib/constants/splash'; /** 로딩 자체가 즉시 끝나도 스플래시가 깜빡이지 않도록 유지하는 최소 표시 시간(ms). */ const SPLASH_MIN_DISPLAY_MS = 700; function hasShownSplashThisSession(): boolean { try { return sessionStorage.getItem(SPLASH_SHOWN_STORAGE_KEY) === '1'; } catch { return false; } } function markSplashShown(): void { try { sessionStorage.setItem(SPLASH_SHOWN_STORAGE_KEY, '1'); } catch { // sessionStorage 접근 불가 환경(시크릿 모드 제한 등) — 매 로드마다 재표시되는 것으로 완화한다. } } /** * 스플래시 표시 게이팅 로직 — 비주얼(SplashScreen, design 소유)은 항상 서버 렌더와 동일하게 * 마운트해 hydration 불일치를 만들지 않는다. "이미 이번 세션에 표시했음"으로 인한 깜빡임 방지는 * `app/layout.tsx`의 `` 동기 inline script + CSS가 첫 페인트 전에 ``에 부여한 * data attribute가 담당한다(useEffect는 첫 페인트 이후에 실행되어 깜빡임을 막을 수 없다 — * preventing-flash-before-hydration.md). 이 컴포넌트는 같은 판정을 렌더 중 ref에 1회 계산해두고, * 이미 표시한 세션이면 effect에서 타이머 자체를 걸지 않는다(= state를 effect 본문에서 동기 * 호출하지 않아 cascading render를 만들지 않는다). */ export function SplashGate() { const [isVisible, setIsVisible] = useState(true); const [isExiting, setIsExiting] = useState(false); const skipIntroRef = useRef(null); if (skipIntroRef.current === null) { skipIntroRef.current = typeof window !== 'undefined' && hasShownSplashThisSession(); } useEffect(() => { if (skipIntroRef.current) { // 이미 inline script + CSS로 숨겨진 상태 — 타이머/퇴장 애니메이션을 돌릴 필요가 없다. return; } const timer = setTimeout(() => { setIsExiting(true); }, SPLASH_MIN_DISPLAY_MS); return () => clearTimeout(timer); }, []); function handleExitAnimationEnd() { markSplashShown(); setIsVisible(false); } if (!isVisible) { return null; } return (
); }