refactor(space): scene 도메인과 current session 계약을 정리
This commit is contained in:
@@ -6,7 +6,7 @@ export type FocusSessionCompletionType = 'goal-complete' | 'timer-complete';
|
||||
|
||||
export interface FocusSession {
|
||||
id: string;
|
||||
roomId: string;
|
||||
sceneId: string;
|
||||
goal: string;
|
||||
timerPresetId: string;
|
||||
soundPresetId: string | null;
|
||||
@@ -24,7 +24,7 @@ export interface FocusSession {
|
||||
}
|
||||
|
||||
export interface StartFocusSessionRequest {
|
||||
roomId: string;
|
||||
sceneId: string;
|
||||
goal: string;
|
||||
timerPresetId: string;
|
||||
soundPresetId?: string | null;
|
||||
@@ -53,7 +53,7 @@ export const focusSessionApi = {
|
||||
/**
|
||||
* Backend Codex:
|
||||
* - 새로운 집중 세션을 생성하고 즉시 running 상태로 시작한다.
|
||||
* - roomId, goal, timerPresetId, soundPresetId를 저장한다.
|
||||
* - sceneId, goal, timerPresetId, soundPresetId를 저장한다.
|
||||
* - 응답에는 생성 직후의 세션 상태와 남은 시간 계산에 필요한 시각 필드를 포함한다.
|
||||
*/
|
||||
startSession: async (payload: StartFocusSessionRequest): Promise<FocusSession> => {
|
||||
@@ -66,11 +66,12 @@ export const focusSessionApi = {
|
||||
/**
|
||||
* Backend Codex:
|
||||
* - 현재 세션의 현재 phase를 일시정지한다.
|
||||
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||
* - phaseRemainingSeconds를 정확히 저장해 재개 시 이어서 동작하게 한다.
|
||||
* - 이미 paused 상태여도 멱등적으로 최신 세션 상태를 반환한다.
|
||||
*/
|
||||
pauseSession: async (sessionId: string): Promise<FocusSession> => {
|
||||
return apiClient<FocusSession>(`api/v1/focus-sessions/${sessionId}/pause`, {
|
||||
pauseSession: async (): Promise<FocusSession> => {
|
||||
return apiClient<FocusSession>('api/v1/focus-sessions/current/pause', {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
@@ -78,11 +79,12 @@ export const focusSessionApi = {
|
||||
/**
|
||||
* Backend Codex:
|
||||
* - 일시정지된 세션을 재개하고 새 phaseEndsAt/serverNow를 반환한다.
|
||||
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||
* - 이미 running 상태여도 멱등적으로 최신 세션 상태를 반환한다.
|
||||
* - 남은 시간을 다시 계산할 수 있게 phaseRemainingSeconds도 함께 내려준다.
|
||||
*/
|
||||
resumeSession: async (sessionId: string): Promise<FocusSession> => {
|
||||
return apiClient<FocusSession>(`api/v1/focus-sessions/${sessionId}/resume`, {
|
||||
resumeSession: async (): Promise<FocusSession> => {
|
||||
return apiClient<FocusSession>('api/v1/focus-sessions/current/resume', {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
@@ -90,11 +92,12 @@ export const focusSessionApi = {
|
||||
/**
|
||||
* Backend Codex:
|
||||
* - 현재 진행 중인 phase를 처음 길이로 다시 시작한다.
|
||||
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||
* - focus/break 어느 phase인지 유지한 채 phaseStartedAt, phaseEndsAt, phaseRemainingSeconds를 갱신한다.
|
||||
* - 클라이언트의 Reset 버튼은 이 API 응답으로 즉시 HUD를 다시 그린다.
|
||||
*/
|
||||
restartCurrentPhase: async (sessionId: string): Promise<FocusSession> => {
|
||||
return apiClient<FocusSession>(`api/v1/focus-sessions/${sessionId}/restart-phase`, {
|
||||
restartCurrentPhase: async (): Promise<FocusSession> => {
|
||||
return apiClient<FocusSession>('api/v1/focus-sessions/current/restart-phase', {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
@@ -102,14 +105,12 @@ export const focusSessionApi = {
|
||||
/**
|
||||
* Backend Codex:
|
||||
* - 현재 세션을 완료 처리하고 통계 집계 대상으로 반영한다.
|
||||
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||
* - completionType으로 goal-complete / timer-complete을 구분해 저장한다.
|
||||
* - 완료된 세션 스냅샷을 반환하거나, 최소한 성공적으로 완료되었음을 알 수 있는 응답을 보낸다.
|
||||
*/
|
||||
completeSession: async (
|
||||
sessionId: string,
|
||||
payload: CompleteFocusSessionRequest,
|
||||
): Promise<FocusSession> => {
|
||||
return apiClient<FocusSession>(`api/v1/focus-sessions/${sessionId}/complete`, {
|
||||
completeSession: async (payload: CompleteFocusSessionRequest): Promise<FocusSession> => {
|
||||
return apiClient<FocusSession>('api/v1/focus-sessions/current/complete', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
@@ -118,11 +119,12 @@ export const focusSessionApi = {
|
||||
/**
|
||||
* Backend Codex:
|
||||
* - 현재 세션을 중도 종료(abandon) 처리한다.
|
||||
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||
* - 통계에서는 abandon 여부를 구분할 수 있게 저장한다.
|
||||
* - 성공 시 204 또는 빈 성공 응답을 반환한다.
|
||||
*/
|
||||
abandonSession: async (sessionId: string): Promise<void> => {
|
||||
return apiClient<void>(`api/v1/focus-sessions/${sessionId}/abandon`, {
|
||||
abandonSession: async (): Promise<void> => {
|
||||
return apiClient<void>('api/v1/focus-sessions/current/abandon', {
|
||||
method: 'POST',
|
||||
expectNoContent: true,
|
||||
});
|
||||
|
||||
@@ -199,7 +199,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
||||
}
|
||||
|
||||
const session = await runMutation(
|
||||
() => focusSessionApi.pauseSession(currentSession.id),
|
||||
() => focusSessionApi.pauseSession(),
|
||||
'세션을 일시정지하지 못했어요.',
|
||||
);
|
||||
|
||||
@@ -211,7 +211,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
||||
}
|
||||
|
||||
const session = await runMutation(
|
||||
() => focusSessionApi.resumeSession(currentSession.id),
|
||||
() => focusSessionApi.resumeSession(),
|
||||
'세션을 다시 시작하지 못했어요.',
|
||||
);
|
||||
|
||||
@@ -223,7 +223,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
||||
}
|
||||
|
||||
const session = await runMutation(
|
||||
() => focusSessionApi.restartCurrentPhase(currentSession.id),
|
||||
() => focusSessionApi.restartCurrentPhase(),
|
||||
'현재 페이즈를 다시 시작하지 못했어요.',
|
||||
);
|
||||
|
||||
@@ -235,7 +235,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
||||
}
|
||||
|
||||
const session = await runMutation(
|
||||
() => focusSessionApi.completeSession(currentSession.id, payload),
|
||||
() => focusSessionApi.completeSession(payload),
|
||||
'세션을 완료 처리하지 못했어요.',
|
||||
);
|
||||
|
||||
@@ -251,7 +251,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
||||
}
|
||||
|
||||
const result = await runMutation(
|
||||
() => focusSessionApi.abandonSession(currentSession.id),
|
||||
() => focusSessionApi.abandonSession(),
|
||||
'세션을 종료하지 못했어요.',
|
||||
);
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export const InboxList = ({ thoughts, onCompleteThought, onDeleteThought, classN
|
||||
{thought.text}
|
||||
</p>
|
||||
<p className="mt-1.5 text-[11px] text-white/54">
|
||||
{thought.roomName} · {thought.capturedAt}
|
||||
{thought.sceneName} · {thought.capturedAt}
|
||||
</p>
|
||||
<div className="absolute right-2 top-2 flex items-center gap-1 opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100">
|
||||
<button
|
||||
|
||||
1
src/features/scene-select/index.ts
Normal file
1
src/features/scene-select/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './ui/SceneSelectCarousel';
|
||||
@@ -1,36 +1,36 @@
|
||||
import { getRoomCardBackgroundStyle, type RoomTheme } from '@/entities/room';
|
||||
import { getSceneCardBackgroundStyle, type SceneTheme } from '@/entities/scene';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
|
||||
interface SpaceSelectCarouselProps {
|
||||
rooms: RoomTheme[];
|
||||
selectedRoomId: string;
|
||||
onSelect: (roomId: string) => void;
|
||||
interface SceneSelectCarouselProps {
|
||||
scenes: SceneTheme[];
|
||||
selectedSceneId: string;
|
||||
onSelect: (sceneId: string) => void;
|
||||
}
|
||||
|
||||
export const SpaceSelectCarousel = ({
|
||||
rooms,
|
||||
selectedRoomId,
|
||||
export const SceneSelectCarousel = ({
|
||||
scenes,
|
||||
selectedSceneId,
|
||||
onSelect,
|
||||
}: SpaceSelectCarouselProps) => {
|
||||
}: SceneSelectCarouselProps) => {
|
||||
return (
|
||||
<div className="-mx-1 overflow-x-auto px-1 pb-1">
|
||||
<div className="flex min-w-full gap-2.5">
|
||||
{rooms.map((room) => {
|
||||
const selected = room.id === selectedRoomId;
|
||||
{scenes.map((scene) => {
|
||||
const selected = scene.id === selectedSceneId;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={room.id}
|
||||
key={scene.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(room.id)}
|
||||
onClick={() => onSelect(scene.id)}
|
||||
className={cn(
|
||||
'group relative h-24 min-w-[138px] overflow-hidden rounded-xl border text-left sm:min-w-[148px]',
|
||||
selected
|
||||
? 'border-sky-200/38 shadow-[0_0_0_1px_rgba(186,230,253,0.2),0_0_10px_rgba(56,189,248,0.12)]'
|
||||
: 'border-white/16 hover:border-white/24',
|
||||
)}
|
||||
style={getRoomCardBackgroundStyle(room)}
|
||||
aria-label={`${room.name} 선택`}
|
||||
style={getSceneCardBackgroundStyle(scene)}
|
||||
aria-label={`${scene.name} 선택`}
|
||||
>
|
||||
<span className="absolute inset-x-0 bottom-0 h-[56%] bg-[linear-gradient(180deg,rgba(2,6,23,0)_0%,rgba(2,6,23,0.2)_52%,rgba(2,6,23,0.24)_100%)]" />
|
||||
{selected ? (
|
||||
@@ -39,8 +39,8 @@ export const SpaceSelectCarousel = ({
|
||||
</span>
|
||||
) : null}
|
||||
<div className="absolute inset-x-2 bottom-2">
|
||||
<p className="truncate text-sm font-semibold text-white/96">{room.name}</p>
|
||||
<p className="truncate text-[11px] text-white/66">{room.vibeLabel}</p>
|
||||
<p className="truncate text-sm font-semibold text-white/96">{scene.name}</p>
|
||||
<p className="truncate text-[11px] text-white/66">{scene.vibeLabel}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export * from './ui/SpaceSelectCarousel';
|
||||
Reference in New Issue
Block a user