refactor(space): 단일 워크스페이스 Setup→Focus 전환 구조 도입
맥락: - 허브를 경유하는 흐름 대신 /space 한 화면에서 설정과 몰입을 이어서 처리할 필요가 있었습니다. - View 로직 분리와 파일 분할 기준을 지키면서 도크/시트 패턴을 통합해야 했습니다. 변경사항: - /space를 Setup(기본)과 Focus(시작 후) 2상태로 운영하는 space-workspace 위젯을 추가했습니다. - Setup Drawer를 추가해 Space 선택, Goal(필수), Sound(선택) 섹션과 하단 고정 CTA를 구성했습니다. - Goal 입력이 비어있으면 시작하기 버튼이 비활성화되도록 UI 검증을 반영했습니다. - Focus 상태에서 하단 HUD만 유지하고 우측 Tools Dock(🎧/📝/📨/📊/⚙) + 우측 시트 패턴을 적용했습니다. - Notes(쓰기)와 Inbox(읽기) 패널을 분리하고 더미 토스트 동작을 연결했습니다. - FSD 분리를 위해 features(space-select/session-goal/inbox)와 widgets(space-workspace/space-setup-drawer/space-focus-hud/space-sheet-shell)를 추가했습니다. - 기존 space-shell은 신규 워크스페이스로 연결되는 얇은 래퍼로 정리했습니다. 검증: - npx tsc --noEmit - npm run build 세션-상태: /space 단일 워크스페이스에서 Setup→Focus 전환이 동작합니다. 세션-다음: 진입 경로를 /space로 통일하고 레거시 /app 라우트를 정리합니다. 세션-리스크: useSearchParams 기반 초기값은 클라이언트 최초 렌더 기준으로만 반영됩니다.
This commit is contained in:
1
src/widgets/space-sheet-shell/index.ts
Normal file
1
src/widgets/space-sheet-shell/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './ui/SpaceSideSheet';
|
||||
85
src/widgets/space-sheet-shell/ui/SpaceSideSheet.tsx
Normal file
85
src/widgets/space-sheet-shell/ui/SpaceSideSheet.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
|
||||
interface SpaceSideSheetProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
footer?: ReactNode;
|
||||
widthClassName?: string;
|
||||
}
|
||||
|
||||
export const SpaceSideSheet = ({
|
||||
open,
|
||||
title,
|
||||
subtitle,
|
||||
onClose,
|
||||
children,
|
||||
footer,
|
||||
widthClassName,
|
||||
}: SpaceSideSheetProps) => {
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="시트 닫기"
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 z-40 bg-slate-950/22 backdrop-blur-[1px]"
|
||||
/>
|
||||
|
||||
<aside
|
||||
className={cn(
|
||||
'fixed inset-y-0 right-0 z-50 p-2 sm:p-3',
|
||||
widthClassName ?? 'w-[min(360px,92vw)]',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full flex-col overflow-hidden rounded-3xl border border-white/16 bg-slate-950/68 text-white shadow-[0_24px_70px_rgba(2,6,23,0.55)] backdrop-blur-2xl">
|
||||
<header className="flex items-start justify-between gap-3 border-b border-white/10 px-4 py-3 sm:px-5">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white">{title}</h2>
|
||||
{subtitle ? <p className="mt-1 text-xs text-white/58">{subtitle}</p> : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="닫기"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-white/18 bg-white/8 text-sm text-white/80 transition-colors hover:bg-white/14 hover:text-white"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-5">{children}</div>
|
||||
|
||||
{footer ? <footer className="border-t border-white/10 bg-white/4 px-4 py-3 sm:px-5">{footer}</footer> : null}
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user