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

@@ -57,13 +57,13 @@ export const AppAtmosphereEntryShell = ({
{/* Main Focus Entry Ritual */}
<div className="flex flex-1 flex-col items-center justify-center px-4 pb-10">
{/* Inline Accessories (No overlap guarantee) */}
<div className="mb-10 flex min-h-[5rem] flex-col items-center justify-end opacity-0 animate-fade-in-up delay-150">
<div className="mb-12 flex min-h-[5rem] flex-col items-center justify-end opacity-0 animate-fade-in-up delay-150">
{errorAccessory}
{!errorAccessory && topAccessory}
</div>
<div className="w-full max-w-4xl space-y-8 text-center opacity-0 animate-fade-in-up delay-150">
<p className="text-sm font-medium uppercase tracking-[0.25em] text-white/50">
<div className="w-full max-w-4xl text-center opacity-0 animate-fade-in-up delay-150 mb-16">
<p className="text-xs font-bold uppercase tracking-[0.3em] text-white/40 drop-shadow-sm mb-6">
What will you focus on?
</p>
<input
@@ -77,62 +77,66 @@ export const AppAtmosphereEntryShell = ({
}
}}
placeholder={goalPlaceholder}
className="w-full bg-transparent text-center text-4xl font-light tracking-tight text-white outline-none placeholder:text-white/20 md:text-5xl lg:text-[4.5rem] lg:leading-[1.1]"
className="w-full bg-transparent text-center text-4xl font-light tracking-tight text-white outline-none placeholder:text-white/20 md:text-5xl lg:text-[4.5rem] lg:leading-[1.1] transition-colors focus:placeholder:text-white/10"
autoFocus
/>
</div>
<div className="flex w-full flex-col items-center space-y-8 opacity-0 animate-fade-in-up delay-150">
<div className="flex flex-col items-center gap-3">
<div className="flex items-center gap-4 rounded-[2rem] border border-white/10 bg-white/5 p-2 pr-6 shadow-2xl backdrop-blur-xl">
<div className="flex gap-2">
{durationSuggestions.map((minutes) => {
const isSelected = durationDraft === String(minutes);
return (
<button
key={minutes}
type="button"
onClick={() => onSelectDuration(minutes)}
className={cn(
'rounded-full px-5 py-2.5 text-sm font-medium transition-all duration-300',
isSelected
? 'bg-white text-black shadow-md'
: 'text-white/70 hover:bg-white/10 hover:text-white',
)}
>
{minutes}m
</button>
);
})}
</div>
<div className="h-8 w-px bg-white/10" />
<div className="flex items-center gap-2">
<input
value={durationDraft}
onChange={(event) => onDurationChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
onStartSession();
}
}}
inputMode="numeric"
placeholder="Custom"
className="w-16 bg-transparent text-right text-lg font-medium text-white outline-none placeholder:text-white/30"
/>
<span className="text-sm text-white/50">min</span>
</div>
<div className="flex w-full flex-col items-center space-y-10 opacity-0 animate-fade-in-up delay-150">
<div className="flex flex-col items-center">
{/* Primary Action: Massive Custom Timer Input */}
<div className="group relative flex items-baseline justify-center">
<input
value={durationDraft}
onChange={(event) => onDurationChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
onStartSession();
}
}}
inputMode="numeric"
placeholder="0"
className="w-24 bg-transparent text-center text-6xl font-light tracking-tighter text-white outline-none placeholder:text-white/10 transition-all focus:w-32 md:text-7xl"
/>
<span className="absolute -right-12 bottom-2 text-xl font-medium tracking-wide text-white/30 transition-colors group-focus-within:text-white/60 md:bottom-3 md:text-2xl">
min
</span>
</div>
<p className="text-xs text-white/40">{durationHelper}</p>
{/* Secondary Action: Subtle Quick Select Pills */}
<div className="mt-8 flex items-center gap-2 rounded-full border border-white/5 bg-[linear-gradient(145deg,rgba(255,255,255,0.04)_0%,rgba(255,255,255,0.01)_100%)] p-1.5 backdrop-blur-md">
{durationSuggestions.map((minutes) => {
const isSelected = durationDraft === String(minutes);
return (
<button
key={minutes}
type="button"
onClick={() => onSelectDuration(minutes)}
className={cn(
'rounded-full px-4 py-2 text-[12px] font-medium tracking-wide transition-all duration-300',
isSelected
? 'bg-white/10 text-white shadow-sm ring-1 ring-white/20'
: 'text-white/40 hover:text-white/80',
)}
>
{minutes}m
</button>
);
})}
</div>
<p className="mt-6 text-[11px] font-medium tracking-wide text-white/30">{durationHelper}</p>
</div>
<button
type="button"
onClick={onStartSession}
disabled={!canStart}
className="group relative flex h-16 items-center justify-center overflow-hidden rounded-full border border-white/20 bg-white/10 px-12 text-lg 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] disabled:pointer-events-none disabled:opacity-40"
className="group relative flex h-14 items-center justify-center overflow-hidden rounded-full border border-white/20 bg-[linear-gradient(145deg,rgba(255,255,255,0.15)_0%,rgba(255,255,255,0.05)_100%)] px-12 text-[14px] font-medium tracking-widest text-white shadow-[0_0_40px_rgba(255,255,255,0.1)] backdrop-blur-2xl transition-all duration-500 hover:border-white/40 hover:bg-white/20 hover:shadow-[0_0_60px_rgba(255,255,255,0.2)] hover:scale-[1.03] active:scale-[0.98] disabled:pointer-events-none disabled:opacity-30 uppercase"
>
<span className="relative z-10">
<span className="relative z-10 drop-shadow-md">
{isStartingSession ? startButtonLoadingLabel : startButtonLabel}
</span>
</button>

