From de95505d2fa2a7af3878c0369144ff53340e6359 Mon Sep 17 00:00:00 2001 From: corpi Date: Sat, 14 Mar 2026 20:00:38 +0900 Subject: [PATCH] =?UTF-8?q?feat(space):=20secondary=20weekly=20review=20te?= =?UTF-8?q?aser=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/12_core_loop_execution_roadmap.md | 5 ++- docs/90_current_state.md | 4 ++ docs/session_brief.md | 5 ++- docs/work.md | 3 +- src/shared/i18n/messages/space.ts | 7 +++ .../ui/SpaceSetupDrawerWidget.tsx | 41 +++++++++++++++++ .../ui/SpaceWorkspaceWidget.tsx | 45 ++++++++++++++++++- 7 files changed, 105 insertions(+), 5 deletions(-) diff --git a/docs/12_core_loop_execution_roadmap.md b/docs/12_core_loop_execution_roadmap.md index d877c12..5ac92a4 100644 --- a/docs/12_core_loop_execution_roadmap.md +++ b/docs/12_core_loop_execution_roadmap.md @@ -331,7 +331,10 @@ Away / Return이 끼어들기 전, 다음으로 예정된 축은 아래 두 가 - `Weekly Review Entry Flow` Slice 3 - Pro에서는 `/stats` carry-forward에 추천 ritual을 함께 보여준다 - `/stats` 마지막 CTA와 `/app` return hint가 더 구체적인 next-session handoff로 바뀐다 - - 다음 구현은 `/space` secondary review teaser +- `Weekly Review Entry Flow` Slice 4 + - `/space`에서 goal complete로 setup 상태로 돌아온 직후에만 secondary review teaser가 보인다 + - full review 강제 이동 없이 작은 후행 경로로 `/stats`를 연다 + - 다음 구현은 weekly review의 실제 recovery 집계 연결이다 --- diff --git a/docs/90_current_state.md b/docs/90_current_state.md index 9cc55a8..1e0b9f1 100644 --- a/docs/90_current_state.md +++ b/docs/90_current_state.md @@ -125,6 +125,10 @@ Last Updated: 2026-03-14 - Pro에서는 `/stats` carry-forward 섹션에 추천 ritual을 함께 보여준다 - `/stats` 마지막 CTA 카피가 generic start가 아니라 `가장 잘 맞은 ritual로 /app 돌아가기`로 바뀐다 - `/app` teaser와 review return hint도 Pro에서 더 구체적인 next-session handoff 톤으로 표시된다 +- `/space` secondary review teaser 4차 연결: + - goal complete로 setup 상태로 돌아왔을 때만 setup drawer 아래에 low-emphasis review teaser가 보인다 + - teaser는 `주간 review 보기`로 `/stats?review=weekly&origin=space-complete`를 연다 + - 다시 시작하거나 dismiss하면 사라지며, live execution 중에는 보이지 않는다 - 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 445f779..455e797 100644 --- a/docs/session_brief.md +++ b/docs/session_brief.md @@ -92,7 +92,10 @@ Last Updated: 2026-03-14 - `Weekly Review Entry Flow`의 Pro personalized handoff까지 연결됐다. - Pro에서는 `/stats` carry-forward에 추천 ritual을 함께 보여준다. - `/stats` 마지막 CTA와 `/app` teaser / return hint가 더 구체적인 handoff 톤으로 바뀐다. - - 다음 구현은 `/space` complete 이후 secondary review teaser다. +- `/space` complete 이후 secondary review teaser까지 연결됐다. + - goal complete로 setup 상태로 돌아왔을 때만 setup drawer 아래에 작은 review teaser가 보인다. + - full review 강제 이동 없이 `/stats`를 여는 secondary entry로만 동작한다. + - 다음 구현은 weekly review의 실제 recovery 집계 연결이다. - 유료화 포지셔닝을 `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 3341bed..a053e1d 100644 --- a/docs/work.md +++ b/docs/work.md @@ -111,7 +111,8 @@ - Slice 2 완료: `/stats` 마지막 CTA가 `/app?review=weekly&carryHint=...` handoff로 연결 - `/app`은 query를 받아 review-aware return hint를 먼저 보여준다 - Slice 3 완료: Pro에서 추천 ritual과 더 구체적인 CTA / return hint가 연결된다 - - 다음 slice: `/space` complete 이후 secondary review teaser + - Slice 4 완료: `/space` complete 이후 setup drawer 아래에 secondary review teaser가 노출된다 + - 다음 작업: recovery browser QA 후 weekly review 실제 recovery 집계 연결 - 검증: - `/app -> /stats -> /app` 실제 브라우저 플로우 확인 - hero와 teaser의 시각 우선순위 확인 diff --git a/src/shared/i18n/messages/space.ts b/src/shared/i18n/messages/space.ts index 3bc99aa..faae62e 100644 --- a/src/shared/i18n/messages/space.ts +++ b/src/shared/i18n/messages/space.ts @@ -16,6 +16,13 @@ export const space = { sceneLabel: '배경', timerLabel: '타이머', soundLabel: '사운드', + reviewTeaserEyebrow: 'Weekly Review', + reviewTeaserTitle: '방금 끝낸 흐름까지 review에 담아둘까요?', + reviewTeaserTitlePro: '방금 끝낸 흐름까지 포함해 이번 주 리듬을 다시 볼까요?', + reviewTeaserHelper: '지금은 바로 다시 시작해도 괜찮고, 원하면 주간 review를 잠깐 보고 갈 수 있어요.', + reviewTeaserHelperPro: '방금 마친 흐름과 가장 잘 맞는 ritual을 같이 보고 다음 세션으로 이어갈 수 있어요.', + reviewTeaserCta: '주간 review 보기', + reviewTeaserDismiss: '나중에', readyHint: '목표를 적으면 시작할 수 있어요.', openFocusScreen: '실행 화면 열기', }, diff --git a/src/widgets/space-setup-drawer/ui/SpaceSetupDrawerWidget.tsx b/src/widgets/space-setup-drawer/ui/SpaceSetupDrawerWidget.tsx index 2c7d356..3c4de16 100644 --- a/src/widgets/space-setup-drawer/ui/SpaceSetupDrawerWidget.tsx +++ b/src/widgets/space-setup-drawer/ui/SpaceSetupDrawerWidget.tsx @@ -1,5 +1,6 @@ 'use client'; +import Link from 'next/link'; import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react'; import type { SceneAssetMap } from '@/entities/media'; import type { SceneTheme } from '@/entities/scene'; @@ -40,6 +41,13 @@ interface SpaceSetupDrawerWidgetProps { onResume: () => void; onStartFresh: () => void; }; + reviewTeaser?: { + title: string; + summary: string; + ctaHref: string; + ctaLabel: string; + onDismiss: () => void; + }; } interface SummaryChipProps { @@ -88,6 +96,7 @@ export const SpaceSetupDrawerWidget = ({ onGoalChipSelect, onStart, resumeHint, + reviewTeaser, }: SpaceSetupDrawerWidgetProps) => { const { setup } = copy.space; const [openPopover, setOpenPopover] = useState(null); @@ -309,6 +318,38 @@ export const SpaceSetupDrawerWidget = ({ + + {reviewTeaser ? ( +
+
+
+

+ {setup.reviewTeaserEyebrow} +

+

+ {reviewTeaser.title} +

+

+ {reviewTeaser.summary} +

+
+ +
+ + + {reviewTeaser.ctaLabel} + +
+ ) : null} ); diff --git a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx index 82f389b..f97e6ed 100644 --- a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx +++ b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx @@ -6,14 +6,17 @@ import { preloadAssetImage, useMediaCatalog, } from "@/entities/media"; +import { usePlanTier } from "@/entities/plan"; import { getSceneById, SCENE_THEMES } from "@/entities/scene"; import { GOAL_CHIPS, SOUND_PRESETS, useThoughtInbox } from "@/entities/session"; import { useFocusSessionEngine } from "@/features/focus-session"; +import { useFocusStats } from "@/features/stats"; import { useSoundPlayback, useSoundPresetSelection, } from "@/features/sound-preset"; import { useHudStatusLine } from "@/shared/lib/useHudStatusLine"; +import { copy } from "@/shared/i18n"; import { SpaceFocusHudWidget } from "@/widgets/space-focus-hud"; import { SpaceSetupDrawerWidget } from "@/widgets/space-setup-drawer"; import { SpaceToolsDockWidget } from "@/widgets/space-tools-dock"; @@ -63,6 +66,8 @@ export const SpaceWorkspaceWidget = () => { usedFallbackManifest, hasResolvedManifest, } = useMediaCatalog(); + const { isPro } = usePlanTier(); + const { review, summary: weeklySummary } = useFocusStats(); const initialSceneId = useMemo( () => resolveInitialSceneId(sceneQuery, undefined), @@ -97,6 +102,7 @@ export const SpaceWorkspaceWidget = () => { >("paused"); const [pendingSessionEntryPoint, setPendingSessionEntryPoint] = useState("space-setup"); + const [showReviewTeaserAfterComplete, setShowReviewTeaserAfterComplete] = useState(false); const { selectedPresetId, @@ -208,6 +214,27 @@ export const SpaceWorkspaceWidget = () => { isBootstrapping, syncCurrentSession, }); + const hasEnoughWeeklyData = + weeklySummary.last7Days.startedSessions >= 3 && + (weeklySummary.last7Days.completedSessions >= 2 || + review.recoveryQuality.availability === "ready"); + const shouldShowSecondaryReviewTeaser = + workspaceMode === "setup" && + showReviewTeaserAfterComplete && + hasEnoughWeeklyData; + const secondaryReviewTeaser = shouldShowSecondaryReviewTeaser + ? { + title: isPro + ? copy.space.setup.reviewTeaserTitlePro + : copy.space.setup.reviewTeaserTitle, + summary: isPro + ? review.carryForward.keepDoing + : copy.space.setup.reviewTeaserHelper, + ctaHref: "/stats?review=weekly&origin=space-complete", + ctaLabel: copy.space.setup.reviewTeaserCta, + onDismiss: () => setShowReviewTeaserAfterComplete(false), + } + : undefined; useEffect(() => { if (!isBootstrapping && !currentSession && !hasQueryOverrides) { @@ -270,12 +297,17 @@ export const SpaceWorkspaceWidget = () => { } onGoalChange={selection.handleGoalChange} onGoalChipSelect={selection.handleGoalChipSelect} - onStart={controls.handleSetupFocusOpen} + onStart={() => { + setShowReviewTeaserAfterComplete(false); + controls.handleSetupFocusOpen(); + }} + reviewTeaser={secondaryReviewTeaser} resumeHint={ selection.showResumePrompt && selection.resumeGoal ? { goal: selection.resumeGoal, onResume: () => { + setShowReviewTeaserAfterComplete(false); selection.setGoalInput(selection.resumeGoal); selection.setSelectedGoalId(null); selection.setShowResumePrompt(false); @@ -285,6 +317,7 @@ export const SpaceWorkspaceWidget = () => { ); }, onStartFresh: () => { + setShowReviewTeaserAfterComplete(false); selection.setGoalInput(""); selection.setSelectedGoalId(null); selection.setShowResumePrompt(false); @@ -319,7 +352,15 @@ export const SpaceWorkspaceWidget = () => { onDismissReturnPrompt={awayReturnRecovery.dismissReturnPrompt} onStatusMessage={pushStatusLine} onIntentUpdate={controls.handleIntentUpdate} - onGoalFinish={controls.handleGoalComplete} + onGoalFinish={async () => { + const didFinish = await controls.handleGoalComplete(); + + if (didFinish) { + setShowReviewTeaserAfterComplete(true); + } + + return didFinish; + }} onGoalUpdate={controls.handleGoalAdvance} /> ) : null}