diff --git a/src/entities/room/model/rooms.ts b/src/entities/room/model/rooms.ts index ba68b16..c754b95 100644 --- a/src/entities/room/model/rooms.ts +++ b/src/entities/room/model/rooms.ts @@ -1,6 +1,16 @@ import type { CSSProperties } from 'react'; import type { RoomTheme } from './types'; +const HUB_CURATION_ORDER = [ + 'quiet-library', + 'rain-window', + 'dawn-cafe', + 'green-forest', + 'fireplace', +] as const; + +const HUB_RECOMMENDED_ROOM_COUNT = 3; + export const ROOM_THEMES: RoomTheme[] = [ { id: 'rain-window', @@ -228,3 +238,40 @@ export const getRoomBackgroundStyle = (room: RoomTheme): CSSProperties => { backgroundPosition: 'center, center', }; }; + +const uniqueByRoomId = (rooms: Array) => { + const seen = new Set(); + + return rooms.filter((room): room is RoomTheme => { + if (!room || seen.has(room.id)) { + return false; + } + + seen.add(room.id); + return true; + }); +}; + +export const getHubRoomSections = ( + rooms: RoomTheme[], + selectedRoomId: string, + recommendedCount = HUB_RECOMMENDED_ROOM_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 recommendedRooms = uniqueByRoomId([ + selectedRoom, + ...curatedRooms, + ...rooms, + ]).slice(0, recommendedCount); + + const recommendedRoomIds = new Set(recommendedRooms.map((room) => room.id)); + const allRooms = [...recommendedRooms, ...rooms.filter((room) => !recommendedRoomIds.has(room.id))]; + + return { + recommendedRooms, + allRooms, + }; +}; diff --git a/src/entities/session/index.ts b/src/entities/session/index.ts index 65013e8..0db20f4 100644 --- a/src/entities/session/index.ts +++ b/src/entities/session/index.ts @@ -1,2 +1,3 @@ export * from './model/mockSession'; export * from './model/types'; +export * from './model/useThoughtInbox'; diff --git a/src/entities/session/model/mockSession.ts b/src/entities/session/model/mockSession.ts index e5bde44..bbbdc6c 100644 --- a/src/entities/session/model/mockSession.ts +++ b/src/entities/session/model/mockSession.ts @@ -2,6 +2,7 @@ import type { CheckInPhrase, FocusStatCard, GoalChip, + RecentThought, ReactionOption, SoundPreset, TimerPreset, @@ -66,3 +67,30 @@ export const WEEKLY_STATS: FocusStatCard[] = [ { id: 'week-best-day', label: '최고 몰입일', value: '수요일', delta: '3h 30m' }, { id: 'week-consistency', label: '연속 달성', value: '4일', delta: '+1일' }, ]; + +export const RECENT_THOUGHTS: RecentThought[] = [ + { + id: 'thought-1', + text: '내일 미팅 전에 제안서 첫 문단만 다시 다듬기', + roomName: '도서관', + capturedAt: '방금 전', + }, + { + id: 'thought-2', + text: '기획 문서의 핵심 흐름을 한 문장으로 정리해두기', + roomName: '비 오는 창가', + capturedAt: '24분 전', + }, + { + id: 'thought-3', + text: '오후에 확인할 이슈 번호만 메모하고 지금 작업 복귀', + roomName: '숲', + capturedAt: '1시간 전', + }, + { + id: 'thought-4', + text: '리뷰 코멘트는 오늘 17시 이후에 한 번에 처리', + roomName: '벽난로', + capturedAt: '어제', + }, +]; diff --git a/src/entities/session/model/types.ts b/src/entities/session/model/types.ts index da5bd66..e405067 100644 --- a/src/entities/session/model/types.ts +++ b/src/entities/session/model/types.ts @@ -32,3 +32,10 @@ export interface FocusStatCard { value: string; delta: string; } + +export interface RecentThought { + id: string; + text: string; + roomName: string; + capturedAt: string; +} diff --git a/src/entities/session/model/useThoughtInbox.ts b/src/entities/session/model/useThoughtInbox.ts new file mode 100644 index 0000000..30f81b8 --- /dev/null +++ b/src/entities/session/model/useThoughtInbox.ts @@ -0,0 +1,107 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { RecentThought } from './types'; + +const THOUGHT_INBOX_STORAGE_KEY = 'viberoom:thought-inbox:v1'; +const MAX_THOUGHT_INBOX_ITEMS = 40; + +const readStoredThoughts = () => { + if (typeof window === 'undefined') { + return []; + } + + const raw = window.localStorage.getItem(THOUGHT_INBOX_STORAGE_KEY); + + if (!raw) { + return []; + } + + try { + const parsed = JSON.parse(raw); + + if (!Array.isArray(parsed)) { + 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' + ); + }); + } catch { + return []; + } +}; + +const persistThoughts = (thoughts: RecentThought[]) => { + if (typeof window === 'undefined') { + return; + } + + window.localStorage.setItem(THOUGHT_INBOX_STORAGE_KEY, JSON.stringify(thoughts)); +}; + +export const useThoughtInbox = () => { + const [thoughts, setThoughts] = useState(() => readStoredThoughts()); + + useEffect(() => { + persistThoughts(thoughts); + }, [thoughts]); + + useEffect(() => { + const handleStorage = (event: StorageEvent) => { + if (event.key !== THOUGHT_INBOX_STORAGE_KEY) { + return; + } + + setThoughts(readStoredThoughts()); + }; + + window.addEventListener('storage', handleStorage); + + return () => { + window.removeEventListener('storage', handleStorage); + }; + }, []); + + const addThought = useCallback((text: string, roomName: string) => { + const trimmedText = text.trim(); + + if (!trimmedText) { + return; + } + + setThoughts((current) => { + const next: RecentThought[] = [ + { + id: `thought-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, + text: trimmedText, + roomName, + capturedAt: '방금 전', + }, + ...current, + ].slice(0, MAX_THOUGHT_INBOX_ITEMS); + + return next; + }); + }, []); + + const clearThoughts = useCallback(() => { + setThoughts([]); + }, []); + + const recentThoughts = useMemo(() => thoughts.slice(0, 3), [thoughts]); + + return { + thoughts, + recentThoughts, + thoughtCount: thoughts.length, + addThought, + clearThoughts, + }; +}; diff --git a/src/features/room-select/ui/RoomPreviewCard.tsx b/src/features/room-select/ui/RoomPreviewCard.tsx index c14d71f..e0703d8 100644 --- a/src/features/room-select/ui/RoomPreviewCard.tsx +++ b/src/features/room-select/ui/RoomPreviewCard.tsx @@ -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 ( ); diff --git a/src/shared/config/appHubVisualMode.ts b/src/shared/config/appHubVisualMode.ts new file mode 100644 index 0000000..0b30f08 --- /dev/null +++ b/src/shared/config/appHubVisualMode.ts @@ -0,0 +1,20 @@ +export type AppHubVisualMode = 'light' | 'cinematic'; + +export const DEFAULT_APP_HUB_VISUAL_MODE: AppHubVisualMode = 'cinematic'; + +export const APP_HUB_VISUAL_OPTIONS = [ + { + id: 'light', + label: 'A안 · 라이트 감성', + description: '밝고 정돈된 무드', + }, + { + id: 'cinematic', + label: 'B안 · 무드 시네마틱', + description: '깊고 몰입적인 무드', + }, +] as const satisfies ReadonlyArray<{ + id: AppHubVisualMode; + label: string; + description: string; +}>; diff --git a/src/widgets/app-hub/ui/AppHubWidget.tsx b/src/widgets/app-hub/ui/AppHubWidget.tsx index 605da33..203b4a0 100644 --- a/src/widgets/app-hub/ui/AppHubWidget.tsx +++ b/src/widgets/app-hub/ui/AppHubWidget.tsx @@ -8,16 +8,24 @@ import { SOUND_PRESETS, TIMER_PRESETS, TODAY_ONE_LINER, + useThoughtInbox, type GoalChip, } from '@/entities/session'; import { MOCK_VIEWER } from '@/entities/user'; import { type CustomEntrySelection } from '@/features/custom-entry-modal'; import { useRoomSelection } from '@/features/room-select'; +import { + DEFAULT_APP_HUB_VISUAL_MODE, + type AppHubVisualMode, +} from '@/shared/config/appHubVisualMode'; +import { cn } from '@/shared/lib/cn'; import { useToast } from '@/shared/ui'; import { AppTopBar } from '@/widgets/app-top-bar/ui/AppTopBar'; import { CustomEntryWidget } from '@/widgets/custom-entry-widget/ui/CustomEntryWidget'; import { RoomsGalleryWidget } from '@/widgets/rooms-gallery-widget/ui/RoomsGalleryWidget'; import { StartRitualWidget } from '@/widgets/start-ritual-widget/ui/StartRitualWidget'; +import { ThoughtInboxSheet } from '@/widgets/thought-inbox-sheet'; +import { ThoughtSummaryEntryWidget } from '@/widgets/thought-summary-entry'; const buildSpaceQuery = ( roomId: string, @@ -43,6 +51,7 @@ const buildSpaceQuery = ( export const AppHubWidget = () => { const router = useRouter(); const { pushToast } = useToast(); + const { thoughts, thoughtCount, clearThoughts } = useThoughtInbox(); const { selectedRoom, selectedRoomId, selectRoom } = useRoomSelection( ROOM_THEMES[0].id, ); @@ -50,6 +59,10 @@ export const AppHubWidget = () => { const [goalInput, setGoalInput] = useState(''); const [selectedGoalId, setSelectedGoalId] = useState(null); const [isCustomEntryOpen, setCustomEntryOpen] = useState(false); + const [isThoughtInboxOpen, setThoughtInboxOpen] = useState(false); + + const visualMode: AppHubVisualMode = DEFAULT_APP_HUB_VISUAL_MODE; + const cinematic = visualMode === 'cinematic'; const enterSpace = (soundPresetId: string, timerLabel: string) => { const query = buildSpaceQuery( @@ -95,50 +108,85 @@ export const AppHubWidget = () => {
+
setThoughtInboxOpen(true)} + onOpenBilling={() => router.push('/settings')} /> -
-
- setCustomEntryOpen(true)} - /> +
+
+
+ setCustomEntryOpen(true)} + /> +
- +
+ +
+ +
+ setThoughtInboxOpen(true)} + /> +
@@ -150,6 +198,16 @@ export const AppHubWidget = () => { onClose={() => setCustomEntryOpen(false)} onEnter={handleCustomEnter} /> + + setThoughtInboxOpen(false)} + onClear={() => { + clearThoughts(); + pushToast({ title: '메모 인박스를 비웠어요' }); + }} + />
); }; diff --git a/src/widgets/app-top-bar/ui/AppTopBar.tsx b/src/widgets/app-top-bar/ui/AppTopBar.tsx index de452bd..bf1b4fe 100644 --- a/src/widgets/app-top-bar/ui/AppTopBar.tsx +++ b/src/widgets/app-top-bar/ui/AppTopBar.tsx @@ -1,23 +1,101 @@ import { MembershipTierBadge, type ViewerProfile } from '@/entities/user'; +import type { AppHubVisualMode } from '@/shared/config/appHubVisualMode'; +import { cn } from '@/shared/lib/cn'; import { ProfileMenu } from '@/features/profile-menu'; interface AppTopBarProps { user: ViewerProfile; oneLiner: string; onLogout: () => void; + visualMode?: AppHubVisualMode; + thoughtCount?: number; + onOpenThoughtInbox?: () => void; + onOpenBilling?: () => void; } -export const AppTopBar = ({ user, oneLiner, onLogout }: AppTopBarProps) => { +export const AppTopBar = ({ + user, + oneLiner, + onLogout, + visualMode = 'light', + thoughtCount = 0, + onOpenThoughtInbox, + onOpenBilling, +}: AppTopBarProps) => { + const cinematic = visualMode === 'cinematic'; + const thoughtCountLabel = thoughtCount > 99 ? '99+' : `${thoughtCount}`; + return ( -
-
-
-

VibeRoom

+
+
+
+

+ VibeRoom +

+
-

{oneLiner}

- -
+
+ {onOpenBilling ? ( + + ) : null} + {onOpenThoughtInbox ? ( + + ) : null}
diff --git a/src/widgets/rooms-gallery-widget/ui/RoomCatalogSheet.tsx b/src/widgets/rooms-gallery-widget/ui/RoomCatalogSheet.tsx new file mode 100644 index 0000000..eb5976f --- /dev/null +++ b/src/widgets/rooms-gallery-widget/ui/RoomCatalogSheet.tsx @@ -0,0 +1,132 @@ +import type { RoomTheme } from '@/entities/room'; +import type { AppHubVisualMode } from '@/shared/config/appHubVisualMode'; +import { cn } from '@/shared/lib/cn'; +import { Button } from '@/shared/ui'; + +interface RoomCatalogSheetProps { + isOpen: boolean; + visualMode: AppHubVisualMode; + rooms: RoomTheme[]; + selectedRoomId: string; + onClose: () => void; + onSelectRoom: (roomId: string) => void; +} + +export const RoomCatalogSheet = ({ + isOpen, + visualMode, + rooms, + selectedRoomId, + onClose, + onSelectRoom, +}: RoomCatalogSheetProps) => { + const cinematic = visualMode === 'cinematic'; + + if (!isOpen) { + return null; + } + + return ( + <> + +
+ +
+
    + {rooms.map((room) => { + const selected = room.id === selectedRoomId; + + return ( +
  • +
    +
    +

    + {room.name} +

    +

    + {room.description} +

    +

    + {room.vibeLabel} · {room.recommendedSound} · {room.recommendedTime} +

    +
    + +
    +
  • + ); + })} +
+
+
+ + + ); +}; diff --git a/src/widgets/rooms-gallery-widget/ui/RoomsGalleryWidget.tsx b/src/widgets/rooms-gallery-widget/ui/RoomsGalleryWidget.tsx index 759819e..72ab734 100644 --- a/src/widgets/rooms-gallery-widget/ui/RoomsGalleryWidget.tsx +++ b/src/widgets/rooms-gallery-widget/ui/RoomsGalleryWidget.tsx @@ -1,38 +1,104 @@ -import type { RoomTheme } from '@/entities/room'; +import { useState } from 'react'; +import { getHubRoomSections, type RoomTheme } from '@/entities/room'; import { RoomPreviewCard } from '@/features/room-select'; -import { GlassCard } from '@/shared/ui'; +import type { AppHubVisualMode } from '@/shared/config/appHubVisualMode'; +import { cn } from '@/shared/lib/cn'; +import { RoomCatalogSheet } from './RoomCatalogSheet'; interface RoomsGalleryWidgetProps { + visualMode: AppHubVisualMode; rooms: RoomTheme[]; selectedRoomId: string; onRoomSelect: (roomId: string) => void; } export const RoomsGalleryWidget = ({ + visualMode, rooms, selectedRoomId, onRoomSelect, }: RoomsGalleryWidgetProps) => { + const [isCatalogOpen, setCatalogOpen] = useState(false); + const cinematic = visualMode === 'cinematic'; + const selectedRoom = rooms.find((room) => room.id === selectedRoomId) ?? rooms[0]; + const { recommendedRooms, allRooms } = getHubRoomSections(rooms, selectedRoom.id, 5); + return ( - -
-

오늘의 공간

-

감정에 맞는 분위기 하나만 고르면 충분해요.

+
+
+
+

+ Scene +

+

+ 오늘의 공간 +

+
+
-
- {rooms.map((room) => ( - - ))} -
- + + +
+

+ 추천 공간 +

+
+
+ {recommendedRooms.map((room) => ( + + ))} +
+
+
+ + setCatalogOpen(false)} + onSelectRoom={onRoomSelect} + /> +
); }; diff --git a/src/widgets/space-exit-summary/index.ts b/src/widgets/space-exit-summary/index.ts new file mode 100644 index 0000000..a7d0e15 --- /dev/null +++ b/src/widgets/space-exit-summary/index.ts @@ -0,0 +1 @@ +export * from './ui/SpaceExitSummarySheet'; diff --git a/src/widgets/space-exit-summary/ui/SpaceExitSummarySheet.tsx b/src/widgets/space-exit-summary/ui/SpaceExitSummarySheet.tsx new file mode 100644 index 0000000..1a1b3cf --- /dev/null +++ b/src/widgets/space-exit-summary/ui/SpaceExitSummarySheet.tsx @@ -0,0 +1,86 @@ +import type { RecentThought } from '@/entities/session'; +import { Button } from '@/shared/ui'; + +interface SpaceExitSummarySheetProps { + isOpen: boolean; + roomName: string; + goal: string; + timerLabel: string; + recentThoughts: RecentThought[]; + onContinue: () => void; + onMoveToHub: () => void; +} + +export const SpaceExitSummarySheet = ({ + isOpen, + roomName, + goal, + timerLabel, + recentThoughts, + onContinue, + onMoveToHub, +}: SpaceExitSummarySheetProps) => { + if (!isOpen) { + return null; + } + + return ( + <> + + + +
+ + + ); +}; diff --git a/src/widgets/space-shell/ui/SpaceSkeletonWidget.tsx b/src/widgets/space-shell/ui/SpaceSkeletonWidget.tsx index 83fbb08..b8529e1 100644 --- a/src/widgets/space-shell/ui/SpaceSkeletonWidget.tsx +++ b/src/widgets/space-shell/ui/SpaceSkeletonWidget.tsx @@ -1,17 +1,21 @@ 'use client'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; +import { useRouter } from 'next/navigation'; import { useSearchParams } from 'next/navigation'; import { getRoomBackgroundStyle, getRoomById, ROOM_THEMES } from '@/entities/room'; -import { SOUND_PRESETS } from '@/entities/session'; +import { SOUND_PRESETS, useThoughtInbox, type RecentThought } from '@/entities/session'; import { useImmersionMode } from '@/features/immersion-mode'; import { cn } from '@/shared/lib/cn'; import { SpaceChromeWidget } from '@/widgets/space-chrome'; +import { SpaceExitSummarySheet } from '@/widgets/space-exit-summary'; import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud'; import { SpaceToolsDockWidget } from '@/widgets/space-tools-dock'; export const SpaceSkeletonWidget = () => { + const router = useRouter(); const searchParams = useSearchParams(); + const { addThought } = useThoughtInbox(); const roomId = searchParams.get('room') ?? ROOM_THEMES[0].id; const goal = searchParams.get('goal') ?? '오늘은 한 조각만 집중해요'; @@ -20,10 +24,32 @@ export const SpaceSkeletonWidget = () => { const room = useMemo(() => getRoomById(roomId) ?? ROOM_THEMES[0], [roomId]); const { isImmersionMode, toggleImmersionMode, exitImmersionMode } = useImmersionMode(); + const [isExitSummaryOpen, setExitSummaryOpen] = useState(false); + const [sessionThoughts, setSessionThoughts] = useState([]); const initialSoundPresetId = SOUND_PRESETS.find((preset) => preset.id === soundFromQuery)?.id ?? SOUND_PRESETS[0].id; + const handleExitRequested = () => { + setExitSummaryOpen(true); + if (isImmersionMode) { + exitImmersionMode(); + } + }; + + const handleThoughtCaptured = (note: string) => { + addThought(note, room.name); + setSessionThoughts((current) => [ + { + id: `session-thought-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, + text: note, + roomName: room.name, + capturedAt: '방금 전', + }, + ...current, + ]); + }; + return (
{ roomName={room.name} vibeLabel={room.vibeLabel} isImmersionMode={isImmersionMode} - onExitRequested={exitImmersionMode} + onExitRequested={handleExitRequested} />
@@ -96,6 +122,17 @@ export const SpaceSkeletonWidget = () => { initialSoundPresetId={initialSoundPresetId} isImmersionMode={isImmersionMode} onToggleImmersionMode={toggleImmersionMode} + onThoughtCaptured={handleThoughtCaptured} + /> + + setExitSummaryOpen(false)} + onMoveToHub={() => router.push('/app')} />
); diff --git a/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx b/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx index 118f80a..297c3b3 100644 --- a/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx +++ b/src/widgets/space-tools-dock/ui/SpaceToolsDockWidget.tsx @@ -20,6 +20,7 @@ interface SpaceToolsDockWidgetProps { initialSoundPresetId?: string; isImmersionMode: boolean; onToggleImmersionMode: () => void; + onThoughtCaptured?: (note: string) => void; } const TOOL_BUTTONS: Array<{ @@ -40,6 +41,7 @@ export const SpaceToolsDockWidget = ({ initialSoundPresetId, isImmersionMode, onToggleImmersionMode, + onThoughtCaptured, }: SpaceToolsDockWidgetProps) => { const { pushToast } = useToast(); const { lastCheckIn, recordCheckIn } = useCheckIn(); @@ -183,7 +185,10 @@ export const SpaceToolsDockWidget = ({ {activePanel === 'notes' ? ( pushToast({ title: `노트 추가: ${note}` })} + onNoteAdded={(note) => { + pushToast({ title: `노트 추가: ${note}` }); + onThoughtCaptured?.(note); + }} onNoteRemoved={() => pushToast({ title: '노트를 정리했어요' })} /> ) : null} diff --git a/src/widgets/start-ritual-widget/ui/StartRitualWidget.tsx b/src/widgets/start-ritual-widget/ui/StartRitualWidget.tsx index 43b9f0c..008365b 100644 --- a/src/widgets/start-ritual-widget/ui/StartRitualWidget.tsx +++ b/src/widgets/start-ritual-widget/ui/StartRitualWidget.tsx @@ -1,7 +1,10 @@ import type { GoalChip } from '@/entities/session'; +import type { AppHubVisualMode } from '@/shared/config/appHubVisualMode'; +import { cn } from '@/shared/lib/cn'; import { Button, Chip, GlassCard } from '@/shared/ui'; interface StartRitualWidgetProps { + visualMode: AppHubVisualMode; goalInput: string; selectedGoalId: string | null; goalChips: GoalChip[]; @@ -12,6 +15,7 @@ interface StartRitualWidgetProps { } export const StartRitualWidget = ({ + visualMode, goalInput, selectedGoalId, goalChips, @@ -20,60 +24,99 @@ export const StartRitualWidget = ({ onQuickEnter, onOpenCustomEntry, }: StartRitualWidgetProps) => { + const cinematic = visualMode === 'cinematic'; + return ( -
-

지금, 몰입을 시작해요

-

- 공간은 들어가서 바꿔도 괜찮아요. 오늘은 한 조각만. +

+

+ Start Ritual +

+

+ 한 줄 목표로 바로 시작 +

+

+ 비어 있어도 괜찮아요. 필요하면 공간 안에서 수정할 수 있어요.

-
-
- onGoalInputChange(event.target.value)} - placeholder="이번 세션 딱 1가지만 (예: 견적서 1페이지)" - className="w-full rounded-xl border border-brand-dark/16 bg-white/86 px-3.5 py-3 text-sm text-brand-dark placeholder:text-brand-dark/42 focus:border-brand-primary/42 focus:outline-none" - /> + onGoalInputChange(event.target.value)} + placeholder="예: 계약서 1페이지 정리" + className={cn( + 'w-full rounded-xl px-3.5 py-3 text-sm focus:outline-none', + cinematic + ? 'border border-white/18 bg-slate-900/56 text-white placeholder:text-white/40 focus:border-sky-200/42' + : 'border border-brand-dark/14 bg-white/90 text-brand-dark placeholder:text-brand-dark/40 focus:border-brand-primary/42', + )} + /> -
- {goalChips.map((chip) => ( - onGoalChipSelect(chip)} - className="!bg-white/74 !text-brand-dark/82 !ring-brand-dark/16 hover:!bg-white" - > - {chip.label} - - ))} -
-
-
+
+ {goalChips.slice(0, 4).map((chip) => ( + onGoalChipSelect(chip)} + className={cn( + cinematic + ? '!bg-white/10 !text-white/74 !ring-white/20 hover:!bg-white/14' + : '!bg-white/72 !text-brand-dark/74 !ring-brand-dark/12 hover:!bg-white', + )} + > + {chip.label} + + ))} +
-
-
- - -
-
+
+ + +
); }; diff --git a/src/widgets/thought-inbox-sheet/index.ts b/src/widgets/thought-inbox-sheet/index.ts new file mode 100644 index 0000000..a980fa7 --- /dev/null +++ b/src/widgets/thought-inbox-sheet/index.ts @@ -0,0 +1 @@ +export * from './ui/ThoughtInboxSheet'; diff --git a/src/widgets/thought-inbox-sheet/ui/ThoughtInboxSheet.tsx b/src/widgets/thought-inbox-sheet/ui/ThoughtInboxSheet.tsx new file mode 100644 index 0000000..8258e86 --- /dev/null +++ b/src/widgets/thought-inbox-sheet/ui/ThoughtInboxSheet.tsx @@ -0,0 +1,83 @@ +import type { RecentThought } from '@/entities/session'; +import { cn } from '@/shared/lib/cn'; + +interface ThoughtInboxSheetProps { + isOpen: boolean; + thoughts: RecentThought[]; + onClose: () => void; + onClear: () => void; +} + +export const ThoughtInboxSheet = ({ + isOpen, + thoughts, + onClose, + onClear, +}: ThoughtInboxSheetProps) => { + if (!isOpen) { + return null; + } + + return ( + <> + + +
+
+ +
+ {thoughts.length === 0 ? ( +

+ 아직 저장된 메모가 없어요. 공간에서 떠오른 생각을 남겨보세요. +

+ ) : ( +
    + {thoughts.map((thought) => ( +
  • +

    {thought.text}

    +
    + {thought.roomName} + {thought.capturedAt} +
    +
  • + ))} +
+ )} +
+
+ + + ); +}; diff --git a/src/widgets/thought-summary-entry/index.ts b/src/widgets/thought-summary-entry/index.ts new file mode 100644 index 0000000..4a00eea --- /dev/null +++ b/src/widgets/thought-summary-entry/index.ts @@ -0,0 +1 @@ +export * from './ui/ThoughtSummaryEntryWidget'; diff --git a/src/widgets/thought-summary-entry/ui/ThoughtSummaryEntryWidget.tsx b/src/widgets/thought-summary-entry/ui/ThoughtSummaryEntryWidget.tsx new file mode 100644 index 0000000..6ceb674 --- /dev/null +++ b/src/widgets/thought-summary-entry/ui/ThoughtSummaryEntryWidget.tsx @@ -0,0 +1,64 @@ +import type { AppHubVisualMode } from '@/shared/config/appHubVisualMode'; +import { cn } from '@/shared/lib/cn'; + +interface ThoughtSummaryEntryWidgetProps { + visualMode: AppHubVisualMode; + thoughtCount: number; + onOpen: () => void; +} + +export const ThoughtSummaryEntryWidget = ({ + visualMode, + thoughtCount, + onOpen, +}: ThoughtSummaryEntryWidgetProps) => { + const cinematic = visualMode === 'cinematic'; + + return ( + + ); +};