From ce1664f472718c6b07e37f2cc17428b1d597f36e Mon Sep 17 00:00:00 2001 From: corpi Date: Fri, 27 Feb 2026 14:14:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(space):=20=EC=82=AC=EC=9A=B4=EB=93=9C=20?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=EC=99=80=20=EB=AA=B0=EC=9E=85=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=ED=81=AC=EB=A1=AC=20=EC=A0=95=EB=A6=AC=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 맥락: - /space 하단 사운드 바를 제거하고 도구를 우측 도크에 수납해 배경 몰입을 강화하며, 몰입 모드 ON 체감을 높이기 위해 변경사항: - 하단 사운드 프리셋 바를 제거하고 도크에 🎧 Sound 패널을 추가 - features/sound-preset + widgets/sound-sheet를 추가해 프리셋 선택/믹서 UI(더미) 구성 - features/immersion-mode + shared/ui/Toggle을 추가하고 Quick 시트 토글과 연결 - 몰입 모드 ON 시 상단 Current Room 숨김, 허브 버튼 소형화, 레일 미니화, HUD 저대비, 비네팅/그레인 강화 - widgets/space-chrome를 신설해 /space 상단 크롬 렌더링을 분리 - docs/90_current_state.md, docs/session_brief.md 최신 상태로 갱신 검증: - npx tsc --noEmit 세션-상태: /space는 사운드 시트 기반 도크 구조와 몰입 모드 UI를 제공함 세션-다음: RoomSheetWidget 인원수 기반 정보를 큐레이션 표현으로 전환 세션-리스크: 터치 환경에서 미니 레일 발견성이 낮을 수 있어 보조 힌트가 필요할 수 있음 --- docs/90_current_state.md | 35 ++++- docs/session_brief.md | 8 +- src/features/immersion-mode/index.ts | 2 + .../immersion-mode/model/useImmersionMode.ts | 16 +++ .../immersion-mode/ui/ImmersionModeToggle.tsx | 25 ++++ src/features/sound-preset/index.ts | 2 + .../model/useSoundPresetSelection.ts | 50 +++++++ .../sound-preset/ui/SoundPresetControls.tsx | 128 +++++++++++++++++ src/shared/ui/Toggle.tsx | 39 +++++ src/shared/ui/index.ts | 1 + .../quick-sheet/ui/QuickSheetWidget.tsx | 43 +++--- src/widgets/sound-sheet/index.ts | 1 + .../sound-sheet/ui/SoundSheetWidget.tsx | 70 +++++++++ src/widgets/space-chrome/index.ts | 1 + .../space-chrome/ui/SpaceChromeWidget.tsx | 56 ++++++++ .../space-shell/ui/SpaceSkeletonWidget.tsx | 98 ++++++------- .../ui/SpaceTimerHudWidget.tsx | 42 +++++- .../model/useSpaceToolsDock.ts | 2 +- .../ui/SpaceToolsDockWidget.tsx | 135 +++++++++++++++--- 19 files changed, 640 insertions(+), 114 deletions(-) create mode 100644 src/features/immersion-mode/index.ts create mode 100644 src/features/immersion-mode/model/useImmersionMode.ts create mode 100644 src/features/immersion-mode/ui/ImmersionModeToggle.tsx create mode 100644 src/features/sound-preset/index.ts create mode 100644 src/features/sound-preset/model/useSoundPresetSelection.ts create mode 100644 src/features/sound-preset/ui/SoundPresetControls.tsx create mode 100644 src/shared/ui/Toggle.tsx create mode 100644 src/widgets/sound-sheet/index.ts create mode 100644 src/widgets/sound-sheet/ui/SoundSheetWidget.tsx create mode 100644 src/widgets/space-chrome/index.ts create mode 100644 src/widgets/space-chrome/ui/SpaceChromeWidget.tsx diff --git a/docs/90_current_state.md b/docs/90_current_state.md index 299deab..68b5b20 100644 --- a/docs/90_current_state.md +++ b/docs/90_current_state.md @@ -19,6 +19,15 @@ Last Updated: 2026-02-27 - `/app` Start Ritual에서 절차감을 높이던 `건너뛰기` 제거 - `/app`에서 `다시 시작(30초)` 제거 - `/space` HUD에 `features/restart-30s` 기반 `↻ 다시 시작 + 30초 배지` 추가 +- `/space` 하단 사운드 프리셋 바 제거, 오른쪽 `🎧 Sound` 시트로 이동 +- `features/sound-preset` + `widgets/sound-sheet` 추가 +- `features/immersion-mode` 추가, Quick 시트에서 몰입 모드 토글 연결 +- 몰입 모드 ON 시 `/space` 크롬 정리: + - 상단 `Current Room` 블록 숨김 + - 우상단 허브 버튼 소형 아이콘화 + - 오른쪽 아이콘 레일 기본 미니화(hover/click 시 확장) + - HUD 대비/불투명도 완화 + - 비네팅/그레인 강화 - `/app` 룸 카드의 인원수 기반 정보 제거 - `entities/room`에 분위기/추천 필드 추가: - `recommendedSound` @@ -28,14 +37,14 @@ Last Updated: 2026-02-27 ## NEXT -1. `/space` 상단 카피의 인원수 문구(`현재 n명`)를 분위기형 문구로 전환 -2. `RoomSheetWidget`/도크 패널의 인원수 기반 UI를 큐레이션형 정보로 재정의할지 정책 확정 +1. `RoomSheetWidget`/도크 패널의 인원수 기반 UI를 큐레이션형 정보로 재정의 +2. 몰입 모드에서 터치 환경(hover 없음) 레일 노출 UX를 보완할지 정책 확정 ## RISKS - `npm run build`는 네트워크 제한 시 Google Font fetch 실패 가능 -- 현재 워크트리는 다수 파일이 수정/추가된 상태라 커밋 단위 분리가 중요 -- 일부 문구가 여전히 실시간 지표처럼 읽힐 수 있으므로 카피 가이드 지속 점검 필요 +- 터치 기기에서 레일 미니 상태가 발견성 낮을 수 있어 추가 힌트가 필요할 수 있음 +- 일부 시트(예: Room)는 아직 인원수 중심 문구가 남아 있어 톤 불일치 가능성 존재 ## CHANGED FILES @@ -56,10 +65,26 @@ Last Updated: 2026-02-27 - `src/features/restart-30s/index.ts` - `src/features/restart-30s/model/useRestart30s.ts` - `src/features/restart-30s/ui/Restart30sAction.tsx` +- `src/features/immersion-mode/index.ts` +- `src/features/immersion-mode/model/useImmersionMode.ts` +- `src/features/immersion-mode/ui/ImmersionModeToggle.tsx` +- `src/features/sound-preset/index.ts` +- `src/features/sound-preset/model/useSoundPresetSelection.ts` +- `src/features/sound-preset/ui/SoundPresetControls.tsx` +- `src/shared/ui/Toggle.tsx` +- `src/widgets/sound-sheet/index.ts` +- `src/widgets/sound-sheet/ui/SoundSheetWidget.tsx` +- `src/widgets/space-chrome/index.ts` +- `src/widgets/space-chrome/ui/SpaceChromeWidget.tsx` +- `src/widgets/quick-sheet/ui/QuickSheetWidget.tsx` +- `src/widgets/space-shell/ui/SpaceSkeletonWidget.tsx` +- `src/widgets/space-tools-dock/model/useSpaceToolsDock.ts` +- `src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx` - `src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx` ## QUICK VERIFY 1. `/app`: 건너뛰기/다시 시작 노출 없음 2. `/app`: 룸 카드에 사람 수 문구 없음, 추천 정보 노출 -3. `/space`: HUD 근처 `↻ 다시 시작` 클릭 시 토스트 노출 +3. `/space`: 하단 사운드 바 없음, 오른쪽 `🎧 Sound` 시트에서 프리셋 선택 가능 +4. `/space`: 몰입 모드 ON 시 상단 룸 블록 숨김 + 레일 미니화 + HUD 저대비 적용 diff --git a/docs/session_brief.md b/docs/session_brief.md index fa7c927..36ee936 100644 --- a/docs/session_brief.md +++ b/docs/session_brief.md @@ -14,19 +14,21 @@ Last Updated: 2026-02-27 ## 현재 우선순위 -1. `/space` 상단의 인원 수 카피를 분위기형 카피로 전환 -2. `RoomSheetWidget`/도크 패널의 인원 수 UI를 큐레이션형으로 재정의할지 결정 +1. `RoomSheetWidget`/도크 패널의 인원 수 UI를 큐레이션형으로 전환 +2. 몰입 모드에서 터치 환경 레일 발견성(미니 핸들 UX) 보완 여부 결정 ## 최근 세션 상태 - 세션 복구용 문서/템플릿/스크립트가 준비되어 있다. - `workFlow.md`는 토큰 절약 모드를 사용한다. +- `/space` 하단 사운드 바를 제거하고 오른쪽 `🎧 Sound` 시트로 이동했다. +- 몰입 모드 ON 시 상단 룸 블록 숨김, 레일 미니화, HUD 저대비, 비네팅 강화가 적용된다. - 이후 작업은 `docs/work.md`를 기준으로 실행한다. ## 리스크 - 네트워크 제한 환경에서는 `npm run build` 시 Google Fonts fetch 실패 가능 -- 작업 범위가 넓을 때 커밋 단위가 커질 수 있으므로 주제 분리 점검 필요 +- 터치 환경에서 레일 미니 상태가 발견성 낮을 수 있어 UX 보완이 필요할 수 있음 ## 상세 원문 위치 diff --git a/src/features/immersion-mode/index.ts b/src/features/immersion-mode/index.ts new file mode 100644 index 0000000..b9226eb --- /dev/null +++ b/src/features/immersion-mode/index.ts @@ -0,0 +1,2 @@ +export * from './model/useImmersionMode'; +export * from './ui/ImmersionModeToggle'; diff --git a/src/features/immersion-mode/model/useImmersionMode.ts b/src/features/immersion-mode/model/useImmersionMode.ts new file mode 100644 index 0000000..8d02811 --- /dev/null +++ b/src/features/immersion-mode/model/useImmersionMode.ts @@ -0,0 +1,16 @@ +'use client'; + +import { useState } from 'react'; + +export const useImmersionMode = () => { + const [isImmersionMode, setImmersionMode] = useState(false); + + const toggleImmersionMode = () => { + setImmersionMode((current) => !current); + }; + + return { + isImmersionMode, + toggleImmersionMode, + }; +}; diff --git a/src/features/immersion-mode/ui/ImmersionModeToggle.tsx b/src/features/immersion-mode/ui/ImmersionModeToggle.tsx new file mode 100644 index 0000000..1e285ad --- /dev/null +++ b/src/features/immersion-mode/ui/ImmersionModeToggle.tsx @@ -0,0 +1,25 @@ +import { Toggle } from '@/shared/ui'; + +interface ImmersionModeToggleProps { + enabled: boolean; + onToggle: () => void; +} + +export const ImmersionModeToggle = ({ + enabled, + onToggle, +}: ImmersionModeToggleProps) => { + return ( +
+
+

