임동욱 임동욱 08-11
feat: 스플래시 제거
디자인 시안이 나오면 다시 추가한다. Tailwind 제거 이후 퇴장 애니메이션(animate-fade-out)
클래스가 죽어 onAnimationEnd가 영영 발화하지 않았고, 그 결과 markSplashShown()도
setIsVisible(false)도 호출되지 않아 모든 화면 상단에 스플래시가 영구히 남아 있었다.

삭제: app/_components/splash-gate.tsx · components/ui/splash-screen.tsx ·
lib/constants/splash.ts. app/layout.tsx의  inline style/script와 도
함께 걷어냈다.

lib/constants/theme.ts의 주석이 삭제될 splash 상수 파일을 가리키고 있어 그 상호참조만
제거했다. public/images/logo-foxpay-white.svg는 재추가 시 다시 쓰므로 남겨둔다.

(public)/layout.tsx는 AuthCard를 더 이상 감싸지 않으므로 그 설명 주석을 제거한다.

Co-Authored-By: Claude Opus 5 
@6649a02c9b957cec8da0ad72ae620646b275707a
app/(public)/layout.tsx
--- app/(public)/layout.tsx
+++ app/(public)/layout.tsx
@@ -1,7 +1,3 @@
-
-// Auth 레이아웃 — design 공용 AuthCard(프라이머리 배경 + 로고 + 카드)로 `(public)` 하위 화면
-// (로그인 등)을 감싼다. 화면별 제목은 AuthCard의 title 슬롯 대신 각 페이지 콘텐츠가 자체
-// 렌더한다 — 레이아웃은 여러 화면이 공유하므로 화면별 제목을 여기서 결정하기 어렵다.
 export default function Layout({
   children,
 }: Readonly<{
 
app/_components/splash-gate.tsx (deleted)
--- app/_components/splash-gate.tsx
@@ -1,76 +0,0 @@
-'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`의 `<head>` 동기 inline script + CSS가 첫 페인트 전에 `<html>`에 부여한
- * 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<boolean | null>(null);
-
-  if (skipIntroRef.current === null) {
-    skipIntroRef.current =
-      typeof window !== 'undefined' && hasShownSplashThisSession();
-  }
-
-  useEffect(() => {
-    if (skipIntroRef.current) {
-      // 이미 <head> 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 (
-    <div
-      data-splash-gate
-      className={isExiting ? 'animate-fade-out' : undefined}
-      onAnimationEnd={isExiting ? handleExitAnimationEnd : undefined}
-    >
-      <SplashScreen />
-    </div>
-  );
-}
app/layout.tsx
--- app/layout.tsx
+++ app/layout.tsx
@@ -3,8 +3,6 @@
 import localFont from "next/font/local";
 import { FeedbackHost } from "@/app/_components/feedback-host";
 import { FeedbackProvider } from "@/app/_components/feedback-provider";
-import { SplashGate } from "@/app/_components/splash-gate";
-import { SPLASH_SHOWN_STORAGE_KEY } from "@/lib/constants/splash";
 import {
   THEME_ATTRIBUTE_NAME,
   THEME_STORAGE_KEY,
@@ -20,26 +18,12 @@
 // THEME_STORAGE_KEY/THEME_ATTRIBUTE_NAME은 반드시 'use client' 파일이 아닌
 // lib/constants/theme에서 import한다 — 'use client' 모듈의 일반 값(문자열) export를
 // Server Component가 직접 import하면 클라이언트 레퍼런스 프록시 특성상 undefined로 평가된다
-// (컴포넌트 export만 정상적으로 프록시된다. splash 상수와 동일한 이유로 분리됨).
+// (컴포넌트 export만 정상적으로 프록시된다).
 const THEME_BOOTSTRAP_SCRIPT = `(function(){try{var t=localStorage.getItem(${JSON.stringify(
   THEME_STORAGE_KEY
 )});if(t==="light"||t==="dark")document.documentElement.setAttribute(${JSON.stringify(
   THEME_ATTRIBUTE_NAME
 )},t)}catch(e){}})()`;
-
-// 스플래시 재표시 방지 — <head>는 <body>보다 먼저 파싱되므로 body 하위 요소는 아직 존재하지
-// 않는다(preventing-flash-before-hydration.md의 Themes 패턴과 동일하게 <html>만 조작 가능).
-// 이번 세션에 이미 표시했다면 첫 페인트 전에 <html>에 data attribute를 부여하고, 아래 스타일
-// 규칙이 SplashGate 루트를 즉시 숨긴다 — useEffect보다 먼저 실행되어 깜빡임이 없다.
-//
-// SPLASH_SHOWN_STORAGE_KEY는 반드시 'use client' 파일이 아닌 lib/constants/splash에서
-// import한다 — splash-gate.tsx('use client')의 일반 값(문자열) export를 Server Component가
-// 직접 import하면 클라이언트 레퍼런스 프록시 특성상 undefined로 평가된다(컴포넌트 export만
-// 정상적으로 프록시된다).
-const SPLASH_HIDE_STYLE = `html[data-splash-shown] [data-splash-gate]{display:none}`;
-const SPLASH_GATE_SCRIPT = `(function(){try{if(sessionStorage.getItem(${JSON.stringify(
-  SPLASH_SHOWN_STORAGE_KEY
-)}))document.documentElement.setAttribute("data-splash-shown","")}catch(e){}})()`;
 
 // 어드민 전체 기본 서체. 폰트 파일은 **앱이 소유**하고 디자인 시스템은 참조만 한다 —
 // `@fox`는 `--app-font-sans`를 읽을 뿐이라(@fox/README.md "호스트 앱과의 계약") 폴더를
@@ -112,11 +96,8 @@
     >
       <head>
         <script dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP_SCRIPT }} />
-        <style dangerouslySetInnerHTML={{ __html: SPLASH_HIDE_STYLE }} />
-        <script dangerouslySetInnerHTML={{ __html: SPLASH_GATE_SCRIPT }} />
       </head>
       <body>
-        <SplashGate />
         <FeedbackProvider>
           {children}
           <FeedbackHost />
 
components/ui/splash-screen.tsx (deleted)
--- components/ui/splash-screen.tsx
@@ -1,22 +0,0 @@
-import Image from "next/image";
-
-/**
- * 풀스크린 스플래시. 네이티브 스플래시(edupay-app SplashConfig)와 동일하게
- * primary 단색 배경 + 백색 로고 240px(logoWidth) 구성을 따른다.
- * 표시 여부·퇴장 타이밍(fade-out 적용)은 호출부(feature) 책임 — 이 컴포넌트는
- * props를 받지 않는다.
- */
-export function SplashScreen() {
-  return (
-    <div className="fixed inset-0 z-50 flex animate-fade-in items-center justify-center bg-primary">
-      <Image
-        src="/images/logo-foxpay-white.svg"
-        alt="FoxPay"
-        width={170}
-        height={62}
-        className="h-auto w-60"
-        priority
-      />
-    </div>
-  );
-}
 
lib/constants/splash.ts (deleted)
--- lib/constants/splash.ts
@@ -1,10 +0,0 @@
-/**
- * 스플래시를 세션당 1회만 표시했음을 기록하는 sessionStorage 키.
- *
- * `app/layout.tsx`(Server Component — `<head>` inline script 문자열 생성)와
- * `app/_components/splash-gate.tsx`('use client') 양쪽이 같은 값을 참조해야 한다. 'use client'
- * 모듈의 export는 컴포넌트가 아닌 일반 값(문자열 등)일 경우 Server Component에서 import 시
- * `undefined`로 평가된다 — Next.js의 클라이언트 레퍼런스 프록시가 컴포넌트 형태만 가정하기
- * 때문이다. 그래서 이 상수는 'use client'가 없는 순수 모듈로 분리한다.
- */
-export const SPLASH_SHOWN_STORAGE_KEY = "edupay-admin-splash-shown";
lib/constants/theme.ts
--- lib/constants/theme.ts
+++ lib/constants/theme.ts
@@ -4,8 +4,7 @@
  * `app/layout.tsx`(Server Component — `<head>` inline script 문자열 생성)와
  * `_hooks/use-theme-preference.ts`('use client') 양쪽이 같은 값을 참조해야 한다. 'use client'
  * 모듈의 export는 컴포넌트가 아닌 일반 값(문자열 등)일 경우 Server Component에서 import 시
- * `undefined`로 평가된다(lib/constants/splash.ts와 동일한 이유) — 그래서 이 상수도 'use client'가
- * 없는 순수 모듈로 분리한다.
+ * `undefined`로 평가된다 — 그래서 이 상수는 'use client'가 없는 순수 모듈로 분리한다.
  */
 
 /** 테마 선택값. "system"은 OS 설정을 따르며 `<html>`에 속성을 부여하지 않는 것으로 표현한다. */
Add a comment
List