View File

@@ -140,6 +140,15 @@ export const FocusDashboardWidget = () => {
() => getAtmosphereOptionById(selectedAtmosphereId),
[selectedAtmosphereId],
);
const rawDurationValue = useMemo(() => {
const digitsOnly = durationDraft.replace(/[^\d]/g, '');
if (!digitsOnly) {
return null;
}
const parsed = Number(digitsOnly);
return Number.isFinite(parsed) ? parsed : null;
}, [durationDraft]);
const parsedDurationMinutes = parseDurationMinutes(durationDraft);
const resolvedTimerPreset = useMemo(() => {
const targetMinutes =
@@ -178,9 +187,11 @@ export const FocusDashboardWidget = () => {
isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null;
const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle;
const durationHelper =
parsedDurationMinutes === null
? 'Please enter the estimated duration in minutes.'
: entryCopy.durationHelper;
rawDurationValue !== null && rawDurationValue < 5
? 'Please enter at least 5 minutes.'
: parsedDurationMinutes === null
? 'Please enter the estimated duration in minutes.'
: entryCopy.durationHelper;
const hasCurrentSession = Boolean(currentSession);
useEffect(() => {
@@ -301,13 +312,13 @@ export const FocusDashboardWidget = () => {
<div
className={cn(
'absolute inset-0 bg-cover bg-center transition-transform duration-[1.5s] ease-[cubic-bezier(0.22,1,0.36,1)]',
isStartingSession ? 'scale-[1.08] blur-[2px] brightness-75' : 'scale-100 blur-0 brightness-100',
isStartingSession ? 'scale-[1.08] blur-[2px] brightness-75' : 'scale-100 blur-[60px] brightness-50',
)}
style={getSceneStageBackgroundStyle(activeScene, sceneAssetMap?.[activeScene.id])}
/>
{/* Immersive Overlay Gradients */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.6)_100%)] mix-blend-multiply pointer-events-none" />
<div className="absolute inset-0 bg-black/10 pointer-events-none" />
<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" />
{/* Header */}
<header className="absolute top-0 inset-x-0 z-50 flex items-center justify-between px-8 py-8 md:px-12 md:py-10">