몰입 모드

+

배경 우선으로 화면 요소를 최소화합니다.

+
+ +
+ ); +}; diff --git a/src/features/sound-preset/index.ts b/src/features/sound-preset/index.ts new file mode 100644 index 0000000..fc2eed7 --- /dev/null +++ b/src/features/sound-preset/index.ts @@ -0,0 +1,2 @@ +export * from './model/useSoundPresetSelection'; +export * from './ui/SoundPresetControls'; diff --git a/src/features/sound-preset/model/useSoundPresetSelection.ts b/src/features/sound-preset/model/useSoundPresetSelection.ts new file mode 100644 index 0000000..73c1dc6 --- /dev/null +++ b/src/features/sound-preset/model/useSoundPresetSelection.ts @@ -0,0 +1,50 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { SOUND_PRESETS } from '@/entities/session'; + +const TRACK_KEYS = ['white', 'rain', 'cafe', 'wave', 'fan'] as const; + +export type SoundTrackKey = (typeof TRACK_KEYS)[number]; + +const DEFAULT_TRACK_LEVELS: Record = { + white: 62, + rain: 55, + cafe: 48, + wave: 44, + fan: 40, +}; + +const DEFAULT_MASTER_VOLUME = 68; + +export const useSoundPresetSelection = (initialPresetId?: string) => { + const safeInitialPresetId = useMemo(() => { + const hasPreset = SOUND_PRESETS.some((preset) => preset.id === initialPresetId); + return hasPreset && initialPresetId ? initialPresetId : SOUND_PRESETS[0].id; + }, [initialPresetId]); + + const [selectedPresetId, setSelectedPresetId] = useState(safeInitialPresetId); + const [isMixerOpen, setMixerOpen] = useState(false); + const [isMuted, setMuted] = useState(false); + const [masterVolume, setMasterVolume] = useState(DEFAULT_MASTER_VOLUME); + const [trackLevels, setTrackLevels] = + useState>(DEFAULT_TRACK_LEVELS); + + const setTrackLevel = (track: SoundTrackKey, level: number) => { + setTrackLevels((current) => ({ ...current, [track]: level })); + }; + + return { + selectedPresetId, + setSelectedPresetId, + isMixerOpen, + setMixerOpen, + isMuted, + setMuted, + masterVolume, + setMasterVolume, + trackLevels, + setTrackLevel, + trackKeys: TRACK_KEYS, + }; +}; diff --git a/src/features/sound-preset/ui/SoundPresetControls.tsx b/src/features/sound-preset/ui/SoundPresetControls.tsx new file mode 100644 index 0000000..9f929b8 --- /dev/null +++ b/src/features/sound-preset/ui/SoundPresetControls.tsx @@ -0,0 +1,128 @@ +import { SOUND_PRESETS } from '@/entities/session'; +import { Chip, Toggle } from '@/shared/ui'; +import type { SoundTrackKey } from '../model/useSoundPresetSelection'; + +interface SoundPresetControlsProps { + selectedPresetId: string; + onSelectPreset: (presetId: string) => void; + isMixerOpen: boolean; + onToggleMixer: () => void; + isMuted: boolean; + onMuteChange: (next: boolean) => void; + masterVolume: number; + onMasterVolumeChange: (next: number) => void; + trackKeys: readonly SoundTrackKey[]; + trackLevels: Record; + onTrackLevelChange: (track: SoundTrackKey, level: number) => void; +} + +const TRACK_LABELS: Record = { + white: 'White', + rain: 'Rain', + cafe: 'Cafe', + wave: 'Wave', + fan: 'Fan', +}; + +const clampSliderValue = (value: number) => Math.max(0, Math.min(100, value)); + +export const SoundPresetControls = ({ + selectedPresetId, + onSelectPreset, + isMixerOpen, + onToggleMixer, + isMuted, + onMuteChange, + masterVolume, + onMasterVolumeChange, + trackKeys, + trackLevels, + onTrackLevelChange, +}: SoundPresetControlsProps) => { + return ( +
+
+

Preset

+
+ {SOUND_PRESETS.map((preset) => ( + + ))} +
+
+ + + + {isMixerOpen ? ( +
+
+ 마스터 볼륨 + {masterVolume}% +
+ + onMasterVolumeChange(clampSliderValue(Number(event.target.value))) + } + className="w-full accent-sky-300/80" + /> + +
+

뮤트

+ +
+ +
+ {trackKeys.map((track) => ( +
+
+ {TRACK_LABELS[track]} + {trackLevels[track]}% +
+ + onTrackLevelChange( + track, + clampSliderValue(Number(event.target.value)), + ) + } + className="w-full accent-sky-200/70" + /> +
+ ))} +
+
+ ) : null} +
+ ); +}; diff --git a/src/shared/ui/Toggle.tsx b/src/shared/ui/Toggle.tsx new file mode 100644 index 0000000..bbad643 --- /dev/null +++ b/src/shared/ui/Toggle.tsx @@ -0,0 +1,39 @@ +import { cn } from '@/shared/lib/cn'; + +interface ToggleProps { + checked: boolean; + onChange: (checked: boolean) => void; + ariaLabel: string; + className?: string; +} + +export const Toggle = ({ + checked, + onChange, + ariaLabel, + className, +}: ToggleProps) => { + return ( + + ); +}; diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 27173b6..3a1d074 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -5,3 +5,4 @@ export * from './GlassCard'; export * from './Modal'; export * from './Tabs'; export * from './Toast'; +export * from './Toggle'; diff --git a/src/widgets/quick-sheet/ui/QuickSheetWidget.tsx b/src/widgets/quick-sheet/ui/QuickSheetWidget.tsx index 3637665..c0b9711 100644 --- a/src/widgets/quick-sheet/ui/QuickSheetWidget.tsx +++ b/src/widgets/quick-sheet/ui/QuickSheetWidget.tsx @@ -1,13 +1,20 @@ 'use client'; import { useState } from 'react'; +import { ImmersionModeToggle } from '@/features/immersion-mode'; +import { Toggle } from '@/shared/ui'; interface QuickSheetWidgetProps { onClose: () => void; + isImmersionMode: boolean; + onToggleImmersionMode: () => void; } -export const QuickSheetWidget = ({ onClose }: QuickSheetWidgetProps) => { - const [immersionMode, setImmersionMode] = useState(false); +export const QuickSheetWidget = ({ + onClose, + isImmersionMode, + onToggleImmersionMode, +}: QuickSheetWidgetProps) => { const [minimalNotice, setMinimalNotice] = useState(false); return ( @@ -25,27 +32,19 @@ export const QuickSheetWidget = ({ onClose }: QuickSheetWidgetProps) => {
- + - +
+ 알림 최소화 + +

빠른 옵션 UI 목업입니다. 실제 동작은 연결하지 않았습니다. diff --git a/src/widgets/sound-sheet/index.ts b/src/widgets/sound-sheet/index.ts new file mode 100644 index 0000000..88a6a76 --- /dev/null +++ b/src/widgets/sound-sheet/index.ts @@ -0,0 +1 @@ +export * from './ui/SoundSheetWidget'; diff --git a/src/widgets/sound-sheet/ui/SoundSheetWidget.tsx b/src/widgets/sound-sheet/ui/SoundSheetWidget.tsx new file mode 100644 index 0000000..6212f44 --- /dev/null +++ b/src/widgets/sound-sheet/ui/SoundSheetWidget.tsx @@ -0,0 +1,70 @@ +import { + SoundPresetControls, + type SoundTrackKey, +} from '@/features/sound-preset'; + +interface SoundSheetWidgetProps { + onClose: () => void; + selectedPresetId: string; + onSelectPreset: (presetId: string) => void; + isMixerOpen: boolean; + onToggleMixer: () => void; + isMuted: boolean; + onMuteChange: (next: boolean) => void; + masterVolume: number; + onMasterVolumeChange: (next: number) => void; + trackKeys: readonly SoundTrackKey[]; + trackLevels: Record; + onTrackLevelChange: (track: SoundTrackKey, level: number) => void; +} + +export const SoundSheetWidget = ({ + onClose, + selectedPresetId, + onSelectPreset, + isMixerOpen, + onToggleMixer, + isMuted, + onMuteChange, + masterVolume, + onMasterVolumeChange, + trackKeys, + trackLevels, + onTrackLevelChange, +}: SoundSheetWidgetProps) => { + return ( +

+ ); +}; diff --git a/src/widgets/space-chrome/index.ts b/src/widgets/space-chrome/index.ts new file mode 100644 index 0000000..a20cd50 --- /dev/null +++ b/src/widgets/space-chrome/index.ts @@ -0,0 +1 @@ +export * from './ui/SpaceChromeWidget'; diff --git a/src/widgets/space-chrome/ui/SpaceChromeWidget.tsx b/src/widgets/space-chrome/ui/SpaceChromeWidget.tsx new file mode 100644 index 0000000..034a788 --- /dev/null +++ b/src/widgets/space-chrome/ui/SpaceChromeWidget.tsx @@ -0,0 +1,56 @@ +import Link from 'next/link'; +import { cn } from '@/shared/lib/cn'; + +interface SpaceChromeWidgetProps { + roomName: string; + vibeLabel: string; + isImmersionMode: boolean; +} + +export const SpaceChromeWidget = ({ + roomName, + vibeLabel, + isImmersionMode, +}: SpaceChromeWidgetProps) => { + return ( +
+
+
+ + V + +

VibeRoom

+
+ + + {isImmersionMode ? ( + <> + + 허브로 돌아가기 + + ) : ( + '허브로 돌아가기' + )} + +
+ + {!isImmersionMode ? ( +
+
+

Current Room

+

{roomName}

+

지금 분위기 · {vibeLabel}

+
+
+ ) : null} +
+ ); +}; diff --git a/src/widgets/space-shell/ui/SpaceSkeletonWidget.tsx b/src/widgets/space-shell/ui/SpaceSkeletonWidget.tsx index f036534..ea6da5f 100644 --- a/src/widgets/space-shell/ui/SpaceSkeletonWidget.tsx +++ b/src/widgets/space-shell/ui/SpaceSkeletonWidget.tsx @@ -1,11 +1,12 @@ 'use client'; -import { useEffect, useMemo, useState } from 'react'; -import Link from 'next/link'; +import { useMemo } from 'react'; import { useSearchParams } from 'next/navigation'; import { getRoomBackgroundStyle, getRoomById, ROOM_THEMES } from '@/entities/room'; import { SOUND_PRESETS } from '@/entities/session'; +import { useImmersionMode } from '@/features/immersion-mode'; import { cn } from '@/shared/lib/cn'; +import { SpaceChromeWidget } from '@/widgets/space-chrome'; import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud'; import { SpaceToolsDockWidget } from '@/widgets/space-tools-dock'; @@ -18,17 +19,11 @@ export const SpaceSkeletonWidget = () => { const soundFromQuery = searchParams.get('sound'); const room = useMemo(() => getRoomById(roomId) ?? ROOM_THEMES[0], [roomId]); - - const defaultSoundId = + const { isImmersionMode, toggleImmersionMode } = useImmersionMode(); + const initialSoundPresetId = SOUND_PRESETS.find((preset) => preset.id === soundFromQuery)?.id ?? SOUND_PRESETS[0].id; - const [selectedSoundId, setSelectedSoundId] = useState(defaultSoundId); - - useEffect(() => { - setSelectedSoundId(defaultSoundId); - }, [defaultSoundId]); - return (
{ className="absolute inset-0 w-full" style={getRoomBackgroundStyle(room)} /> -
+
+
-
-
- - V - -

VibeRoom

-
+ - - 허브로 돌아가기 - -
- -
-
-
-

Current Room

-

{room.name}

-

현재 {room.activeMembers}명 함께 집중 중

-
-
-
- -
-
- {SOUND_PRESETS.map((preset) => ( - - ))} -
-
+
- +
); diff --git a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx index c895336..5f57585 100644 --- a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx +++ b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx @@ -5,6 +5,7 @@ interface SpaceTimerHudWidgetProps { timerLabel: string; goal: string; className?: string; + isImmersionMode?: boolean; } const HUD_ACTIONS = [ @@ -17,20 +18,44 @@ export const SpaceTimerHudWidget = ({ timerLabel, goal, className, + isImmersionMode = false, }: SpaceTimerHudWidgetProps) => { return (
-
+
- + Focus - 25:00 - {timerLabel} + + 25:00 + + + {timerLabel} +
-

목표: {goal}

+

+ 목표: {goal} +

@@ -40,7 +65,12 @@ export const SpaceTimerHudWidget = ({ key={action.id} type="button" title={action.label} - className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-white/15 bg-white/8 text-sm text-white/82 transition-colors hover:bg-white/14 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80" + className={cn( + 'inline-flex h-8 w-8 items-center justify-center rounded-full border text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80', + isImmersionMode + ? 'border-white/10 bg-white/5 text-white/64 hover:bg-white/10' + : 'border-white/15 bg-white/8 text-white/82 hover:bg-white/14', + )} > {action.icon} {action.label} diff --git a/src/widgets/space-tools-dock/model/useSpaceToolsDock.ts b/src/widgets/space-tools-dock/model/useSpaceToolsDock.ts index 058cebd..3022b9e 100644 --- a/src/widgets/space-tools-dock/model/useSpaceToolsDock.ts +++ b/src/widgets/space-tools-dock/model/useSpaceToolsDock.ts @@ -2,7 +2,7 @@ import { useState } from 'react'; -export type SpaceToolPanel = 'room' | 'notes' | 'quick' | null; +export type SpaceToolPanel = 'sound' | 'room' | 'notes' | 'quick' | null; export const useSpaceToolsDock = () => { const [activePanel, setActivePanel] = useState(null); diff --git a/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx b/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx index 3e31aa3..118f80a 100644 --- a/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx +++ b/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx @@ -1,26 +1,33 @@ 'use client'; +import { useState } from 'react'; import type { RoomPresence } from '@/entities/room'; import { CHECK_IN_PHRASES, REACTION_OPTIONS } from '@/entities/session'; import { useCheckIn } from '@/features/check-in'; +import { useSoundPresetSelection } from '@/features/sound-preset'; import { useToast } from '@/shared/ui'; import { cn } from '@/shared/lib/cn'; import { NotesSheetWidget } from '@/widgets/notes-sheet'; import { QuickSheetWidget } from '@/widgets/quick-sheet'; import { RoomSheetWidget } from '@/widgets/room-sheet'; +import { SoundSheetWidget } from '@/widgets/sound-sheet'; import { useSpaceToolsDock } from '../model/useSpaceToolsDock'; interface SpaceToolsDockWidgetProps { roomName: string; activeMembers: number; presence: RoomPresence; + initialSoundPresetId?: string; + isImmersionMode: boolean; + onToggleImmersionMode: () => void; } const TOOL_BUTTONS: Array<{ - id: 'room' | 'notes' | 'quick'; + id: 'sound' | 'room' | 'notes' | 'quick'; icon: string; label: string; }> = [ + { id: 'sound', icon: '🎧', label: 'Sound' }, { id: 'room', icon: '👥', label: 'Room' }, { id: 'notes', icon: '📝', label: 'Notes' }, { id: 'quick', icon: '⚙️', label: 'Quick' }, @@ -30,10 +37,36 @@ export const SpaceToolsDockWidget = ({ roomName, activeMembers, presence, + initialSoundPresetId, + isImmersionMode, + onToggleImmersionMode, }: SpaceToolsDockWidgetProps) => { const { pushToast } = useToast(); const { lastCheckIn, recordCheckIn } = useCheckIn(); const { activePanel, closePanel, togglePanel } = useSpaceToolsDock(); + const [isRailExpanded, setRailExpanded] = useState(false); + const { + selectedPresetId, + setSelectedPresetId, + isMixerOpen, + setMixerOpen, + isMuted, + setMuted, + masterVolume, + setMasterVolume, + trackLevels, + setTrackLevel, + trackKeys, + } = useSoundPresetSelection(initialSoundPresetId); + + const isDockExpanded = !isImmersionMode || isRailExpanded || activePanel !== null; + + const handleClosePanel = () => { + closePanel(); + if (isImmersionMode) { + setRailExpanded(false); + } + }; const handleCheckIn = (message: string) => { recordCheckIn(message); @@ -44,39 +77,95 @@ export const SpaceToolsDockWidget = ({ pushToast({ title: `리액션: ${emoji}` }); }; + const handleToolSelect = (panel: (typeof TOOL_BUTTONS)[number]['id']) => { + if (isImmersionMode) { + setRailExpanded(true); + } + togglePanel(panel); + }; + return ( <> {activePanel ? ( + )) + ) : ( - ))} + )}
+ {activePanel === 'sound' ? ( + setMixerOpen((current) => !current)} + isMuted={isMuted} + onMuteChange={setMuted} + masterVolume={masterVolume} + onMasterVolumeChange={setMasterVolume} + trackKeys={trackKeys} + trackLevels={trackLevels} + onTrackLevelChange={setTrackLevel} + /> + ) : null} + {activePanel === 'room' ? ( handleReaction(reaction.emoji)} /> @@ -93,13 +182,19 @@ export const SpaceToolsDockWidget = ({ {activePanel === 'notes' ? ( pushToast({ title: `노트 추가: ${note}` })} onNoteRemoved={() => pushToast({ title: '노트를 정리했어요' })} /> ) : null} - {activePanel === 'quick' ? : null} + {activePanel === 'quick' ? ( + + ) : null} ); };