diff --git a/docs/README.md b/docs/README.md index d791599..2e09957 100644 --- a/docs/README.md +++ b/docs/README.md @@ -77,8 +77,10 @@ review 구조와 BM 연결을 볼 때 읽는다. 구현 규칙이나 구조를 볼 때 읽는다. +- [08_premium_uiux_guideline.md](./foundation/08_premium_uiux_guideline.md) `source-of-truth` + - 세계 최고급(LifeAt, Portal 수준) UI/UX 톤앤매너, 글래스모피즘, 모션 등 프리미엄 디자인 절대 원칙 - [01_ui_guidelines.md](./foundation/01_ui_guidelines.md) `source-of-truth` - - UI 톤, CTA 위계, premium 품질 기준 + - UI 톤, CTA 위계, premium 품질 기준 (일반 가이드) - [02_arch_fsd_rules.md](./foundation/02_arch_fsd_rules.md) `source-of-truth` - FSD/레이어 구조 규칙 - [03_routes_map.md](./foundation/03_routes_map.md) `source-of-truth` diff --git a/docs/foundation/08_premium_uiux_guideline.md b/docs/foundation/08_premium_uiux_guideline.md new file mode 100644 index 0000000..94e987b --- /dev/null +++ b/docs/foundation/08_premium_uiux_guideline.md @@ -0,0 +1,122 @@ +# Premium Immersive UI/UX Guidelines (VibeRoom Core) + +이 문서는 VibeRoom 프로젝트가 LifeAt, Portal, Focusmate 등 **세계 최고급의 프리미엄 UI/UX 경험**을 일관되게 유지하기 위해 작성된 절대적인 디자인 헌장입니다. +어떤 에이전트, 어떤 개발자가 코드를 작성하든 화면을 추가하거나 수정할 때 이 가이드라인을 반드시 숙지하고 엄격하게 준수해야 합니다. + +--- + +## 1. 핵심 철학 (Core Philosophy) + +### 1-1. 무대 우선주의 (Stage-first & Immersive) +- **절대 원칙:** 사용자가 선택한 **배경(Atmosphere/Scene) 자체가 곧 앱의 정체성이자 무대**입니다. +- 배경을 가리거나 시야를 방해하는 거대한 대시보드 형태의 레이아웃(Split-screen, 거대한 Solid Card Grid)은 절대 금지합니다. +- UI는 무대 위에 떠 있는 얇고 투명한 유리 조각(Glass)처럼 존재해야 하며, 화면의 여백(White Space)을 극대화하여 공간감을 제공해야 합니다. + +### 1-2. 중앙 집중의 의식 (Minimal Central Ritual) +- 사용자가 수행해야 할 가장 중요한 단 하나의 핵심 액션(예: "무엇에 집중할 것인가?")은 **항상 화면의 정중앙에 거대하고 우아하게 배치**합니다. +- 텍스트 입력창은 투명하게(`bg-transparent`), 폰트는 크고 얇게(`text-4xl font-light tracking-tight`) 유지하여 단순한 '입력'이 아닌 '의식(Ritual)'처럼 느껴지게 합니다. + +### 1-3. 압도적인 글래스모피즘 (Premium Glassmorphism) +- 단순히 투명도를 낮추는 것이 아닙니다. 뒤의 빛과 배경이 은은하게 굴절되는 진짜 유리의 질감을 구현해야 합니다. +- **필수 속성:** `backdrop-blur-xl` 또는 `backdrop-blur-2xl`을 반드시 사용합니다. +- **테두리와 명암:** 투박한 solid border 대신, `border-white/5` ~ `border-white/10` 수준의 매우 얇고 투명한 테두리를 사용합니다. 깊이감을 위해 다중 그림자(`shadow-2xl` 등)와 미세한 그라데이션(`bg-[linear-gradient(...)]`)을 조합합니다. + +--- + +## 2. 레이아웃 & 컴포넌트 배치 원칙 (Layout & Placement) + +### 2-1. 절대 위치(Absolute) 사용의 엄격한 제한 +- UI 요소가 창 크기 조절(Resizing) 시 중앙의 핵심 컨텐츠(Main Ritual)를 가리거나 겹치는 현상(Overlap)은 치명적인 결함입니다. +- **해결책:** 좌/우측에 둥둥 떠 있는(Floating) 위젯 형태를 구현할 때, 단순히 `absolute top-x left-y`로 띄워두지 마십시오. 창이 작아져도 겹치지 않게 하려면 중앙 컨테이너의 흐름(Inline Flex) 내부에 배치하거나, 화면 크기에 따른 철저한 미디어 쿼리 제어를 통해 **어떤 해상도에서도 메인 텍스트 영역을 침범하지 않도록 보장**해야 합니다. + +### 2-2. 보조 위젯의 극단적 미니멀리즘 (Subtle Accessories) +- 메인 액션이 아닌 모든 정보(예: Weekly Review, Error Message, 힌트 등)는 **크기를 최소화하고 시각적 대비를 낮춥니다.** +- 정보 텍스트는 `text-[12px]` 또는 `text-[13px]`, 라벨이나 뱃지는 `text-[9px] ~ text-[10px]`에 두꺼운 자간(`tracking-[0.25em]`)과 대문자(`uppercase`) 조합을 사용하여 명품 브랜드의 타이포그래피처럼 디자인합니다. +- 예: 투박한 'Weekly Review' 카드 ❌ -> 한 줄의 세련된 'Smart Hint Pill' 형태 ⭕ + +### 2-3. 가장자리 도킹 (Edge Docking) +- 선택형 리스트(Atmosphere 선택 등)는 화면을 덮는 Grid 대신 **화면 최하단에 스와이프 가능한 가로 독(Carousel Dock)** 형태로 배치합니다. +- 독 내부의 아이템이 확대(Scale)되거나 애니메이션 될 때, 스크롤 컨테이너의 영역이 좁아 카드가 잘려 보이는 현상(Clipping)이 발생하지 않도록 **컨테이너 자체에 충분한 상하 여백(`py-8` 등)**을 확보해야 합니다. + +--- + +## 3. 애니메이션 및 상호작용 (Motion & Interaction) + +### 3-1. 부드럽고 웅장한 진입 (Stately Entrance) +- 뚝 떨어지거나 딱딱하게 나타나는 화면 전환은 금지합니다. +- 프리미엄 서비스 특유의 '서서히 떠오르는' 모션을 위해 커스텀 Keyframe(`fade-in-up`, `fade-in`)과 섬세한 이징 커브(`cubic-bezier(0.16, 1, 0.3, 1)`)를 적용합니다. +- `animation-delay`를 활용하여 헤더 -> 중앙 텍스트 -> 하단 독 순서로 물결치듯 순차적으로 나타나는 시퀀스를 구성합니다. + +### 3-2. Hover 및 포커스 상태 (Fluid Feedback) +- 카드 호버 시 단순히 색만 변하는 것이 아니라, 부드러운 스케일 업(`hover:scale-105 ~ 110`)과 함께 그림자가 깊어지고(`hover:shadow-2xl`) 내부 텍스트 및 오버레이의 명도가 미세하게 조절되어야 합니다. +- 버튼의 경우 누를 때 미세하게 작아지는 햅틱 피드백(`active:scale-[0.98]`)을 적용하여 쫀득한 터치감을 줍니다. + +--- + +## 4. 타이포그래피 및 카피 (Typography & Copy) + +### 4-1. 소음 줄이기 (Reduce Visual Noise) +- 중요하지 않은 부가 설명(Helper Text)은 `text-white/40` ~ `text-white/60` 정도로 투명도를 과감히 낮춰 시야에서 멀어지게 합니다. +- 강렬한 Primary Color(파란색, 빨간색 등)는 에러나 꼭 필요한 CTA에만 극도로 제한적으로 사용하고, 기본적으로는 **무채색(흰색, 검은색)의 투명도 조절만으로 위계를 표현**합니다. + +### 4-2. 프리미엄 카피라이팅 (Tone & Manner) +- 기능적인 설명보다 감성적이고 몰입을 돕는 문구를 사용합니다. +- "시간을 입력하세요" ❌ -> "What will you focus on?" / "의식을 시작합니다" ⭕ +- 업그레이드 등 상업적 CTA도 "결제하기"보다 작고 섬세한 캡슐 버튼(`Upgrade →`)으로 디자인하여 브랜드의 우아함을 지킵니다. + +## 5. AI 에이전트를 위한 Tailwind CSS 프롬프팅 & 코드 패턴 (Crucial for AI) + +AI 에이전트(Codex, Cursor, Cline 등)는 "프리미엄하게 만들어줘"라는 추상적인 지시를 이해하지 못하고 평범한 UI(`bg-gray-100 rounded-md` 등)를 생성하는 경향이 있습니다. AI에게 작업을 지시할 때는 아래의 **명확한 Tailwind 유틸리티 패턴(Snippet)** 을 그대로 복사해서 사용하라고 지시해야 합니다. + +### 5-1. Floating Smart Pill (보조 정보 위젯) +AI가 투박한 Card를 만들지 못하게 하고, 이 코드를 복사하라고 지시하세요. +```tsx +// DO: 아주 작고 은은하게 떠 있는 스마트 필 +
+ + + 내용 텍스트 + +
+ +// DON'T: AI가 자주 실수하는 투박한 솔리드 카드 +
내용 텍스트
+``` + +### 5-2. 메인 입력창 (Ritual Input) +AI가 흔한 폼 인풋(`border rounded px-4`)을 만들지 못하게 하세요. +```tsx +// DO: 거대하고 얇고 투명한 의식적 텍스트 + + +// DON'T: 일반적인 대시보드 폼 + +``` + +### 5-3. 프리미엄 버튼 (Primary Action) +```tsx +// DO: 빛나는 투명 테두리와 쫀득한 햅틱 모션을 가진 버튼 + +``` + +### 5-4. 그라데이션 오버레이 (배경 어둡게 하기) +이미지 위에서 글씨가 잘 보이게 하려면 단순 `bg-black/50` 대신 깊이감 있는 그라데이션을 써야 합니다. +```tsx +// DO: 복합 그라데이션 (중앙은 비우고 테두리만 어둡게) +
+
+``` + +--- + +## 6. QA 체크리스트 (커밋 전 반드시 확인) + +1. **Overlap Check:** 브라우저 창을 최소 크기(모바일/작은 데스크탑)로 줄였을 때 위젯이나 텍스트가 겹치거나 잘리는 곳이 단 한 곳이라도 있는가? (있다면 즉시 수정) +2. **Clipping Check:** 리스트 내의 아이템에 마우스를 올렸을 때(hover:scale) 아이템의 상하좌우 테두리가 부모 컨테이너에 의해 잘려 나가는 현상이 없는가? +3. **Hierarchy Check:** 화면 내에서 가장 눈에 띄는 것이 "현재 사용자가 해야 할 단 하나의 액션"인가? 부가 정보가 너무 커서 메인 액션을 압도하지 않는가? +4. **Motion Check:** 화면 진입 시 모든 요소가 우아하고 부드럽게 등장하는가? 깜빡이거나 투박하게 나타나는 요소는 없는가? +5. **Glass Check:** 모든 팝오버, 시트, 위젯이 뒷 배경을 우아하게 투영(backdrop-blur)하고 있으며, 테두리가 지나치게 두껍지 않은가? diff --git a/package-lock.json b/package-lock.json index 9287e10..7d37025 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react-apple-signin-auth": "^1.1.2", "react-dom": "19.2.3", "react-facebook-login": "^4.1.1", + "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.11" }, "devDependencies": { @@ -6167,6 +6168,15 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", diff --git a/package.json b/package.json index 0ac2723..30cbbd3 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "react-apple-signin-auth": "^1.1.2", "react-dom": "19.2.3", "react-facebook-login": "^4.1.1", + "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.11" }, "devDependencies": { diff --git a/src/app/globals.css b/src/app/globals.css index d5d5112..eb30978 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@plugin "tailwindcss-animate"; @theme { /* Noto Sans 다국어 폰트 적용 (next/font/google 변수) */ @@ -81,6 +82,42 @@ body { } } +@keyframes fade-in-up { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.animate-fade-in-up { + animation: fade-in-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +.animate-fade-in { + animation: fade-in 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +.delay-150 { + animation-delay: 150ms; +} + +.delay-300 { + animation-delay: 300ms; +} + .scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; diff --git a/src/widgets/focus-dashboard/ui/AppAtmosphereEntryShell.tsx b/src/widgets/focus-dashboard/ui/AppAtmosphereEntryShell.tsx index 38f017c..8c4daca 100644 --- a/src/widgets/focus-dashboard/ui/AppAtmosphereEntryShell.tsx +++ b/src/widgets/focus-dashboard/ui/AppAtmosphereEntryShell.tsx @@ -5,15 +5,6 @@ import { getSceneCardPhotoUrl } from '@/entities/scene'; import { cn } from '@/shared/lib/cn'; import type { AtmosphereOption } from '../model/atmosphereEntry'; -const stageShellClass = - 'relative overflow-hidden rounded-[2.35rem] border border-white/12 bg-[linear-gradient(160deg,rgba(9,13,20,0.54)_0%,rgba(9,13,20,0.2)_52%,rgba(9,13,20,0.48)_100%)] shadow-[0_26px_90px_rgba(3,7,18,0.34)] backdrop-blur-[26px]'; -const fieldShellClass = - 'w-full rounded-[1.45rem] border border-white/12 bg-[linear-gradient(180deg,rgba(255,255,255,0.09)_0%,rgba(255,255,255,0.05)_100%)] px-5 py-4 text-white outline-none transition focus:border-white/24 focus:bg-white/[0.1]'; -const reviewDockClass = - 'group relative overflow-hidden rounded-[1.65rem] border border-white/12 bg-[linear-gradient(145deg,rgba(255,255,255,0.09)_0%,rgba(255,255,255,0.04)_100%)] px-5 py-4 backdrop-blur-xl transition hover:border-white/18 hover:bg-white/[0.1]'; -const primaryButtonClass = - 'inline-flex min-h-[3.65rem] items-center justify-center rounded-full border border-white/14 bg-white/[0.16] px-6 text-[15px] font-medium tracking-[-0.01em] text-white shadow-[0_10px_22px_rgba(5,10,20,0.24)] transition hover:bg-white/[0.2] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-50'; - interface AppAtmosphereEntryShellProps { canStart: boolean; durationDraft: string; @@ -25,9 +16,9 @@ interface AppAtmosphereEntryShellProps { goalInputRef: RefObject; goalPlaceholder: string; isStartingSession: boolean; - reviewEntry?: ReactNode; + topAccessory?: ReactNode; + errorAccessory?: ReactNode; selectedAtmosphere: AtmosphereOption; - sessionLookupError?: string | null; startButtonLabel: string; startButtonLoadingLabel: string; atmosphereOptions: AtmosphereOption[]; @@ -44,21 +35,17 @@ export const AppAtmosphereEntryShell = ({ canStart, durationDraft, durationHelper, - durationInputLabel, - durationPlaceholder, durationSuggestions, goalDraft, goalInputRef, goalPlaceholder, isStartingSession, - reviewEntry, + topAccessory, + errorAccessory, selectedAtmosphere, - sessionLookupError, startButtonLabel, startButtonLoadingLabel, atmosphereOptions, - atmosphereTitle, - atmosphereBody, onDurationChange, onGoalChange, onSelectAtmosphere, @@ -66,313 +53,144 @@ export const AppAtmosphereEntryShell = ({ onStartSession, }: AppAtmosphereEntryShellProps) => { return ( -
-
-
-
-
+
+ {/* Main Focus Entry Ritual */} +
+ {/* Inline Accessories (No overlap guarantee) */} +
+ {errorAccessory} + {!errorAccessory && topAccessory} +
-
-
-
-

- Focus Entry -

-

- 이번 세션을 -
- 어떤 온도로 열까요? -

-

- 목표는 한 줄이면 충분합니다. 걸릴 시간을 적고, 오늘의 atmosphere 하나만 고르면 - 바로 공간 안으로 들어갈 수 있어요. -

+
+

+ What will you focus on? +

+ onGoalChange(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + onStartSession(); + } + }} + placeholder={goalPlaceholder} + className="w-full bg-transparent text-center text-4xl font-light tracking-tight text-white outline-none placeholder:text-white/20 md:text-5xl lg:text-[4.5rem] lg:leading-[1.1]" + autoFocus + /> +
+ +
+
+
+
+ {durationSuggestions.map((minutes) => { + const isSelected = durationDraft === String(minutes); + return ( + + ); + })}
- -
-

- 선택한 Atmosphere -

-

- {selectedAtmosphere.name} -

-

- {selectedAtmosphere.soundLabel} -
- {selectedAtmosphere.caption} -

-
-
- -
-
- + +
-
-
-
-

- Atmosphere Library -

-

- {atmosphereTitle} -

-

- {atmosphereBody} -

-
-

- 총 {atmosphereOptions.length}개의 curated atmosphere + {/* Atmosphere Selection Dock */} +

+
+

+ Atmosphere

-
+
+ {atmosphereOptions.map((option) => { + const isSelected = option.id === selectedAtmosphere.id; -
- {atmosphereOptions.map((option) => { - const isSelected = option.id === selectedAtmosphere.id; - - return ( - - ); - })} + )} + + ); + })} +
- -

- 고른 atmosphere는 배경과 사운드가 함께 적용됩니다. 시간은 직접 적은 값을 우선 - 유지하고, 아직 손대지 않았다면 선택한 atmosphere의 기본 길이로 먼저 맞춰집니다. -

-
+
); }; diff --git a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx index 0827b0f..a4cf99c 100644 --- a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx +++ b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx @@ -8,7 +8,6 @@ import { usePlanTier } from '@/entities/plan'; import { getSceneById, SCENE_THEMES } from '@/entities/scene'; import { SOUND_PRESETS } from '@/entities/session'; import { PaywallSheetContent } from '@/features/paywall-sheet'; -import { PlanPill } from '@/features/plan-pill'; import { focusSessionApi, type FocusSession } from '@/features/focus-session/api/focusSessionApi'; import { useFocusStats, type ReviewCarryHint } from '@/features/stats'; import { copy } from '@/shared/i18n'; @@ -42,40 +41,37 @@ const DEFAULT_ATMOSPHERE = const entryCopy = { eyebrow: 'VibeRoom', - goalPlaceholder: '예: 제안서 첫 문단만 다듬기', - durationLabel: '예상 시간(분)', - durationPlaceholder: '예: 70', - durationHelper: '이 목표를 끝내는 데 걸릴 것 같은 시간을 적어요.', - startNow: '이 분위기로 들어가기', - startLoading: '입장 준비 중...', - atmosphereTitle: '어떤 분위기에서 들어갈까요?', + goalPlaceholder: 'e.g. Write the first draft', + durationLabel: 'Estimated Time', + durationPlaceholder: 'e.g. 70', + durationHelper: 'Set a realistic time to accomplish this goal.', + startNow: 'Begin Session', + startLoading: 'Entering...', + atmosphereTitle: 'Atmosphere', atmosphereBody: - '배경과 사운드는 하나의 atmosphere로 움직입니다. 지금 할 일의 온도에 맞는 분위기 하나만 고르면 바로 들어갈 수 있어요.', - loadFailed: '세션 상태를 불러오지 못했어요. 새로 시작은 계속 할 수 있어요.', + 'Background and sound play together. Choose an atmosphere to dive into deep focus.', + loadFailed: 'Failed to load session state. You can still start a new one.', reviewEyebrow: 'Weekly Review', - reviewTitle: '이번 주 review를 잠깐 보고 갈까요?', - reviewCta: '주간 review 보기', - reviewHelper: '다음 세션 전에 가볍게 보고 갈 수 있어요.', - reviewTitlePro: '나에게 잘 맞았던 흐름을 다시 보고 갈까요?', - reviewCtaPro: '나에게 맞는 흐름 보기', - reviewHelperPro: '가장 잘 맞았던 ritual과 carry-forward를 보고 돌아올 수 있어요.', - reviewReturnEyebrow: '방금 본 review 기준', - reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.', - reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.', - reviewReturnTitleClosure: '이번엔 어디서 닫을지 먼저 정해보세요.', - reviewReturnTitleStart: '이번 주는 시작 횟수 하나를 더 만드는 게 먼저예요.', - reviewReturnBodySteady: 'goal은 직접 정하되, 지금처럼 가볍게 들어가는 리듬을 유지해 보세요.', - reviewReturnBodySmaller: '길이를 늘리기보다, 더 작은 goal과 더 구체적인 첫 한 조각으로 시작하면 이어가기 쉬워져요.', - reviewReturnBodyClosure: '큰 흐름보다 지금 블록을 어디서 마무리할지 먼저 떠올리면 끝까지 가져가기 쉬워져요.', - reviewReturnBodyStart: '길이를 늘리기보다, 아주 작은 goal로 이번 주 첫 세션 하나를 더 여는 데 집중해 보세요.', - reviewReturnRitualLabel: '추천 atmosphere · 숲 · Forest Birds', + reviewTitle: 'Take a quick look at your weekly review?', + reviewCta: 'View Review', + reviewHelper: 'A brief look back before you start.', + reviewTitlePro: 'Revisit a flow that worked well for you?', + reviewCtaPro: 'View My Flow', + reviewHelperPro: 'Check your best rituals and carry-forwards.', + reviewReturnEyebrow: 'From your recent review', + reviewReturnTitleSteady: 'Keep the rhythm that worked well this week.', + reviewReturnTitleSmaller: 'Try setting a smaller goal this time.', + reviewReturnTitleClosure: 'Decide where to wrap up before you start.', + reviewReturnTitleStart: 'Your focus right now: just start one more session.', + reviewReturnBodySteady: 'Set your own goal, but maintain the light entry rhythm.', + reviewReturnBodySmaller: 'Instead of extending time, a smaller goal makes it easier to keep going.', + reviewReturnBodyClosure: 'Think about where to close the block first to finish strong.', + reviewReturnBodyStart: 'Just aim to open one more short session to build momentum.', + reviewReturnRitualLabel: 'Recommended Ritual · Forest · Forest Birds', paywallLead: 'Calm Session OS PRO', - paywallBody: 'Pro는 더 빠른 ritual과 더 깊은 review로 시작과 복귀를 가볍게 만듭니다.', + paywallBody: 'Pro enables faster rituals and deeper reviews for seamless entry and return.', }; -const goalCardClass = - 'w-full rounded-[2.2rem] border border-white/12 bg-[linear-gradient(160deg,rgba(9,13,20,0.52)_0%,rgba(9,13,20,0.24)_52%,rgba(9,13,20,0.5)_100%)] px-6 py-6 shadow-[0_26px_90px_rgba(3,7,18,0.34)] backdrop-blur-[24px] md:px-8 md:py-8'; - const reviewCarryCopyByHint: Record< ReviewCarryHint, { title: string; body: string } @@ -103,7 +99,7 @@ export const FocusDashboardWidget = () => { const searchParams = useSearchParams(); const { plan, isPro, setPlan } = usePlanTier(); const { sceneAssetMap } = useMediaCatalog(); - const { review, summary: weeklySummary } = useFocusStats(); + const { summary: weeklySummary } = useFocusStats(); const reviewEntryPreset = searchParams.get('entryPreset'); const reviewEntryPresetConfig = useMemo(() => { @@ -181,12 +177,9 @@ export const FocusDashboardWidget = () => { const reviewReturnRitualLabel = isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null; const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle; - const reviewTeaserSummary = isPro ? review.carryForward.keepDoing : review.snapshotSummary; - const reviewTeaserHelper = isPro ? entryCopy.reviewHelperPro : entryCopy.reviewHelper; - const reviewTeaserCta = isPro ? entryCopy.reviewCtaPro : entryCopy.reviewCta; const durationHelper = parsedDurationMinutes === null - ? '이 목표를 끝내는 데 걸릴 것 같은 시간을 분 단위로 적어주세요.' + ? 'Please enter the estimated duration in minutes.' : entryCopy.durationHelper; const hasCurrentSession = Boolean(currentSession); @@ -303,120 +296,133 @@ export const FocusDashboardWidget = () => { !isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn; return ( -
+
+ {/* Background Media */}
-
-
-
+ {/* Immersive Overlay Gradients */} +
+
-
-

- {entryCopy.eyebrow} -

- -
- -
-
- {isCheckingSession ? ( -
-

세션 상태를 불러오는 중이에요.

-
- ) : ( -
- {reviewReturnCopy ? ( -
-

- {entryCopy.reviewReturnEyebrow} -

-

- {reviewReturnCopy.title} -

-

- {reviewReturnCopy.body} -

- {reviewReturnRitualLabel ? ( -

{reviewReturnRitualLabel}

- ) : null} -
- ) : null} - - -
-
-

- {entryCopy.reviewEyebrow} -

-

- {reviewTeaserTitle} -

-

- {reviewTeaserSummary} -

-

{reviewTeaserHelper}

-
- - {reviewTeaserCta} - -
- - ) : undefined - } - selectedAtmosphere={selectedAtmosphere} - sessionLookupError={sessionLookupError} - startButtonLabel={entryCopy.startNow} - startButtonLoadingLabel={entryCopy.startLoading} - atmosphereOptions={ATMOSPHERE_OPTIONS} - atmosphereTitle={entryCopy.atmosphereTitle} - atmosphereBody={entryCopy.atmosphereBody} - onDurationChange={handleDurationChange} - onGoalChange={setGoalDraft} - onSelectAtmosphere={handleSelectAtmosphere} - onSelectDuration={handleSelectDuration} - onStartSession={() => { - void handleStartSession(); - }} - /> -
+ {/* Header */} +
+
+

+ {entryCopy.eyebrow} +

+ {plan === 'pro' && ( + + PRO + )}
+ {plan !== 'pro' && ( + + )} +
+ + {/* Main Content Area */} +
+ {isCheckingSession ? ( +
+

Loading session...

+
+ ) : ( +
+ +
+ + {entryCopy.reviewReturnEyebrow} +
+

+ {reviewReturnCopy.title} +

+

+ {reviewReturnCopy.body} +

+ {reviewReturnRitualLabel && ( +

+ {reviewReturnRitualLabel} +

+ )} +
+ ) : shouldShowWeeklyReviewTeaser ? ( + + + + {reviewTeaserTitle} + + + + ) : undefined + } + errorAccessory={ + sessionLookupError ? ( +
+ + + + {sessionLookupError} +
+ ) : undefined + } + selectedAtmosphere={selectedAtmosphere} + startButtonLabel={entryCopy.startNow} + startButtonLoadingLabel={entryCopy.startLoading} + atmosphereOptions={ATMOSPHERE_OPTIONS} + atmosphereTitle={entryCopy.atmosphereTitle} + atmosphereBody={entryCopy.atmosphereBody} + onDurationChange={handleDurationChange} + onGoalChange={setGoalDraft} + onSelectAtmosphere={handleSelectAtmosphere} + onSelectDuration={handleSelectDuration} + onStartSession={() => { + void handleStartSession(); + }} + /> +
+ )}
- {paywallSource ? ( -
+ {/* Paywall Overlay */} + {paywallSource && ( +
); };