fix(app): premium entry 조정과 duration 입력 버그 수정

This commit is contained in:
2026-03-16 14:35:26 +09:00
parent 6b25a18d5a
commit c63ddc4e98
6 changed files with 235 additions and 315 deletions

View File

@@ -9,11 +9,6 @@ import { useFocusStats } from '@/features/stats';
import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
const panelClass =
'relative overflow-hidden rounded-[2rem] border border-white/10 bg-[linear-gradient(160deg,rgba(8,12,18,0.46)_0%,rgba(8,12,18,0.2)_58%,rgba(8,12,18,0.52)_100%)] shadow-[0_24px_80px_rgba(3,7,18,0.28)] backdrop-blur-[24px]';
const innerTileClass =
'rounded-[1.4rem] border border-white/10 bg-[linear-gradient(145deg,rgba(255,255,255,0.08)_0%,rgba(255,255,255,0.04)_100%)] px-4 py-4 backdrop-blur-xl';
const DEFAULT_STATS_SCENE_ID = getSceneById('forest')?.id ?? SCENE_THEMES[0].id;
const reviewStageSceneByPreset = (presetId: string) => {
@@ -24,138 +19,6 @@ const reviewStageSceneByPreset = (presetId: string) => {
return getSceneById(DEFAULT_STATS_SCENE_ID) ?? SCENE_THEMES[0];
};
const AccessoryPill = ({
label,
subtle = false,
}: {
label: string;
subtle?: boolean;
}) => {
return (
<span
className={cn(
'inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.2em] backdrop-blur-md',
subtle
? 'border-white/8 bg-white/[0.05] text-white/46'
: 'border-white/12 bg-white/[0.07] text-white/64',
)}
>
<span className="h-1.5 w-1.5 rounded-full bg-white/50" />
{label}
</span>
);
};
const SnapshotCell = ({
label,
value,
hint,
}: {
label: string;
value: string;
hint: string;
}) => {
return (
<div className={innerTileClass}>
<p className="text-[10px] font-medium uppercase tracking-[0.18em] text-white/42">{label}</p>
<p className="mt-3 text-[1.1rem] font-medium tracking-[-0.03em] text-white/92">{value}</p>
<p className="mt-2 text-[12px] leading-[1.58] text-white/54">{hint}</p>
</div>
);
};
const InsightBoard = ({
title,
summary,
metrics,
availability,
note,
accentClass,
}: {
title: string;
summary: string;
metrics: Array<{ id: string; label: string; value: string; hint: string }>;
availability: 'ready' | 'limited';
note?: string;
accentClass: string;
}) => {
const heroMetric = metrics[0];
const supportMetrics = metrics.slice(1);
return (
<section className={cn(panelClass, 'p-5 sm:p-6')}>
<div className={cn('pointer-events-none absolute inset-x-0 top-0 h-24 opacity-90', accentClass)} />
<div className="relative">
<div className="flex items-start justify-between gap-4">
<div className="max-w-[34rem]">
<p className="text-[10px] font-medium uppercase tracking-[0.2em] text-white/40">
Review Signal
</p>
<h2 className="mt-3 text-[1.18rem] font-medium tracking-[-0.04em] text-white">
{title}
</h2>
<p className="mt-3 text-[13px] leading-[1.68] text-white/64">{summary}</p>
</div>
{availability === 'limited' ? <AccessoryPill label="Limited" subtle /> : null}
</div>
<div className="mt-6 grid gap-4 xl:grid-cols-[minmax(0,0.94fr)_minmax(15rem,0.78fr)]">
<div className={cn(innerTileClass, 'min-h-[11.5rem]')}>
{heroMetric ? (
<>
<p className="text-[10px] font-medium uppercase tracking-[0.18em] text-white/42">
{heroMetric.label}
</p>
<div className="mt-5 flex items-end gap-3">
<p className="text-[2.55rem] font-light leading-none tracking-[-0.06em] text-white md:text-[3rem]">
{heroMetric.value}
</p>
</div>
<p className="mt-4 max-w-[24rem] text-[12px] leading-[1.62] text-white/54">
{heroMetric.hint}
</p>
</>
) : (
<p className="text-[12px] leading-[1.62] text-white/52"> .</p>
)}
</div>
<div className="space-y-3">
{supportMetrics.length > 0 ? (
supportMetrics.map((metric) => (
<div key={metric.id} className={innerTileClass}>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-[10px] font-medium uppercase tracking-[0.18em] text-white/42">
{metric.label}
</p>
<p className="mt-2 text-[1rem] font-medium tracking-[-0.03em] text-white/88">
{metric.value}
</p>
</div>
</div>
<p className="mt-3 text-[12px] leading-[1.58] text-white/54">{metric.hint}</p>
</div>
))
) : (
<div className={innerTileClass}>
<p className="text-[12px] leading-[1.58] text-white/52"> .</p>
</div>
)}
</div>
</div>
{note ? (
<p className="mt-4 rounded-[1.3rem] border border-white/8 bg-white/[0.05] px-4 py-3 text-[12px] leading-[1.62] text-white/52">
{note}
</p>
) : null}
</div>
</section>
);
};
export const StatsOverviewWidget = () => {
const { stats } = copy;
const { isPro } = usePlanTier();
@@ -171,181 +34,214 @@ export const StatsOverviewWidget = () => {
const carryForwardCtaLabel = isPro ? stats.reviewCarryCtaPro : review.carryForward.ctaLabel;
return (
<div className="relative min-h-dvh overflow-hidden bg-black text-white selection:bg-white/20">
<div className="relative min-h-dvh overflow-hidden bg-black text-white selection:bg-white/20 font-sans">
{/* Immersive Background */}
<div
className="absolute inset-0 bg-cover bg-center"
className="absolute inset-0 bg-cover bg-center transition-transform duration-[2s] ease-[cubic-bezier(0.22,1,0.36,1)] scale-105 blur-[60px] brightness-50"
style={getSceneStageBackgroundStyle(activeScene, sceneAssetMap?.[activeScene.id])}
/>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.62)_100%)] mix-blend-multiply pointer-events-none" />
<div className="absolute inset-0 bg-black/20 pointer-events-none" />
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.14)_0%,rgba(2,6,23,0.34)_44%,rgba(2,6,23,0.64)_100%)] pointer-events-none" />
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.035)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:72px_72px] opacity-[0.14] pointer-events-none" />
{/* Premium Cinematic Overlays */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.85)_100%)] mix-blend-multiply pointer-events-none" />
<div className="absolute inset-0 bg-black/40 pointer-events-none" />
<div className="absolute inset-0 bg-[linear-gradient(180deg,transparent_0%,rgba(0,0,0,0.6)_50%,rgba(0,0,0,0.9)_100%)] pointer-events-none" />
<header className="absolute inset-x-0 top-0 z-40 flex items-center justify-between px-5 py-5 md:px-8 md:py-8">
{/* Header */}
<header className="absolute inset-x-0 top-0 z-40 flex items-center justify-between px-8 py-8 md:px-12 md:py-10 opacity-0 animate-fade-in delay-150">
<div className="flex items-center gap-3">
<p className="text-[11px] font-bold uppercase tracking-[0.4em] text-white/54">
<p className="text-[11px] font-bold uppercase tracking-[0.4em] text-white/50 drop-shadow-sm">
Weekly Review
</p>
{isPro ? (
<span className="rounded-full bg-white/10 px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.18em] text-white/60 backdrop-blur-md">
{isPro && (
<span className="rounded-full bg-white/10 px-2 py-0.5 text-[9px] font-bold uppercase tracking-widest text-white/60 backdrop-blur-md">
PRO
</span>
) : null}
)}
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => {
void refetch();
}}
className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-white/62 backdrop-blur-md transition hover:bg-white/[0.1] hover:text-white"
className="group flex items-center gap-2 rounded-full border border-white/5 bg-white/5 px-4 py-1.5 text-[10px] font-semibold uppercase tracking-widest text-white/50 backdrop-blur-md transition-all hover:bg-white/10 hover:text-white/80"
>
{stats.refresh}
</button>
<Link
href="/app"
className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-white/62 backdrop-blur-md transition hover:bg-white/[0.1] hover:text-white"
className="group flex items-center gap-2 rounded-full border border-white/5 bg-white/5 px-4 py-1.5 text-[10px] font-semibold uppercase tracking-widest text-white/50 backdrop-blur-md transition-all hover:bg-white/10 hover:text-white/80"
>
{copy.common.hub}
<span>{copy.common.hub}</span>
<span className="transition-transform group-hover:translate-x-0.5">&rarr;</span>
</Link>
</div>
</header>
<main className="relative z-10 mx-auto flex min-h-dvh w-full max-w-[92rem] flex-col px-4 pb-10 pt-28 md:px-8 md:pb-12 md:pt-32">
<section className="grid gap-5 xl:grid-cols-[minmax(0,1.08fr)_minmax(20rem,0.92fr)]">
<div className={cn(panelClass, 'p-6 sm:p-7')}>
<div className="pointer-events-none absolute inset-x-0 top-0 h-24 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.18),rgba(255,255,255,0)_62%)]" />
<div className="relative">
<div className="flex flex-wrap items-center gap-2">
<AccessoryPill label={review.periodLabel} />
<AccessoryPill label={sourceLabel} subtle />
</div>
<div className="mt-8 max-w-[44rem]">
<p className="text-[11px] font-medium uppercase tracking-[0.22em] text-white/42">
</p>
<h1 className="mt-4 text-[2.35rem] font-light leading-[0.98] tracking-[-0.06em] text-white md:text-[3.65rem]">
{review.snapshotSummary}
</h1>
<p className="mt-4 max-w-[34rem] text-[13px] leading-[1.72] text-white/54 md:text-[14px]">
{syncLabel}
</p>
</div>
{/* Main Flow */}
<main className="relative z-10 mx-auto flex min-h-dvh w-full max-w-6xl flex-col px-6 pb-24 pt-32 md:px-12 md:pt-40">
{/* 1. Hero Summary (Cinematic Entry) */}
<section className="mb-20 flex flex-col items-center text-center opacity-0 animate-fade-in-up delay-150">
<div className="mb-6 flex flex-wrap justify-center gap-3">
<div className="inline-flex items-center gap-2 rounded-full border border-white/5 bg-white/5 px-3 py-1.5 backdrop-blur-md">
<span className="h-1.5 w-1.5 rounded-full bg-white/40" />
<span className="text-[10px] font-bold uppercase tracking-widest text-white/70">
{review.periodLabel}
</span>
</div>
<div className="inline-flex items-center gap-2 rounded-full border border-white/5 bg-white/5 px-3 py-1.5 backdrop-blur-md">
<span className="text-[10px] font-bold uppercase tracking-widest text-white/40">
{sourceLabel}
</span>
</div>
</div>
<h1 className="max-w-4xl text-[2.5rem] font-light leading-[1.1] tracking-tight text-white/95 md:text-[4rem]">
{review.snapshotSummary}
</h1>
<p className="mt-6 text-[13px] font-medium tracking-widest uppercase text-white/40">
{syncLabel}
</p>
<div className={cn(panelClass, 'p-5 sm:p-6')}>
<div className="pointer-events-none absolute inset-x-0 top-0 h-24 bg-[radial-gradient(circle_at_top_left,rgba(96,165,250,0.24),rgba(96,165,250,0)_62%)]" />
<div className="relative">
<p className="text-[10px] font-medium uppercase tracking-[0.2em] text-white/40">
Snapshot Signals
</p>
<div className="mt-5 grid gap-3 sm:grid-cols-2">
{review.snapshotMetrics.map((metric) => (
<SnapshotCell
key={metric.id}
label={metric.label}
value={metric.value}
hint={metric.hint}
/>
))}
{/* Inline Snapshot Metrics */}
<div className="mt-16 flex flex-wrap justify-center gap-8 md:gap-16">
{review.snapshotMetrics.map((metric) => (
<div key={metric.id} className="flex flex-col items-center">
<p className="text-[10px] font-bold uppercase tracking-[0.2em] text-white/40">
{metric.label}
</p>
<p className="mt-2 text-4xl font-light tracking-tight text-white/90 md:text-5xl">
{metric.value}
</p>
<p className="mt-2 max-w-[12rem] text-center text-[12px] leading-relaxed text-white/50">
{metric.hint}
</p>
</div>
</div>
))}
</div>
</section>
<section className="mt-12 grid gap-5 xl:grid-cols-[1.06fr_0.94fr]">
<InsightBoard
title={review.startQuality.title}
summary={review.startQuality.summary}
metrics={review.startQuality.metrics}
availability={review.startQuality.availability}
note={review.startQuality.note}
accentClass="bg-[radial-gradient(circle_at_top_left,rgba(96,165,250,0.32),rgba(96,165,250,0)_62%)]"
/>
<InsightBoard
title={review.recoveryQuality.title}
summary={review.recoveryQuality.summary}
metrics={review.recoveryQuality.metrics}
availability={review.recoveryQuality.availability}
note={review.recoveryQuality.note}
accentClass="bg-[radial-gradient(circle_at_top_left,rgba(20,184,166,0.3),rgba(20,184,166,0)_62%)]"
/>
</section>
<section className="mt-5 grid gap-5 xl:grid-cols-[0.9fr_1.1fr]">
<InsightBoard
title={review.completionQuality.title}
summary={review.completionQuality.summary}
metrics={review.completionQuality.metrics}
availability={review.completionQuality.availability}
note={review.completionQuality.note}
accentClass="bg-[radial-gradient(circle_at_top_left,rgba(245,158,11,0.32),rgba(245,158,11,0)_62%)]"
/>
<section className={cn(panelClass, 'p-6 sm:p-7')}>
<div className="pointer-events-none absolute inset-x-0 top-0 h-28 bg-[radial-gradient(circle_at_top_left,rgba(168,85,247,0.32),rgba(168,85,247,0)_62%)]" />
<div className="relative">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="max-w-[34rem]">
<p className="text-[10px] font-medium uppercase tracking-[0.2em] text-white/40">
Carry Forward
</p>
<h2 className="mt-3 text-[1.45rem] font-medium tracking-[-0.04em] text-white md:text-[1.8rem]">
</h2>
<p className="mt-4 text-[14px] leading-[1.72] text-white/66">
{review.carryForward.keepDoing}
</p>
{/* 2. Insight Pillars (Start, Recovery, Completion) */}
<section className="mb-24 grid gap-6 md:grid-cols-3 opacity-0 animate-fade-in-up delay-300">
{[
{
data: review.startQuality,
accent: 'bg-[radial-gradient(circle_at_top,rgba(96,165,250,0.15),transparent_70%)]'
},
{
data: review.recoveryQuality,
accent: 'bg-[radial-gradient(circle_at_top,rgba(20,184,166,0.15),transparent_70%)]'
},
{
data: review.completionQuality,
accent: 'bg-[radial-gradient(circle_at_top,rgba(245,158,11,0.15),transparent_70%)]'
}
].map((column, idx) => (
<div
key={idx}
className="relative flex flex-col overflow-hidden rounded-[2rem] border border-white/5 bg-white/5 p-8 backdrop-blur-2xl transition-all hover:bg-white/10 hover:border-white/10"
>
<div className={cn("absolute inset-0 pointer-events-none", column.accent)} />
<div className="relative z-10 flex flex-col h-full">
<p className="text-[10px] font-bold uppercase tracking-[0.2em] text-white/40">
{column.data.title}
</p>
<p className="mt-4 text-[14px] leading-relaxed text-white/70">
{column.data.summary}
</p>
<div className="mt-8 flex-1 space-y-6">
{column.data.metrics.map((metric, mIdx) => (
<div key={metric.id} className={cn("flex flex-col", mIdx > 0 && "pt-6 border-t border-white/5")}>
<p className="text-[10px] font-semibold uppercase tracking-widest text-white/40">
{metric.label}
</p>
<p className={cn("mt-1 font-light tracking-tight text-white/90", mIdx === 0 ? "text-4xl" : "text-2xl")}>
{metric.value}
</p>
<p className="mt-2 text-[12px] leading-relaxed text-white/50">
{metric.hint}
</p>
</div>
))}
{column.data.metrics.length === 0 && (
<p className="text-[12px] leading-relaxed text-white/30 italic"> .</p>
)}
</div>
{isPro ? <AccessoryPill label="Recommended Ritual" subtle /> : null}
{column.data.note && (
<div className="mt-8 rounded-2xl border border-white/5 bg-white/5 p-4">
<p className="text-[11px] leading-relaxed text-white/50">
{column.data.note}
</p>
</div>
)}
{column.data.availability === 'limited' && (
<div className="mt-6 inline-flex self-start rounded-full border border-white/5 bg-white/5 px-3 py-1 text-[9px] font-bold uppercase tracking-widest text-white/30">
Limited Data
</div>
)}
</div>
</div>
))}
</section>
<div className="mt-6 grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(15rem,0.78fr)]">
<div className={innerTileClass}>
<p className="text-[10px] font-medium uppercase tracking-[0.18em] text-white/42">
{/* 3. Carry Forward Climax */}
<section className="flex flex-col items-center text-center opacity-0 animate-fade-in-up delay-500">
<div className="relative w-full max-w-3xl overflow-hidden rounded-[2.5rem] border border-white/10 bg-white/5 p-10 backdrop-blur-3xl shadow-2xl md:p-16">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(168,85,247,0.15),transparent_70%)] pointer-events-none" />
<div className="relative z-10 flex flex-col items-center">
<div className="inline-flex items-center gap-2 rounded-full border border-white/5 bg-white/5 px-3 py-1.5 backdrop-blur-md mb-6">
<span className="h-1.5 w-1.5 rounded-full bg-white/40" />
<span className="text-[10px] font-bold uppercase tracking-widest text-white/60">
Carry Forward
</span>
</div>
<h2 className="text-3xl font-light tracking-tight text-white/95 md:text-4xl">
</h2>
<p className="mt-6 text-lg text-white/70 font-light leading-relaxed max-w-xl">
&quot;{review.carryForward.keepDoing}&quot;
</p>
<div className="mt-10 grid gap-6 text-left w-full md:grid-cols-2">
<div className="rounded-2xl border border-white/5 bg-white/5 p-5">
<p className="text-[10px] font-bold uppercase tracking-[0.2em] text-white/40">
</p>
<p className="mt-3 text-[14px] leading-[1.72] text-white/72">
<p className="mt-3 text-[13px] leading-relaxed text-white/70">
{review.carryForward.tryNext}
</p>
</div>
<div className={innerTileClass}>
<p className="text-[10px] font-medium uppercase tracking-[0.18em] text-white/42">
Atmosphere
<div className="rounded-2xl border border-white/5 bg-white/5 p-5">
<p className="text-[10px] font-bold uppercase tracking-[0.2em] text-white/40 flex justify-between">
<span> Atmosphere</span>
{isPro && <span className="text-white/30">PRO</span>}
</p>
<p className="mt-3 text-[14px] font-medium tracking-[-0.03em] text-white/88">
<p className="mt-3 text-[15px] font-medium tracking-tight text-white/90">
{review.carryForward.presetLabel}
</p>
<p className="mt-2 text-[12px] leading-[1.58] text-white/52">
.
</p>
<p className="mt-1 text-[12px] text-white/50"> .</p>
</div>
</div>
<div className="mt-6 flex flex-wrap items-center justify-between gap-4 border-t border-white/10 pt-5">
<p className="max-w-[30rem] text-[12px] leading-[1.65] text-white/44">
review는 ,
.
</p>
<div className="mt-12 flex flex-col items-center gap-4">
<Link
href={review.carryForward.ctaHref}
className="inline-flex min-h-[3.4rem] items-center justify-center rounded-full border border-white/14 bg-white/[0.14] px-6 text-[13px] font-medium tracking-[0.02em] text-white shadow-[0_12px_28px_rgba(3,7,18,0.22)] backdrop-blur-md transition hover:bg-white/[0.18] active:scale-[0.99]"
className="group relative flex h-14 items-center justify-center overflow-hidden rounded-full border border-white/20 bg-white/10 px-10 text-[14px] font-medium tracking-wide text-white shadow-2xl backdrop-blur-md transition-all duration-300 hover:bg-white/20 hover:scale-[1.02] active:scale-[0.98]"
>
{carryForwardCtaLabel}
<span className="relative z-10">{carryForwardCtaLabel}</span>
</Link>
<p className="text-[11px] text-white/30 tracking-wide">
Review는 , .
</p>
</div>
</div>
</section>
</div>
</section>
</main>
</div>
);