From 675014166a65601214f0b856fa0fb654747d643a Mon Sep 17 00:00:00 2001
From: corpi
Date: Mon, 9 Mar 2026 13:05:44 +0900
Subject: [PATCH] =?UTF-8?q?refactor(space):=20scene=20=EB=8F=84=EB=A9=94?=
=?UTF-8?q?=EC=9D=B8=EA=B3=BC=20current=20session=20=EA=B3=84=EC=95=BD?=
=?UTF-8?q?=EC=9D=84=20=EC=A0=95=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/entities/room/index.ts | 2 -
src/entities/scene/index.ts | 2 +
.../model/rooms.ts => scene/model/scenes.ts} | 60 +++++-----
src/entities/{room => scene}/model/types.ts | 10 +-
src/entities/session/model/mockSession.ts | 8 +-
src/entities/session/model/types.ts | 2 +-
src/entities/session/model/useThoughtInbox.ts | 44 ++++++--
.../focus-session/api/focusSessionApi.ts | 34 +++---
.../model/useFocusSessionEngine.ts | 10 +-
src/features/inbox/ui/InboxList.tsx | 2 +-
src/features/scene-select/index.ts | 1 +
.../ui/SceneSelectCarousel.tsx} | 34 +++---
src/features/space-select/index.ts | 1 -
src/shared/lib/apiClient.ts | 13 ++-
.../ui/ControlCenterSheetWidget.tsx | 36 +++---
.../ui/SpaceSetupDrawerWidget.tsx | 36 +++---
.../ui/SpaceToolsDockWidget.tsx | 22 ++--
.../ui/panels/SettingsToolPanel.tsx | 28 ++---
.../ui/SpaceWorkspaceWidget.tsx | 106 +++++++++---------
19 files changed, 243 insertions(+), 208 deletions(-)
delete mode 100644 src/entities/room/index.ts
create mode 100644 src/entities/scene/index.ts
rename src/entities/{room/model/rooms.ts => scene/model/scenes.ts} (86%)
rename src/entities/{room => scene}/model/types.ts (69%)
create mode 100644 src/features/scene-select/index.ts
rename src/features/{space-select/ui/SpaceSelectCarousel.tsx => scene-select/ui/SceneSelectCarousel.tsx} (67%)
delete mode 100644 src/features/space-select/index.ts
diff --git a/src/entities/room/index.ts b/src/entities/room/index.ts
deleted file mode 100644
index 4f02999..0000000
--- a/src/entities/room/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './model/rooms';
-export * from './model/types';
diff --git a/src/entities/scene/index.ts b/src/entities/scene/index.ts
new file mode 100644
index 0000000..e66091f
--- /dev/null
+++ b/src/entities/scene/index.ts
@@ -0,0 +1,2 @@
+export * from './model/scenes';
+export * from './model/types';
diff --git a/src/entities/room/model/rooms.ts b/src/entities/scene/model/scenes.ts
similarity index 86%
rename from src/entities/room/model/rooms.ts
rename to src/entities/scene/model/scenes.ts
index b8f6892..521537a 100644
--- a/src/entities/room/model/rooms.ts
+++ b/src/entities/scene/model/scenes.ts
@@ -1,5 +1,5 @@
import type { CSSProperties } from 'react';
-import type { RoomTheme } from './types';
+import type { SceneTheme } from './types';
const HUB_CURATION_ORDER = [
'quiet-library',
@@ -9,9 +9,9 @@ const HUB_CURATION_ORDER = [
'fireplace',
] as const;
-const HUB_RECOMMENDED_ROOM_COUNT = 3;
+const HUB_RECOMMENDED_SCENE_COUNT = 3;
-export const ROOM_THEMES: RoomTheme[] = [
+export const SCENE_THEMES: SceneTheme[] = [
{
id: 'rain-window',
name: '비 오는 창가',
@@ -234,65 +234,65 @@ export const ROOM_THEMES: RoomTheme[] = [
},
];
-export const getRoomById = (roomId: string) => {
- return ROOM_THEMES.find((room) => room.id === roomId);
+export const getSceneById = (id: string) => {
+ return SCENE_THEMES.find((scene) => scene.id === id);
};
-export const getRoomCardPhotoUrl = (room: RoomTheme) => {
+export const getSceneCardPhotoUrl = (scene: SceneTheme) => {
// Swap managedCardPhotoUrl when you start serving first-party assets.
- return room.managedCardPhotoUrl ?? room.cardPhotoUrl;
+ return scene.managedCardPhotoUrl ?? scene.cardPhotoUrl;
};
-export const getRoomCardBackgroundStyle = (room: RoomTheme): CSSProperties => {
+export const getSceneCardBackgroundStyle = (scene: SceneTheme): CSSProperties => {
return {
- backgroundImage: `url('${getRoomCardPhotoUrl(room)}')`,
+ backgroundImage: `url('${getSceneCardPhotoUrl(scene)}')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
};
};
-export const getRoomBackgroundStyle = (room: RoomTheme): CSSProperties => {
+export const getSceneBackgroundStyle = (scene: SceneTheme): CSSProperties => {
return {
- backgroundImage: `url('${getRoomCardPhotoUrl(room)}'), linear-gradient(160deg, #1e293b 0%, #0f172a 100%)`,
+ backgroundImage: `url('${getSceneCardPhotoUrl(scene)}'), linear-gradient(160deg, #1e293b 0%, #0f172a 100%)`,
backgroundSize: 'cover, cover',
backgroundPosition: 'center, center',
backgroundRepeat: 'no-repeat, no-repeat',
};
};
-const uniqueByRoomId = (rooms: Array) => {
+const uniqueBySceneId = (scenes: Array) => {
const seen = new Set();
- return rooms.filter((room): room is RoomTheme => {
- if (!room || seen.has(room.id)) {
+ return scenes.filter((scene): scene is SceneTheme => {
+ if (!scene || seen.has(scene.id)) {
return false;
}
- seen.add(room.id);
+ seen.add(scene.id);
return true;
});
};
-export const getHubRoomSections = (
- rooms: RoomTheme[],
- selectedRoomId: string,
- recommendedCount = HUB_RECOMMENDED_ROOM_COUNT,
+export const getHubSceneSections = (
+ scenes: SceneTheme[],
+ selectedSceneId: string,
+ recommendedCount = HUB_RECOMMENDED_SCENE_COUNT,
) => {
- const roomById = new Map(rooms.map((room) => [room.id, room] as const));
- const selectedRoom = roomById.get(selectedRoomId);
- const curatedRooms = HUB_CURATION_ORDER.map((id) => roomById.get(id));
+ const sceneById = new Map(scenes.map((scene) => [scene.id, scene] as const));
+ const selectedScene = sceneById.get(selectedSceneId);
+ const curatedScenes = HUB_CURATION_ORDER.map((id) => sceneById.get(id));
- const recommendedRooms = uniqueByRoomId([
- selectedRoom,
- ...curatedRooms,
- ...rooms,
+ const recommendedScenes = uniqueBySceneId([
+ selectedScene,
+ ...curatedScenes,
+ ...scenes,
]).slice(0, recommendedCount);
- const recommendedRoomIds = new Set(recommendedRooms.map((room) => room.id));
- const allRooms = [...recommendedRooms, ...rooms.filter((room) => !recommendedRoomIds.has(room.id))];
+ const recommendedSceneIds = new Set(recommendedScenes.map((scene) => scene.id));
+ const allScenes = [...recommendedScenes, ...scenes.filter((scene) => !recommendedSceneIds.has(scene.id))];
return {
- recommendedRooms,
- allRooms,
+ recommendedScenes,
+ allScenes,
};
};
diff --git a/src/entities/room/model/types.ts b/src/entities/scene/model/types.ts
similarity index 69%
rename from src/entities/room/model/types.ts
rename to src/entities/scene/model/types.ts
index 99a6a32..4177335 100644
--- a/src/entities/room/model/types.ts
+++ b/src/entities/scene/model/types.ts
@@ -1,16 +1,16 @@
-export type RoomTag = '저자극' | '움직임 적음' | '딥워크' | '감성';
+export type SceneTag = '저자극' | '움직임 적음' | '딥워크' | '감성';
-export interface RoomPresence {
+export interface ScenePresence {
focus: number;
break: number;
away: number;
}
-export interface RoomTheme {
+export interface SceneTheme {
id: string;
name: string;
description: string;
- tags: RoomTag[];
+ tags: SceneTag[];
recommendedSound: string;
recommendedSoundPresetId: string;
recommendedTimerPresetId: string;
@@ -21,7 +21,7 @@ export interface RoomTheme {
googleImageSearchUrl: string;
managedCardPhotoUrl: string | null;
activeMembers: number;
- presence: RoomPresence;
+ presence: ScenePresence;
previewImage: string;
previewGradient: string;
}
diff --git a/src/entities/session/model/mockSession.ts b/src/entities/session/model/mockSession.ts
index bbbdc6c..7f9a3fa 100644
--- a/src/entities/session/model/mockSession.ts
+++ b/src/entities/session/model/mockSession.ts
@@ -72,25 +72,25 @@ export const RECENT_THOUGHTS: RecentThought[] = [
{
id: 'thought-1',
text: '내일 미팅 전에 제안서 첫 문단만 다시 다듬기',
- roomName: '도서관',
+ sceneName: '도서관',
capturedAt: '방금 전',
},
{
id: 'thought-2',
text: '기획 문서의 핵심 흐름을 한 문장으로 정리해두기',
- roomName: '비 오는 창가',
+ sceneName: '비 오는 창가',
capturedAt: '24분 전',
},
{
id: 'thought-3',
text: '오후에 확인할 이슈 번호만 메모하고 지금 작업 복귀',
- roomName: '숲',
+ sceneName: '숲',
capturedAt: '1시간 전',
},
{
id: 'thought-4',
text: '리뷰 코멘트는 오늘 17시 이후에 한 번에 처리',
- roomName: '벽난로',
+ sceneName: '벽난로',
capturedAt: '어제',
},
];
diff --git a/src/entities/session/model/types.ts b/src/entities/session/model/types.ts
index 16de486..0bc0939 100644
--- a/src/entities/session/model/types.ts
+++ b/src/entities/session/model/types.ts
@@ -36,7 +36,7 @@ export interface FocusStatCard {
export interface RecentThought {
id: string;
text: string;
- roomName: string;
+ sceneName: string;
capturedAt: string;
isCompleted?: boolean;
}
diff --git a/src/entities/session/model/useThoughtInbox.ts b/src/entities/session/model/useThoughtInbox.ts
index e3090e1..eeb3352 100644
--- a/src/entities/session/model/useThoughtInbox.ts
+++ b/src/entities/session/model/useThoughtInbox.ts
@@ -24,15 +24,37 @@ const readStoredThoughts = () => {
return [];
}
- return parsed.filter((thought): thought is RecentThought => {
- return (
- thought &&
- typeof thought.id === 'string' &&
- typeof thought.text === 'string' &&
- typeof thought.roomName === 'string' &&
- typeof thought.capturedAt === 'string' &&
- (typeof thought.isCompleted === 'undefined' || typeof thought.isCompleted === 'boolean')
- );
+ return parsed.flatMap((thought): RecentThought[] => {
+ if (!thought || typeof thought !== 'object') {
+ return [];
+ }
+
+ const sceneName =
+ typeof thought.sceneName === 'string'
+ ? thought.sceneName
+ : typeof thought.roomName === 'string'
+ ? thought.roomName
+ : null;
+
+ if (
+ typeof thought.id !== 'string' ||
+ typeof thought.text !== 'string' ||
+ typeof thought.capturedAt !== 'string' ||
+ !sceneName ||
+ (typeof thought.isCompleted !== 'undefined' && typeof thought.isCompleted !== 'boolean')
+ ) {
+ return [];
+ }
+
+ return [
+ {
+ id: thought.id,
+ text: thought.text,
+ sceneName,
+ capturedAt: thought.capturedAt,
+ isCompleted: thought.isCompleted,
+ },
+ ];
});
} catch {
return [];
@@ -70,7 +92,7 @@ export const useThoughtInbox = () => {
};
}, []);
- const addThought = useCallback((text: string, roomName: string) => {
+ const addThought = useCallback((text: string, sceneName: string) => {
const trimmedText = text.trim();
if (!trimmedText) {
@@ -80,7 +102,7 @@ export const useThoughtInbox = () => {
const thought: RecentThought = {
id: `thought-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
text: trimmedText,
- roomName,
+ sceneName,
capturedAt: '방금 전',
isCompleted: false,
};
diff --git a/src/features/focus-session/api/focusSessionApi.ts b/src/features/focus-session/api/focusSessionApi.ts
index d31ec1f..288728b 100644
--- a/src/features/focus-session/api/focusSessionApi.ts
+++ b/src/features/focus-session/api/focusSessionApi.ts
@@ -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 => {
@@ -66,11 +66,12 @@ export const focusSessionApi = {
/**
* Backend Codex:
* - 현재 세션의 현재 phase를 일시정지한다.
+ * - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
* - phaseRemainingSeconds를 정확히 저장해 재개 시 이어서 동작하게 한다.
* - 이미 paused 상태여도 멱등적으로 최신 세션 상태를 반환한다.
*/
- pauseSession: async (sessionId: string): Promise => {
- return apiClient(`api/v1/focus-sessions/${sessionId}/pause`, {
+ pauseSession: async (): Promise => {
+ return apiClient('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 => {
- return apiClient(`api/v1/focus-sessions/${sessionId}/resume`, {
+ resumeSession: async (): Promise => {
+ return apiClient('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 => {
- return apiClient(`api/v1/focus-sessions/${sessionId}/restart-phase`, {
+ restartCurrentPhase: async (): Promise => {
+ return apiClient('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 => {
- return apiClient(`api/v1/focus-sessions/${sessionId}/complete`, {
+ completeSession: async (payload: CompleteFocusSessionRequest): Promise => {
+ return apiClient('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 => {
- return apiClient(`api/v1/focus-sessions/${sessionId}/abandon`, {
+ abandonSession: async (): Promise => {
+ return apiClient('api/v1/focus-sessions/current/abandon', {
method: 'POST',
expectNoContent: true,
});
diff --git a/src/features/focus-session/model/useFocusSessionEngine.ts b/src/features/focus-session/model/useFocusSessionEngine.ts
index 761c64a..c803101 100644
--- a/src/features/focus-session/model/useFocusSessionEngine.ts
+++ b/src/features/focus-session/model/useFocusSessionEngine.ts
@@ -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(),
'세션을 종료하지 못했어요.',
);
diff --git a/src/features/inbox/ui/InboxList.tsx b/src/features/inbox/ui/InboxList.tsx
index 38458f4..789acd0 100644
--- a/src/features/inbox/ui/InboxList.tsx
+++ b/src/features/inbox/ui/InboxList.tsx
@@ -38,7 +38,7 @@ export const InboxList = ({ thoughts, onCompleteThought, onDeleteThought, classN
{thought.text}
- {thought.roomName} · {thought.capturedAt}
+ {thought.sceneName} · {thought.capturedAt}