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
... | ... | @@ -1,7 +1,3 @@ |
| 1 |
- |
|
| 2 |
-// Auth 레이아웃 — design 공용 AuthCard(프라이머리 배경 + 로고 + 카드)로 `(public)` 하위 화면 |
|
| 3 |
-// (로그인 등)을 감싼다. 화면별 제목은 AuthCard의 title 슬롯 대신 각 페이지 콘텐츠가 자체 |
|
| 4 |
-// 렌더한다 — 레이아웃은 여러 화면이 공유하므로 화면별 제목을 여기서 결정하기 어렵다. |
|
| 5 | 1 |
export default function Layout({
|
| 6 | 2 |
children, |
| 7 | 3 |
}: Readonly<{
|
--- app/_components/splash-gate.tsx
... | ... | @@ -1,76 +0,0 @@ |
| 1 | -'use client'; | |
| 2 | - | |
| 3 | -import { useEffect, useRef, useState } from 'react'; | |
| 4 | -import { SplashScreen } from '@/components/ui/splash-screen'; | |
| 5 | -import { SPLASH_SHOWN_STORAGE_KEY } from '@/lib/constants/splash'; | |
| 6 | - | |
| 7 | -/** 로딩 자체가 즉시 끝나도 스플래시가 깜빡이지 않도록 유지하는 최소 표시 시간(ms). */ | |
| 8 | -const SPLASH_MIN_DISPLAY_MS = 700; | |
| 9 | - | |
| 10 | -function hasShownSplashThisSession(): boolean { | |
| 11 | - try { | |
| 12 | - return sessionStorage.getItem(SPLASH_SHOWN_STORAGE_KEY) === '1'; | |
| 13 | - } catch { | |
| 14 | - return false; | |
| 15 | - } | |
| 16 | -} | |
| 17 | - | |
| 18 | -function markSplashShown(): void { | |
| 19 | - try { | |
| 20 | - sessionStorage.setItem(SPLASH_SHOWN_STORAGE_KEY, '1'); | |
| 21 | - } catch { | |
| 22 | - // sessionStorage 접근 불가 환경(시크릿 모드 제한 등) — 매 로드마다 재표시되는 것으로 완화한다. | |
| 23 | - } | |
| 24 | -} | |
| 25 | - | |
| 26 | -/** | |
| 27 | - * 스플래시 표시 게이팅 로직 — 비주얼(SplashScreen, design 소유)은 항상 서버 렌더와 동일하게 | |
| 28 | - * 마운트해 hydration 불일치를 만들지 않는다. "이미 이번 세션에 표시했음"으로 인한 깜빡임 방지는 | |
| 29 | - * `app/layout.tsx`의 `<head>` 동기 inline script + CSS가 첫 페인트 전에 `<html>`에 부여한 | |
| 30 | - * data attribute가 담당한다(useEffect는 첫 페인트 이후에 실행되어 깜빡임을 막을 수 없다 — | |
| 31 | - * preventing-flash-before-hydration.md). 이 컴포넌트는 같은 판정을 렌더 중 ref에 1회 계산해두고, | |
| 32 | - * 이미 표시한 세션이면 effect에서 타이머 자체를 걸지 않는다(= state를 effect 본문에서 동기 | |
| 33 | - * 호출하지 않아 cascading render를 만들지 않는다). | |
| 34 | - */ | |
| 35 | -export function SplashGate() { | |
| 36 | - const [isVisible, setIsVisible] = useState(true); | |
| 37 | - const [isExiting, setIsExiting] = useState(false); | |
| 38 | - const skipIntroRef = useRef<boolean | null>(null); | |
| 39 | - | |
| 40 | - if (skipIntroRef.current === null) { | |
| 41 | - skipIntroRef.current = | |
| 42 | - typeof window !== 'undefined' && hasShownSplashThisSession(); | |
| 43 | - } | |
| 44 | - | |
| 45 | - useEffect(() => { | |
| 46 | - if (skipIntroRef.current) { | |
| 47 | - // 이미 <head> inline script + CSS로 숨겨진 상태 — 타이머/퇴장 애니메이션을 돌릴 필요가 없다. | |
| 48 | - return; | |
| 49 | - } | |
| 50 | - | |
| 51 | - const timer = setTimeout(() => { | |
| 52 | - setIsExiting(true); | |
| 53 | - }, SPLASH_MIN_DISPLAY_MS); | |
| 54 | - | |
| 55 | - return () => clearTimeout(timer); | |
| 56 | - }, []); | |
| 57 | - | |
| 58 | - function handleExitAnimationEnd() { | |
| 59 | - markSplashShown(); | |
| 60 | - setIsVisible(false); | |
| 61 | - } | |
| 62 | - | |
| 63 | - if (!isVisible) { | |
| 64 | - return null; | |
| 65 | - } | |
| 66 | - | |
| 67 | - return ( | |
| 68 | - <div | |
| 69 | - data-splash-gate | |
| 70 | - className={isExiting ? 'animate-fade-out' : undefined} | |
| 71 | - onAnimationEnd={isExiting ? handleExitAnimationEnd : undefined} | |
| 72 | - > | |
| 73 | - <SplashScreen /> | |
| 74 | - </div> | |
| 75 | - ); | |
| 76 | -} |
--- app/layout.tsx
+++ app/layout.tsx
... | ... | @@ -3,8 +3,6 @@ |
| 3 | 3 |
import localFont from "next/font/local"; |
| 4 | 4 |
import { FeedbackHost } from "@/app/_components/feedback-host";
|
| 5 | 5 |
import { FeedbackProvider } from "@/app/_components/feedback-provider";
|
| 6 |
-import { SplashGate } from "@/app/_components/splash-gate";
|
|
| 7 |
-import { SPLASH_SHOWN_STORAGE_KEY } from "@/lib/constants/splash";
|
|
| 8 | 6 |
import {
|
| 9 | 7 |
THEME_ATTRIBUTE_NAME, |
| 10 | 8 |
THEME_STORAGE_KEY, |
... | ... | @@ -20,26 +18,12 @@ |
| 20 | 18 |
// THEME_STORAGE_KEY/THEME_ATTRIBUTE_NAME은 반드시 'use client' 파일이 아닌 |
| 21 | 19 |
// lib/constants/theme에서 import한다 — 'use client' 모듈의 일반 값(문자열) export를 |
| 22 | 20 |
// Server Component가 직접 import하면 클라이언트 레퍼런스 프록시 특성상 undefined로 평가된다 |
| 23 |
-// (컴포넌트 export만 정상적으로 프록시된다. splash 상수와 동일한 이유로 분리됨). |
|
| 21 |
+// (컴포넌트 export만 정상적으로 프록시된다). |
|
| 24 | 22 |
const THEME_BOOTSTRAP_SCRIPT = `(function(){try{var t=localStorage.getItem(${JSON.stringify(
|
| 25 | 23 |
THEME_STORAGE_KEY |
| 26 | 24 |
)});if(t==="light"||t==="dark")document.documentElement.setAttribute(${JSON.stringify(
|
| 27 | 25 |
THEME_ATTRIBUTE_NAME |
| 28 | 26 |
)},t)}catch(e){}})()`;
|
| 29 |
- |
|
| 30 |
-// 스플래시 재표시 방지 — <head>는 <body>보다 먼저 파싱되므로 body 하위 요소는 아직 존재하지 |
|
| 31 |
-// 않는다(preventing-flash-before-hydration.md의 Themes 패턴과 동일하게 <html>만 조작 가능). |
|
| 32 |
-// 이번 세션에 이미 표시했다면 첫 페인트 전에 <html>에 data attribute를 부여하고, 아래 스타일 |
|
| 33 |
-// 규칙이 SplashGate 루트를 즉시 숨긴다 — useEffect보다 먼저 실행되어 깜빡임이 없다. |
|
| 34 |
-// |
|
| 35 |
-// SPLASH_SHOWN_STORAGE_KEY는 반드시 'use client' 파일이 아닌 lib/constants/splash에서 |
|
| 36 |
-// import한다 — splash-gate.tsx('use client')의 일반 값(문자열) export를 Server Component가
|
|
| 37 |
-// 직접 import하면 클라이언트 레퍼런스 프록시 특성상 undefined로 평가된다(컴포넌트 export만 |
|
| 38 |
-// 정상적으로 프록시된다). |
|
| 39 |
-const SPLASH_HIDE_STYLE = `html[data-splash-shown] [data-splash-gate]{display:none}`;
|
|
| 40 |
-const SPLASH_GATE_SCRIPT = `(function(){try{if(sessionStorage.getItem(${JSON.stringify(
|
|
| 41 |
- SPLASH_SHOWN_STORAGE_KEY |
|
| 42 |
-)}))document.documentElement.setAttribute("data-splash-shown","")}catch(e){}})()`;
|
|
| 43 | 27 |
|
| 44 | 28 |
// 어드민 전체 기본 서체. 폰트 파일은 **앱이 소유**하고 디자인 시스템은 참조만 한다 — |
| 45 | 29 |
// `@fox`는 `--app-font-sans`를 읽을 뿐이라(@fox/README.md "호스트 앱과의 계약") 폴더를 |
... | ... | @@ -112,11 +96,8 @@ |
| 112 | 96 |
> |
| 113 | 97 |
<head> |
| 114 | 98 |
<script dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP_SCRIPT }} />
|
| 115 |
- <style dangerouslySetInnerHTML={{ __html: SPLASH_HIDE_STYLE }} />
|
|
| 116 |
- <script dangerouslySetInnerHTML={{ __html: SPLASH_GATE_SCRIPT }} />
|
|
| 117 | 99 |
</head> |
| 118 | 100 |
<body> |
| 119 |
- <SplashGate /> |
|
| 120 | 101 |
<FeedbackProvider> |
| 121 | 102 |
{children}
|
| 122 | 103 |
<FeedbackHost /> |
--- components/ui/splash-screen.tsx
... | ... | @@ -1,22 +0,0 @@ |
| 1 | -import Image from "next/image"; | |
| 2 | - | |
| 3 | -/** | |
| 4 | - * 풀스크린 스플래시. 네이티브 스플래시(edupay-app SplashConfig)와 동일하게 | |
| 5 | - * primary 단색 배경 + 백색 로고 240px(logoWidth) 구성을 따른다. | |
| 6 | - * 표시 여부·퇴장 타이밍(fade-out 적용)은 호출부(feature) 책임 — 이 컴포넌트는 | |
| 7 | - * props를 받지 않는다. | |
| 8 | - */ | |
| 9 | -export function SplashScreen() { | |
| 10 | - return ( | |
| 11 | - <div className="fixed inset-0 z-50 flex animate-fade-in items-center justify-center bg-primary"> | |
| 12 | - <Image | |
| 13 | - src="/images/logo-foxpay-white.svg" | |
| 14 | - alt="FoxPay" | |
| 15 | - width={170} | |
| 16 | - height={62} | |
| 17 | - className="h-auto w-60" | |
| 18 | - priority | |
| 19 | - /> | |
| 20 | - </div> | |
| 21 | - ); | |
| 22 | -} |
--- lib/constants/splash.ts
... | ... | @@ -1,10 +0,0 @@ |
| 1 | -/** | |
| 2 | - * 스플래시를 세션당 1회만 표시했음을 기록하는 sessionStorage 키. | |
| 3 | - * | |
| 4 | - * `app/layout.tsx`(Server Component — `<head>` inline script 문자열 생성)와 | |
| 5 | - * `app/_components/splash-gate.tsx`('use client') 양쪽이 같은 값을 참조해야 한다. 'use client' | |
| 6 | - * 모듈의 export는 컴포넌트가 아닌 일반 값(문자열 등)일 경우 Server Component에서 import 시 | |
| 7 | - * `undefined`로 평가된다 — Next.js의 클라이언트 레퍼런스 프록시가 컴포넌트 형태만 가정하기 | |
| 8 | - * 때문이다. 그래서 이 상수는 'use client'가 없는 순수 모듈로 분리한다. | |
| 9 | - */ | |
| 10 | -export const SPLASH_SHOWN_STORAGE_KEY = "edupay-admin-splash-shown"; |
--- lib/constants/theme.ts
+++ lib/constants/theme.ts
... | ... | @@ -4,8 +4,7 @@ |
| 4 | 4 |
* `app/layout.tsx`(Server Component — `<head>` inline script 문자열 생성)와 |
| 5 | 5 |
* `_hooks/use-theme-preference.ts`('use client') 양쪽이 같은 값을 참조해야 한다. 'use client'
|
| 6 | 6 |
* 모듈의 export는 컴포넌트가 아닌 일반 값(문자열 등)일 경우 Server Component에서 import 시 |
| 7 |
- * `undefined`로 평가된다(lib/constants/splash.ts와 동일한 이유) — 그래서 이 상수도 'use client'가 |
|
| 8 |
- * 없는 순수 모듈로 분리한다. |
|
| 7 |
+ * `undefined`로 평가된다 — 그래서 이 상수는 'use client'가 없는 순수 모듈로 분리한다. |
|
| 9 | 8 |
*/ |
| 10 | 9 |
|
| 11 | 10 |
/** 테마 선택값. "system"은 OS 설정을 따르며 `<html>`에 속성을 부여하지 않는 것으로 표현한다. */ |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?