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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user