feat(app-hub): 씬 중심 허브 화면으로 전면 리빌드

맥락:
- /app 첫 화면의 정보량이 많아 LifeAt/Portal 같은 몰입형 경험과 거리가 있었습니다.
- Start, 공간 선택, 메모 진입을 점진 노출 구조로 재정렬할 필요가 있었습니다.

변경사항:
- AppHub를 데스크톱 2열(컨트롤/씬) 구조와 모바일 스택 구조로 재편했습니다.
- Start CTA를 컴팩트 위계로 정리하고 커스텀 액션을 2순위 텍스트형으로 유지했습니다.
- Room 영역을 선택 Hero 1개 + 추천 썸네일 스트립 + 더보기 시트 구조로 전면 변경했습니다.
- 최근 생각은 단일 진입점 + 인박스 시트로 통합하고 localStorage 기반 thought inbox를 추가했습니다.
- /space 종료 동선에 세션 요약 시트(최근 메모 3개)를 연결해 허브 복귀 흐름을 정리했습니다.
- AppHub 기본 비주얼 모드를 cinematic으로 고정하고 배경 오버레이를 가독성 중심으로 재조정했습니다.

검증:
- npx tsc --noEmit
- npm run build

세션-상태: 씬 중심 허브 리빌드와 메모/시트 기반 점진 노출 구조 반영 완료
세션-다음: 실제 사용자 테스트 후 카드/텍스트 밀도 미세 조정
세션-리스크: 실디바이스 대비(특히 저사양 모바일)에서 배경/블러 렌더링 비용 확인 필요
This commit is contained in:
2026-03-01 20:06:57 +09:00
parent 85488f542e
commit 47e80e59d2
20 changed files with 1034 additions and 131 deletions

View File

@@ -1,26 +1,44 @@
import { getRoomCardBackgroundStyle, type RoomTheme } from '@/entities/room';
import type { AppHubVisualMode } from '@/shared/config/appHubVisualMode';
import { cn } from '@/shared/lib/cn';
interface RoomPreviewCardProps {
room: RoomTheme;
visualMode: AppHubVisualMode;
variant?: 'hero' | 'compact';
className?: string;
selected: boolean;
onSelect: (roomId: string) => void;
}
export const RoomPreviewCard = ({
room,
visualMode,
variant = 'hero',
className,
selected,
onSelect,
}: RoomPreviewCardProps) => {
const cinematic = visualMode === 'cinematic';
const compact = variant === 'compact';
return (
<button
type="button"
onClick={() => onSelect(room.id)}
className={cn(
'group relative h-[248px] overflow-hidden rounded-2xl border p-4 text-left transition-all duration-250 motion-reduce:transition-none',
'group relative overflow-hidden rounded-3xl border text-left transition-all duration-250 motion-reduce:transition-none',
compact
? 'h-[112px] min-w-[164px] snap-start p-2.5 sm:min-w-0 sm:aspect-[4/3]'
: 'h-[338px] p-3 sm:h-[420px] sm:p-4',
selected
? 'border-brand-dark/28 shadow-[0_0_0_1px_rgba(48,77,109,0.18)]'
: 'border-brand-dark/16 hover:border-brand-dark/28',
? cinematic
? 'border-sky-200/44 shadow-[0_0_0_1px_rgba(186,230,253,0.34),0_20px_50px_rgba(2,6,23,0.32)]'
: 'border-brand-dark/28 shadow-[0_0_0_1px_rgba(48,77,109,0.18),0_16px_40px_rgba(15,23,42,0.16)]'
: cinematic
? 'border-white/16 hover:border-white/28'
: 'border-brand-dark/16 hover:border-brand-dark/26',
className,
)}
>
<div
@@ -28,39 +46,59 @@ export const RoomPreviewCard = ({
className="absolute inset-0"
style={getRoomCardBackgroundStyle(room)}
/>
<div aria-hidden className="absolute inset-0 bg-slate-900/28" />
<div aria-hidden className="absolute inset-0 bg-[linear-gradient(to_top,rgba(2,6,23,0.64),rgba(2,6,23,0.2))]" />
<div
aria-hidden
className={cn(
'absolute inset-0',
cinematic ? 'bg-slate-950/32' : 'bg-slate-900/20',
)}
/>
<div
aria-hidden
className={cn(
'absolute inset-0',
cinematic
? 'bg-[linear-gradient(to_top,rgba(2,6,23,0.86),rgba(2,6,23,0.42),rgba(2,6,23,0.06))]'
: 'bg-[linear-gradient(to_top,rgba(2,6,23,0.58),rgba(2,6,23,0.12))]',
)}
/>
<div className="relative flex h-full flex-col justify-between space-y-3 rounded-xl border border-white/20 bg-slate-950/22 p-3 backdrop-blur-[1.5px]">
<div>
<h3 className="text-base font-semibold text-white">{room.name}</h3>
<p className="mt-1 text-xs text-white/82">{room.description}</p>
</div>
<div
className={cn(
'relative flex h-full rounded-2xl',
cinematic
? 'border border-white/14 bg-slate-950/16'
: 'border border-white/16 bg-slate-950/10',
compact ? 'items-end p-2.5' : 'items-end p-3 sm:p-4',
)}
>
{selected ? (
<span
className={cn(
'absolute left-3 top-3 inline-flex items-center rounded-full border px-2 py-1 text-[10px] font-medium',
cinematic
? 'border-white/26 bg-slate-950/42 text-white/78'
: 'border-white/52 bg-white/70 text-brand-dark/74',
)}
>
</span>
) : null}
<div className="flex flex-wrap gap-2">
{room.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center gap-1 rounded-full bg-white/16 px-3 py-1.5 text-xs font-medium text-white/92 ring-1 ring-white/22"
>
{tag}
</span>
))}
</div>
<div className="space-y-2">
<p className="text-xs text-white/84">
: <span className="font-medium text-white">{room.recommendedSound}</span>
</p>
<div className="flex flex-wrap gap-2">
<span className="inline-flex items-center gap-1 rounded-full bg-white/14 px-3 py-1.5 text-xs font-medium text-white/90 ring-1 ring-white/22">
· {room.recommendedTime}
</span>
<span className="inline-flex items-center gap-1 rounded-full bg-white/14 px-3 py-1.5 text-xs font-medium text-white/90 ring-1 ring-white/22">
· {room.vibeLabel}
</span>
{compact ? (
<div className="w-full rounded-lg bg-slate-950/44 px-2.5 py-2">
<p className="truncate text-sm font-semibold text-white">{room.name}</p>
<p className="mt-0.5 truncate text-[11px] text-white/70">{room.vibeLabel}</p>
</div>
</div>
) : (
<div className="w-full max-w-[440px] rounded-2xl border border-white/14 bg-slate-950/46 px-4 py-3 backdrop-blur-md">
<p className="text-[11px] uppercase tracking-[0.16em] text-white/56">Selected Space</p>
<h3 className="mt-1 text-2xl font-semibold tracking-tight text-white sm:text-[2rem]">
{room.name}
</h3>
<p className="mt-1 text-sm text-white/74">{room.description}</p>
</div>
)}
</div>
</button>
);