diff --git a/docs/12_core_loop_execution_roadmap.md b/docs/12_core_loop_execution_roadmap.md index 197c22d..69c8c31 100644 --- a/docs/12_core_loop_execution_roadmap.md +++ b/docs/12_core_loop_execution_roadmap.md @@ -325,7 +325,10 @@ Away / Return이 끼어들기 전, 다음으로 예정된 축은 아래 두 가 - `Weekly Review Entry Flow` Slice 1 - `/app` hero 아래 low-emphasis weekly review teaser 추가 - 충분한 최근 7일 데이터가 있을 때만 `/stats` primary entry를 노출 - - 다음 구현은 `/stats -> /app` return handoff +- `Weekly Review Entry Flow` Slice 2 + - `/stats` 마지막 CTA가 `/app?review=weekly&carryHint=...` handoff로 연결 + - `/app`은 review-aware return hint를 먼저 보여주고, goal 입력은 그대로 사용자가 결정한다 + - 다음 구현은 Pro personalized handoff --- diff --git a/docs/90_current_state.md b/docs/90_current_state.md index 8e0f04a..f9fa614 100644 --- a/docs/90_current_state.md +++ b/docs/90_current_state.md @@ -117,7 +117,10 @@ Last Updated: 2026-03-14 - `/app -> /stats` primary entry의 1차 연결: - current session이 없고 최근 7일 데이터가 충분할 때 `/app` hero 아래에 low-emphasis `Weekly Review` teaser를 노출한다 - teaser는 `/stats`로 연결되며, hero CTA보다 한 단계 아래 시각 우선순위를 유지한다 - - `/stats -> /app` return handoff는 다음 slice로 남겨둔다 +- `/stats -> /app` handoff의 2차 연결: + - `/stats` 마지막 CTA는 `/app?review=weekly&carryHint=...&entryPreset=forest-50-10`으로 연결된다 + - `/app`은 이 query를 받아 hero 위에 review-aware return hint를 노출한다 + - goal과 microStep은 자동 입력하지 않고, 방향만 가볍게 제안한다 - paywall / plan / landing 메시지 재정렬: - paywall 가치 포인트를 multi-queue, rituals, weekly review 중심으로 재작성 - landing pricing에서 구현되지 않은 1:1 매칭 / 오픈 코워킹 / 팀 대시보드를 메인 판매 포인트에서 제거 diff --git a/docs/session_brief.md b/docs/session_brief.md index a5ee0ab..78ac0bf 100644 --- a/docs/session_brief.md +++ b/docs/session_brief.md @@ -86,7 +86,10 @@ Last Updated: 2026-03-14 - `/app`에서 `/stats`로 들어가는 primary path 1차가 생겼다. - current session이 없고 최근 7일 데이터가 충분하면 hero 아래에 weekly review teaser가 보인다. - teaser는 `/stats`로 이동시키되, main start CTA보다 낮은 강조로 유지한다. - - 다음 구현은 `/stats` 마지막 CTA의 `/app` return handoff다. +- `/stats` 마지막 CTA의 `/app` return handoff가 연결됐다. + - carry-forward CTA는 `/app?review=weekly&carryHint=...`로 돌아온다. + - `/app`은 review-aware return hint를 먼저 보여주되, goal은 사용자가 직접 입력하게 유지한다. + - 다음 구현은 Pro personalized handoff다. - 유료화 포지셔닝을 `Calm Session OS`로 재정의했다. - Free는 기본 집중 시작, Pro는 더 잘 이어가기라는 메시지로 정리했다. - old `Scene Packs / Sound Packs / Profiles` 중심 카피를 `Daily plan / Rituals / Weekly review` 구조로 교체했다. diff --git a/docs/work.md b/docs/work.md index f4b8911..c2c6c2a 100644 --- a/docs/work.md +++ b/docs/work.md @@ -108,8 +108,9 @@ - review가 읽고 끝나는 페이지가 아니라 next-session ritual처럼 동작한다. - 진행 상태: - Slice 1 완료: `/app` hero 아래 low-emphasis weekly review teaser 추가 - - 다음 slice: `/stats` 마지막 CTA의 `/app` return handoff - - 그 다음 slice: `/app` review-aware return state + - Slice 2 완료: `/stats` 마지막 CTA가 `/app?review=weekly&carryHint=...` handoff로 연결 + - `/app`은 query를 받아 review-aware return hint를 먼저 보여준다 + - 다음 slice: Pro personalized handoff - 검증: - `/app -> /stats -> /app` 실제 브라우저 플로우 확인 - hero와 teaser의 시각 우선순위 확인 diff --git a/src/features/stats/model/useFocusStats.ts b/src/features/stats/model/useFocusStats.ts index 3971d41..a068017 100644 --- a/src/features/stats/model/useFocusStats.ts +++ b/src/features/stats/model/useFocusStats.ts @@ -68,6 +68,8 @@ export interface WeeklyReviewSection { note?: string; } +export type ReviewCarryHint = 'smaller' | 'closure' | 'start' | 'steady'; + export interface WeeklyReviewViewModel { periodLabel: string; snapshotTitle: string; @@ -77,6 +79,7 @@ export interface WeeklyReviewViewModel { recoveryQuality: WeeklyReviewSection; completionQuality: WeeklyReviewSection; carryForward: { + hintKey: ReviewCarryHint; keepDoing: string; tryNext: string; ctaLabel: string; @@ -151,21 +154,32 @@ const buildCarryForward = (summary: FocusStatsSummary): WeeklyReviewViewModel['c ? copy.stats.reviewCarryKeep(summary.last7Days.bestDayLabel) : copy.stats.reviewCarryKeepGeneric; + let hintKey: ReviewCarryHint = 'steady'; let tryNext: string = copy.stats.reviewCarryTryDefault; if (summary.last7Days.carriedOverCount >= 2) { + hintKey = 'smaller'; tryNext = copy.stats.reviewCarryTrySmaller; } else if (completionRate < 0.45) { + hintKey = 'closure'; tryNext = copy.stats.reviewCarryTryClosure; } else if (summary.last7Days.startedSessions <= 3) { + hintKey = 'start'; tryNext = copy.stats.reviewCarryTryStart; } + const params = new URLSearchParams({ + review: 'weekly', + carryHint: hintKey, + entryPreset: 'forest-50-10', + }); + return { + hintKey, keepDoing, tryNext, ctaLabel: copy.stats.reviewCarryCta, - ctaHref: '/app', + ctaHref: `/app?${params.toString()}`, }; }; diff --git a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx index 233b019..ee39df4 100644 --- a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx +++ b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { useEffect, useMemo, useRef, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { useMediaCatalog, getSceneStageBackgroundStyle } from '@/entities/media'; import { usePlanTier } from '@/entities/plan'; import { getSceneById, SCENE_THEMES } from '@/entities/scene'; @@ -10,7 +10,7 @@ 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 } from '@/features/stats'; +import { useFocusStats, type ReviewCarryHint } from '@/features/stats'; import { copy } from '@/shared/i18n'; import { cn } from '@/shared/lib/cn'; @@ -42,6 +42,16 @@ const entryCopy = { reviewTitle: '이번 주 review를 잠깐 보고 갈까요?', reviewCta: '주간 review 보기', reviewHelper: '다음 세션 전에 가볍게 보고 갈 수 있어요.', + reviewReturnEyebrow: '방금 본 review 기준', + reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.', + reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.', + reviewReturnTitleClosure: '이번엔 어디서 닫을지 먼저 정해보세요.', + reviewReturnTitleStart: '이번 주는 시작 횟수 하나를 더 만드는 게 먼저예요.', + reviewReturnBodySteady: 'goal은 직접 정하되, 지금처럼 가볍게 들어가는 리듬을 유지해 보세요.', + reviewReturnBodySmaller: '길이를 늘리기보다, 더 작은 goal과 더 구체적인 첫 한 조각으로 시작하면 이어가기 쉬워져요.', + reviewReturnBodyClosure: '큰 흐름보다 지금 블록을 어디서 마무리할지 먼저 떠올리면 끝까지 가져가기 쉬워져요.', + reviewReturnBodyStart: '길이를 늘리기보다, 아주 작은 goal로 이번 주 첫 세션 하나를 더 여는 데 집중해 보세요.', + reviewReturnRitualLabel: '추천 ritual · 숲 · 50/10 · Forest Birds', paywallLead: 'Calm Session OS PRO', paywallBody: 'Pro는 더 빠른 ritual과 더 깊은 review로 시작과 복귀를 가볍게 만듭니다.', }; @@ -67,8 +77,31 @@ const resolveSoundLabel = (soundPresetId?: string | null) => { return SOUND_PRESETS.find((preset) => preset.id === soundPresetId)?.label ?? 'Silent'; }; +const reviewCarryCopyByHint: Record< + ReviewCarryHint, + { title: string; body: string } +> = { + steady: { + title: entryCopy.reviewReturnTitleSteady, + body: entryCopy.reviewReturnBodySteady, + }, + smaller: { + title: entryCopy.reviewReturnTitleSmaller, + body: entryCopy.reviewReturnBodySmaller, + }, + closure: { + title: entryCopy.reviewReturnTitleClosure, + body: entryCopy.reviewReturnBodyClosure, + }, + start: { + title: entryCopy.reviewReturnTitleStart, + body: entryCopy.reviewReturnBodyStart, + }, +}; + export const FocusDashboardWidget = () => { const router = useRouter(); + const searchParams = useSearchParams(); const { plan, isPro, setPlan } = usePlanTier(); const { sceneAssetMap } = useMediaCatalog(); const { review, summary: weeklySummary } = useFocusStats(); @@ -100,6 +133,22 @@ export const FocusDashboardWidget = () => { weeklySummary.last7Days.startedSessions >= 3 && (weeklySummary.last7Days.completedSessions >= 2 || review.recoveryQuality.availability === 'ready'); + const reviewSource = searchParams.get('review'); + const reviewCarryHint = searchParams.get('carryHint'); + const reviewEntryPreset = searchParams.get('entryPreset'); + const normalizedReviewCarryHint: ReviewCarryHint | null = + reviewCarryHint === 'steady' || + reviewCarryHint === 'smaller' || + reviewCarryHint === 'closure' || + reviewCarryHint === 'start' + ? reviewCarryHint + : null; + const isReviewReturn = + reviewSource === 'weekly' && normalizedReviewCarryHint !== null; + const reviewReturnCopy = + normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null; + const reviewReturnRitualLabel = + reviewEntryPreset === 'forest-50-10' ? entryCopy.reviewReturnRitualLabel : null; useEffect(() => { let cancelled = false; @@ -178,7 +227,7 @@ export const FocusDashboardWidget = () => { }; const shouldShowWeeklyReviewTeaser = - !isCheckingSession && !currentSession && hasEnoughWeeklyData; + !isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn; return (
@@ -241,6 +290,23 @@ export const FocusDashboardWidget = () => {
) : (
+ {reviewReturnCopy ? ( +
+

+ {entryCopy.reviewReturnEyebrow} +

+

+ {reviewReturnCopy.title} +

+

+ {reviewReturnCopy.body} +

+ {reviewReturnRitualLabel ? ( +

{reviewReturnRitualLabel}

+ ) : null} +
+ ) : null} +