feat(stats): pro personalized handoff 추가
This commit is contained in:
@@ -328,7 +328,10 @@ Away / Return이 끼어들기 전, 다음으로 예정된 축은 아래 두 가
|
|||||||
- `Weekly Review Entry Flow` Slice 2
|
- `Weekly Review Entry Flow` Slice 2
|
||||||
- `/stats` 마지막 CTA가 `/app?review=weekly&carryHint=...` handoff로 연결
|
- `/stats` 마지막 CTA가 `/app?review=weekly&carryHint=...` handoff로 연결
|
||||||
- `/app`은 review-aware return hint를 먼저 보여주고, goal 입력은 그대로 사용자가 결정한다
|
- `/app`은 review-aware return hint를 먼저 보여주고, goal 입력은 그대로 사용자가 결정한다
|
||||||
- 다음 구현은 Pro personalized handoff
|
- `Weekly Review Entry Flow` Slice 3
|
||||||
|
- Pro에서는 `/stats` carry-forward에 추천 ritual을 함께 보여준다
|
||||||
|
- `/stats` 마지막 CTA와 `/app` return hint가 더 구체적인 next-session handoff로 바뀐다
|
||||||
|
- 다음 구현은 `/space` secondary review teaser
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ Last Updated: 2026-03-14
|
|||||||
- `/stats` 마지막 CTA는 `/app?review=weekly&carryHint=...&entryPreset=forest-50-10`으로 연결된다
|
- `/stats` 마지막 CTA는 `/app?review=weekly&carryHint=...&entryPreset=forest-50-10`으로 연결된다
|
||||||
- `/app`은 이 query를 받아 hero 위에 review-aware return hint를 노출한다
|
- `/app`은 이 query를 받아 hero 위에 review-aware return hint를 노출한다
|
||||||
- goal과 microStep은 자동 입력하지 않고, 방향만 가볍게 제안한다
|
- goal과 microStep은 자동 입력하지 않고, 방향만 가볍게 제안한다
|
||||||
|
- Pro personalized handoff 3차 연결:
|
||||||
|
- Pro에서는 `/stats` carry-forward 섹션에 추천 ritual을 함께 보여준다
|
||||||
|
- `/stats` 마지막 CTA 카피가 generic start가 아니라 `가장 잘 맞은 ritual로 /app 돌아가기`로 바뀐다
|
||||||
|
- `/app` teaser와 review return hint도 Pro에서 더 구체적인 next-session handoff 톤으로 표시된다
|
||||||
- paywall / plan / landing 메시지 재정렬:
|
- paywall / plan / landing 메시지 재정렬:
|
||||||
- paywall 가치 포인트를 multi-queue, rituals, weekly review 중심으로 재작성
|
- paywall 가치 포인트를 multi-queue, rituals, weekly review 중심으로 재작성
|
||||||
- landing pricing에서 구현되지 않은 1:1 매칭 / 오픈 코워킹 / 팀 대시보드를 메인 판매 포인트에서 제거
|
- landing pricing에서 구현되지 않은 1:1 매칭 / 오픈 코워킹 / 팀 대시보드를 메인 판매 포인트에서 제거
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ Last Updated: 2026-03-14
|
|||||||
- `/stats` 마지막 CTA의 `/app` return handoff가 연결됐다.
|
- `/stats` 마지막 CTA의 `/app` return handoff가 연결됐다.
|
||||||
- carry-forward CTA는 `/app?review=weekly&carryHint=...`로 돌아온다.
|
- carry-forward CTA는 `/app?review=weekly&carryHint=...`로 돌아온다.
|
||||||
- `/app`은 review-aware return hint를 먼저 보여주되, goal은 사용자가 직접 입력하게 유지한다.
|
- `/app`은 review-aware return hint를 먼저 보여주되, goal은 사용자가 직접 입력하게 유지한다.
|
||||||
- 다음 구현은 Pro personalized handoff다.
|
- `Weekly Review Entry Flow`의 Pro personalized handoff까지 연결됐다.
|
||||||
|
- Pro에서는 `/stats` carry-forward에 추천 ritual을 함께 보여준다.
|
||||||
|
- `/stats` 마지막 CTA와 `/app` teaser / return hint가 더 구체적인 handoff 톤으로 바뀐다.
|
||||||
|
- 다음 구현은 `/space` complete 이후 secondary review teaser다.
|
||||||
- 유료화 포지셔닝을 `Calm Session OS`로 재정의했다.
|
- 유료화 포지셔닝을 `Calm Session OS`로 재정의했다.
|
||||||
- Free는 기본 집중 시작, Pro는 더 잘 이어가기라는 메시지로 정리했다.
|
- Free는 기본 집중 시작, Pro는 더 잘 이어가기라는 메시지로 정리했다.
|
||||||
- old `Scene Packs / Sound Packs / Profiles` 중심 카피를 `Daily plan / Rituals / Weekly review` 구조로 교체했다.
|
- old `Scene Packs / Sound Packs / Profiles` 중심 카피를 `Daily plan / Rituals / Weekly review` 구조로 교체했다.
|
||||||
|
|||||||
@@ -110,7 +110,8 @@
|
|||||||
- Slice 1 완료: `/app` hero 아래 low-emphasis weekly review teaser 추가
|
- Slice 1 완료: `/app` hero 아래 low-emphasis weekly review teaser 추가
|
||||||
- Slice 2 완료: `/stats` 마지막 CTA가 `/app?review=weekly&carryHint=...` handoff로 연결
|
- Slice 2 완료: `/stats` 마지막 CTA가 `/app?review=weekly&carryHint=...` handoff로 연결
|
||||||
- `/app`은 query를 받아 review-aware return hint를 먼저 보여준다
|
- `/app`은 query를 받아 review-aware return hint를 먼저 보여준다
|
||||||
- 다음 slice: Pro personalized handoff
|
- Slice 3 완료: Pro에서 추천 ritual과 더 구체적인 CTA / return hint가 연결된다
|
||||||
|
- 다음 slice: `/space` complete 이후 secondary review teaser
|
||||||
- 검증:
|
- 검증:
|
||||||
- `/app -> /stats -> /app` 실제 브라우저 플로우 확인
|
- `/app -> /stats -> /app` 실제 브라우저 플로우 확인
|
||||||
- hero와 teaser의 시각 우선순위 확인
|
- hero와 teaser의 시각 우선순위 확인
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export interface WeeklyReviewViewModel {
|
|||||||
completionQuality: WeeklyReviewSection;
|
completionQuality: WeeklyReviewSection;
|
||||||
carryForward: {
|
carryForward: {
|
||||||
hintKey: ReviewCarryHint;
|
hintKey: ReviewCarryHint;
|
||||||
|
presetId: string;
|
||||||
|
presetLabel: string;
|
||||||
keepDoing: string;
|
keepDoing: string;
|
||||||
tryNext: string;
|
tryNext: string;
|
||||||
ctaLabel: string;
|
ctaLabel: string;
|
||||||
@@ -176,6 +178,8 @@ const buildCarryForward = (summary: FocusStatsSummary): WeeklyReviewViewModel['c
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
hintKey,
|
hintKey,
|
||||||
|
presetId: 'forest-50-10',
|
||||||
|
presetLabel: 'Forest · 50/10 · Forest Birds',
|
||||||
keepDoing,
|
keepDoing,
|
||||||
tryNext,
|
tryNext,
|
||||||
ctaLabel: copy.stats.reviewCarryCta,
|
ctaLabel: copy.stats.reviewCarryCta,
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ export const app = {
|
|||||||
reviewCarryTryClosure: '시작은 있었지만 마무리가 약했어요. 다음 주에는 완료 직전에 다른 블록으로 넘어가지 않는 흐름을 한 번 만들어 보세요.',
|
reviewCarryTryClosure: '시작은 있었지만 마무리가 약했어요. 다음 주에는 완료 직전에 다른 블록으로 넘어가지 않는 흐름을 한 번 만들어 보세요.',
|
||||||
reviewCarryTryStart: '시작 횟수가 적었어요. 다음 주에는 길이를 늘리기보다 첫 세션을 한 번 더 여는 것에 집중해 보세요.',
|
reviewCarryTryStart: '시작 횟수가 적었어요. 다음 주에는 길이를 늘리기보다 첫 세션을 한 번 더 여는 것에 집중해 보세요.',
|
||||||
reviewCarryCta: '이 흐름으로 다음 세션 시작',
|
reviewCarryCta: '이 흐름으로 다음 세션 시작',
|
||||||
|
reviewCarryCtaPro: '가장 잘 맞은 ritual로 /app 돌아가기',
|
||||||
reviewCarryKeepTitle: '다음 주에 유지할 것',
|
reviewCarryKeepTitle: '다음 주에 유지할 것',
|
||||||
reviewCarryTryTitle: '다음 주에 바꿔볼 것',
|
reviewCarryTryTitle: '다음 주에 바꿔볼 것',
|
||||||
|
reviewCarryPresetLabel: '추천 ritual',
|
||||||
today: '오늘',
|
today: '오늘',
|
||||||
last7Days: '최근 7일',
|
last7Days: '최근 7일',
|
||||||
chartTitle: '집중 흐름 그래프',
|
chartTitle: '집중 흐름 그래프',
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ const entryCopy = {
|
|||||||
reviewTitle: '이번 주 review를 잠깐 보고 갈까요?',
|
reviewTitle: '이번 주 review를 잠깐 보고 갈까요?',
|
||||||
reviewCta: '주간 review 보기',
|
reviewCta: '주간 review 보기',
|
||||||
reviewHelper: '다음 세션 전에 가볍게 보고 갈 수 있어요.',
|
reviewHelper: '다음 세션 전에 가볍게 보고 갈 수 있어요.',
|
||||||
|
reviewTitlePro: '나에게 잘 맞았던 흐름을 다시 보고 갈까요?',
|
||||||
|
reviewCtaPro: '나에게 맞는 흐름 보기',
|
||||||
|
reviewHelperPro: '가장 잘 맞았던 ritual과 carry-forward를 보고 돌아올 수 있어요.',
|
||||||
reviewReturnEyebrow: '방금 본 review 기준',
|
reviewReturnEyebrow: '방금 본 review 기준',
|
||||||
reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.',
|
reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.',
|
||||||
reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.',
|
reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.',
|
||||||
@@ -148,7 +151,11 @@ export const FocusDashboardWidget = () => {
|
|||||||
const reviewReturnCopy =
|
const reviewReturnCopy =
|
||||||
normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null;
|
normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null;
|
||||||
const reviewReturnRitualLabel =
|
const reviewReturnRitualLabel =
|
||||||
reviewEntryPreset === 'forest-50-10' ? entryCopy.reviewReturnRitualLabel : null;
|
isPro && reviewEntryPreset === 'forest-50-10' ? entryCopy.reviewReturnRitualLabel : 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;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -417,15 +424,15 @@ export const FocusDashboardWidget = () => {
|
|||||||
{entryCopy.reviewEyebrow}
|
{entryCopy.reviewEyebrow}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-[1rem] font-medium tracking-[-0.02em] text-white/88">
|
<p className="mt-2 text-[1rem] font-medium tracking-[-0.02em] text-white/88">
|
||||||
{entryCopy.reviewTitle}
|
{reviewTeaserTitle}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 max-w-[34rem] text-[13px] leading-[1.6] text-white/62">
|
<p className="mt-2 max-w-[34rem] text-[13px] leading-[1.6] text-white/62">
|
||||||
{review.snapshotSummary}
|
{reviewTeaserSummary}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-[12px] text-white/44">{entryCopy.reviewHelper}</p>
|
<p className="mt-2 text-[12px] text-white/44">{reviewTeaserHelper}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-flex shrink-0 items-center text-[12px] font-medium tracking-[0.04em] text-white/74">
|
<span className="inline-flex shrink-0 items-center text-[12px] font-medium tracking-[0.04em] text-white/74">
|
||||||
{entryCopy.reviewCta}
|
{reviewTeaserCta}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { usePlanTier } from '@/entities/plan';
|
||||||
import { useFocusStats } from '@/features/stats';
|
import { useFocusStats } from '@/features/stats';
|
||||||
import { copy } from '@/shared/i18n';
|
import { copy } from '@/shared/i18n';
|
||||||
import { cn } from '@/shared/lib/cn';
|
import { cn } from '@/shared/lib/cn';
|
||||||
@@ -79,7 +80,9 @@ const ReviewSection = ({
|
|||||||
|
|
||||||
export const StatsOverviewWidget = () => {
|
export const StatsOverviewWidget = () => {
|
||||||
const { stats } = copy;
|
const { stats } = copy;
|
||||||
|
const { isPro } = usePlanTier();
|
||||||
const { review, isLoading, error, source, refetch } = useFocusStats();
|
const { review, isLoading, error, source, refetch } = useFocusStats();
|
||||||
|
const carryForwardCtaLabel = isPro ? stats.reviewCarryCtaPro : review.carryForward.ctaLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[radial-gradient(circle_at_14%_0%,rgba(198,219,244,0.52),transparent_42%),radial-gradient(circle_at_88%_12%,rgba(232,240,249,0.8),transparent_34%),linear-gradient(180deg,#f8fbff_0%,#f2f7fc_46%,#edf3f9_100%)] text-brand-dark">
|
<div className="min-h-screen bg-[radial-gradient(circle_at_14%_0%,rgba(198,219,244,0.52),transparent_42%),radial-gradient(circle_at_88%_12%,rgba(232,240,249,0.8),transparent_34%),linear-gradient(180deg,#f8fbff_0%,#f2f7fc_46%,#edf3f9_100%)] text-brand-dark">
|
||||||
@@ -193,11 +196,22 @@ export const StatsOverviewWidget = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isPro ? (
|
||||||
|
<div className="mt-5 rounded-2xl border border-brand-dark/8 bg-white/56 px-4 py-3">
|
||||||
|
<p className="text-[11px] font-medium tracking-[0.08em] text-brand-dark/42">
|
||||||
|
{stats.reviewCarryPresetLabel}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[13px] font-medium text-brand-dark/82">
|
||||||
|
{review.carryForward.presetLabel}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={review.carryForward.ctaHref}
|
href={review.carryForward.ctaHref}
|
||||||
className="mt-6 inline-flex rounded-full border border-brand-dark/14 bg-brand-dark px-4 py-2.5 text-[12px] font-medium tracking-[0.04em] text-white transition hover:bg-brand-dark/92"
|
className="mt-6 inline-flex rounded-full border border-brand-dark/14 bg-brand-dark px-4 py-2.5 text-[12px] font-medium tracking-[0.04em] text-white transition hover:bg-brand-dark/92"
|
||||||
>
|
>
|
||||||
{review.carryForward.ctaLabel}
|
{carryForwardCtaLabel}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user