feat(space): secondary weekly review teaser 추가

This commit is contained in:
2026-03-14 20:00:38 +09:00
parent 5d3a5ac8ac
commit de95505d2f
7 changed files with 105 additions and 5 deletions

View File

@@ -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<SelectionPopover | null>(null);
@@ -309,6 +318,38 @@ export const SpaceSetupDrawerWidget = ({
</Button>
</div>
</form>
{reviewTeaser ? (
<div className="mt-3 rounded-[1.25rem] border border-white/10 bg-black/16 px-3.5 py-3 backdrop-blur-md">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-[10px] uppercase tracking-[0.16em] text-white/42">
{setup.reviewTeaserEyebrow}
</p>
<p className="mt-1 text-[13px] font-medium leading-[1.5] text-white/88">
{reviewTeaser.title}
</p>
<p className="mt-1 text-[11px] leading-[1.55] text-white/56">
{reviewTeaser.summary}
</p>
</div>
<button
type="button"
onClick={reviewTeaser.onDismiss}
className="shrink-0 rounded-full border border-white/12 bg-white/[0.04] px-2 py-1 text-[10px] text-white/62 transition hover:bg-white/[0.09] hover:text-white/84"
>
{setup.reviewTeaserDismiss}
</button>
</div>
<Link
href={reviewTeaser.ctaHref}
className="mt-3 inline-flex items-center rounded-full border border-white/12 bg-white/[0.06] px-3 py-1.5 text-[11px] font-medium text-white/82 transition hover:bg-white/[0.1] hover:text-white"
>
{reviewTeaser.ctaLabel}
</Link>
</div>
) : null}
</div>
</section>
);

View File

@@ -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<SessionEntryPoint>("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}