style(app-hub): 오늘의 공간 카드를 룸별 단색 팔레트로 전환

맥락:
- 오늘의 공간 카드가 이미지 기반 배경이라 색/질감이 복잡하게 보였고, 카드와 페이지 배경의 톤 연결이 약했다.
- 룸 이름별로 분리된 단색 팔레트를 사용해 카드 가독성과 허브의 색 일관성을 높일 필요가 있었다.

변경사항:
- entities/room 모델에 hubColor 필드를 추가하고 10개 룸에 겹치지 않는 단색 팔레트를 정의했다.
- RoomPreviewCard를 이미지 배경 대신 룸 고유 단색 배경으로 렌더링하도록 변경했다.
- RoomPreviewCard 내부 텍스트/칩 스타일을 단색 카드 대비에 맞춰 어두운 텍스트 중심으로 재정렬했다.
- AppHub 배경을 선택 룸의 hubColor로 동기화해 카드 선택과 페이지 배경이 같은 색 계열로 연결되게 조정했다.
- 세션 문서(90_current_state, session_brief)에 이번 작업 내역과 리스크를 반영했다.

검증:
- npx tsc --noEmit

세션-상태: /app 오늘의 공간 카드/허브 배경 단색 팔레트 전환 완료
세션-다음: RoomSheet/도크 패널의 인원수 기반 표현을 분위기형 정보로 전환
세션-리스크: 일부 디스플레이에서 단색 팔레트 간 체감 차이가 작아 보일 수 있어 기기별 색 분리도 점검 필요
This commit is contained in:
2026-02-28 23:53:34 +09:00
parent e041f6b25a
commit 22ff9e6e41
6 changed files with 41 additions and 24 deletions

View File

@@ -60,6 +60,10 @@ Last Updated: 2026-02-28
- `지금, 몰입을 시작해요` / `오늘의 공간` 컨테이너를 흰 표면 + 어두운 텍스트 톤으로 조정 - `지금, 몰입을 시작해요` / `오늘의 공간` 컨테이너를 흰 표면 + 어두운 텍스트 톤으로 조정
- 입력/칩/보조 버튼 색상도 라이트 카드 기준으로 재정렬 - 입력/칩/보조 버튼 색상도 라이트 카드 기준으로 재정렬
- 허브 배경은 가상 공간 이미지를 더 강한 blur로 노출해 카드 외곽 배경으로 유지 - 허브 배경은 가상 공간 이미지를 더 강한 blur로 노출해 카드 외곽 배경으로 유지
- `/app` 오늘의 공간 카드/배경을 룸별 단색 팔레트로 전환:
- `entities/room`에 룸별 `hubColor`를 추가하고 색이 서로 겹치지 않도록 분리
- 오늘의 공간 카드는 이미지 대신 룸 고유 단색 배경으로 렌더링
- 허브 페이지 배경도 선택된 룸의 동일 색으로 전환되도록 연결
- 몰입 모드 ON 시 `/space` 크롬 정리: - 몰입 모드 ON 시 `/space` 크롬 정리:
- 상단 `Current Room` 블록 숨김 - 상단 `Current Room` 블록 숨김
- 우상단 허브 버튼 소형 아이콘화 - 우상단 허브 버튼 소형 아이콘화
@@ -95,6 +99,7 @@ Last Updated: 2026-02-28
- 배경 필터/블러 적용으로 저사양 환경에서 스크롤 시 미세한 페인팅 비용 증가 가능성 존재 - 배경 필터/블러 적용으로 저사양 환경에서 스크롤 시 미세한 페인팅 비용 증가 가능성 존재
- 모달 본문 고정 높이 적용으로 작은 화면에서 내부 스크롤 의존도가 이전보다 높아질 수 있음 - 모달 본문 고정 높이 적용으로 작은 화면에서 내부 스크롤 의존도가 이전보다 높아질 수 있음
- 룸 프리뷰 카드 내부는 이미지 기반 다크 텍스트 체계라 컨테이너와 톤 차이가 남아 추가 톤 정리가 필요할 수 있음 - 룸 프리뷰 카드 내부는 이미지 기반 다크 텍스트 체계라 컨테이너와 톤 차이가 남아 추가 톤 정리가 필요할 수 있음
- 단색 팔레트가 일부 기기 색감에서 유사하게 보일 수 있어 실제 디스플레이 기준 재점검 필요
## CHANGED FILES ## CHANGED FILES
@@ -157,6 +162,9 @@ Last Updated: 2026-02-28
- `src/widgets/start-ritual-widget/ui/StartRitualWidget.tsx` - `src/widgets/start-ritual-widget/ui/StartRitualWidget.tsx`
- `src/widgets/rooms-gallery-widget/ui/RoomsGalleryWidget.tsx` - `src/widgets/rooms-gallery-widget/ui/RoomsGalleryWidget.tsx`
- `src/widgets/app-hub/ui/AppHubWidget.tsx` - `src/widgets/app-hub/ui/AppHubWidget.tsx`
- `src/entities/room/model/types.ts`
- `src/entities/room/model/rooms.ts`
- `src/features/room-select/ui/RoomPreviewCard.tsx`
## QUICK VERIFY ## QUICK VERIFY
@@ -168,3 +176,4 @@ Last Updated: 2026-02-28
6. `/app`: 숲/벽난로처럼 텍스처가 많은 룸 선택 시에도 카드 내부 텍스트 시인성이 유지됨 6. `/app`: 숲/벽난로처럼 텍스처가 많은 룸 선택 시에도 카드 내부 텍스트 시인성이 유지됨
7. 커스텀 입장 모달 탭 전환(공간/사운드/타이머) 시 외곽 모달 크기가 유지됨 7. 커스텀 입장 모달 탭 전환(공간/사운드/타이머) 시 외곽 모달 크기가 유지됨
8. `/app`: 시작 카드/공간 카드가 흰 표면 + 어두운 텍스트로 표시되고, 카드 밖 배경은 블러된 가상 공간으로 노출됨 8. `/app`: 시작 카드/공간 카드가 흰 표면 + 어두운 텍스트로 표시되고, 카드 밖 배경은 블러된 가상 공간으로 노출됨
9. `/app`: 오늘의 공간 카드가 룸별 단색으로 표시되고, 허브 배경도 선택 카드 색으로 동기화됨

View File

@@ -51,6 +51,9 @@ Last Updated: 2026-02-28
- `/app`의 핵심 카드 2개를 라이트 카드 톤으로 전환했다. - `/app`의 핵심 카드 2개를 라이트 카드 톤으로 전환했다.
- `지금, 몰입을 시작해요``오늘의 공간` 컨테이너를 흰 표면 + 어두운 텍스트로 정리했다. - `지금, 몰입을 시작해요``오늘의 공간` 컨테이너를 흰 표면 + 어두운 텍스트로 정리했다.
- 허브 배경 블러를 강화해 카드 외곽은 가상 공간 배경이 부드럽게 보이도록 조정했다. - 허브 배경 블러를 강화해 카드 외곽은 가상 공간 배경이 부드럽게 보이도록 조정했다.
- `/app` 오늘의 공간 카드를 룸별 단색 팔레트로 전환했다.
- `entities/room``hubColor`를 추가해 카드별 고유 단색을 분리했다.
- 허브 배경도 선택 룸의 같은 색으로 동기화해 톤 연결성을 높였다.
- 몰입 모드 ON 시 상단 룸 블록 숨김, 레일 미니화, HUD 저대비, 비네팅 강화가 적용된다. - 몰입 모드 ON 시 상단 룸 블록 숨김, 레일 미니화, HUD 저대비, 비네팅 강화가 적용된다.
- 이후 작업은 `docs/work.md`를 기준으로 실행한다. - 이후 작업은 `docs/work.md`를 기준으로 실행한다.
@@ -66,6 +69,7 @@ Last Updated: 2026-02-28
- 배경 blur/filter 적용으로 저사양 환경에서 렌더링 비용이 소폭 증가할 수 있음 - 배경 blur/filter 적용으로 저사양 환경에서 렌더링 비용이 소폭 증가할 수 있음
- 모달 고정 높이로 인해 작은 화면에서는 탭 본문 내부 스크롤 사용 빈도가 늘 수 있음 - 모달 고정 높이로 인해 작은 화면에서는 탭 본문 내부 스크롤 사용 빈도가 늘 수 있음
- 룸 프리뷰 카드의 이미지 기반 다크 톤과 라이트 컨테이너 사이 미세한 톤 차이가 남을 수 있음 - 룸 프리뷰 카드의 이미지 기반 다크 톤과 라이트 컨테이너 사이 미세한 톤 차이가 남을 수 있음
- 단색 팔레트가 일부 디스플레이에서 유사하게 보일 수 있어 기기별 색 분리도 점검이 필요함
## 상세 원문 위치 ## 상세 원문 위치

View File

@@ -10,6 +10,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Rain Focus', recommendedSound: 'Rain Focus',
recommendedTime: '밤', recommendedTime: '밤',
vibeLabel: '잔잔함', vibeLabel: '잔잔함',
hubColor: '#D6E6F7',
activeMembers: 32, activeMembers: 32,
presence: { focus: 23, break: 6, away: 3 }, presence: { focus: 23, break: 6, away: 3 },
previewImage: '/spaces/rain-window.jpg', previewImage: '/spaces/rain-window.jpg',
@@ -24,6 +25,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Cafe Murmur', recommendedSound: 'Cafe Murmur',
recommendedTime: '새벽', recommendedTime: '새벽',
vibeLabel: '포근함', vibeLabel: '포근함',
hubColor: '#F5DDCB',
activeMembers: 28, activeMembers: 28,
presence: { focus: 18, break: 7, away: 3 }, presence: { focus: 18, break: 7, away: 3 },
previewImage: '/spaces/dawn-cafe.jpg', previewImage: '/spaces/dawn-cafe.jpg',
@@ -38,6 +40,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Deep White', recommendedSound: 'Deep White',
recommendedTime: '오후', recommendedTime: '오후',
vibeLabel: '몰입', vibeLabel: '몰입',
hubColor: '#DCE4D1',
activeMembers: 41, activeMembers: 41,
presence: { focus: 31, break: 7, away: 3 }, presence: { focus: 31, break: 7, away: 3 },
previewImage: '/spaces/library.jpg', previewImage: '/spaces/library.jpg',
@@ -52,6 +55,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Ocean Breath', recommendedSound: 'Ocean Breath',
recommendedTime: '밤', recommendedTime: '밤',
vibeLabel: '차분함', vibeLabel: '차분함',
hubColor: '#CFE9EA',
activeMembers: 19, activeMembers: 19,
presence: { focus: 13, break: 4, away: 2 }, presence: { focus: 13, break: 4, away: 2 },
previewImage: '/spaces/ocean.jpg', previewImage: '/spaces/ocean.jpg',
@@ -66,6 +70,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Forest Hush', recommendedSound: 'Forest Hush',
recommendedTime: '오전', recommendedTime: '오전',
vibeLabel: '맑음', vibeLabel: '맑음',
hubColor: '#D1E7C9',
activeMembers: 26, activeMembers: 26,
presence: { focus: 17, break: 6, away: 3 }, presence: { focus: 17, break: 6, away: 3 },
previewImage: '/spaces/forest.jpg', previewImage: '/spaces/forest.jpg',
@@ -80,6 +85,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Fireplace', recommendedSound: 'Fireplace',
recommendedTime: '밤', recommendedTime: '밤',
vibeLabel: '온기', vibeLabel: '온기',
hubColor: '#F2D4C0',
activeMembers: 21, activeMembers: 21,
presence: { focus: 15, break: 4, away: 2 }, presence: { focus: 15, break: 4, away: 2 },
previewImage: '/spaces/fireplace.jpg', previewImage: '/spaces/fireplace.jpg',
@@ -94,6 +100,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Night Lo-fi', recommendedSound: 'Night Lo-fi',
recommendedTime: '심야', recommendedTime: '심야',
vibeLabel: '고요함', vibeLabel: '고요함',
hubColor: '#D9D3ED',
activeMembers: 34, activeMembers: 34,
presence: { focus: 24, break: 7, away: 3 }, presence: { focus: 24, break: 7, away: 3 },
previewImage: '/spaces/city-night.jpg', previewImage: '/spaces/city-night.jpg',
@@ -108,6 +115,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Cold Wind', recommendedSound: 'Cold Wind',
recommendedTime: '새벽', recommendedTime: '새벽',
vibeLabel: '선명함', vibeLabel: '선명함',
hubColor: '#D8E7F3',
activeMembers: 15, activeMembers: 15,
presence: { focus: 11, break: 3, away: 1 }, presence: { focus: 11, break: 3, away: 1 },
previewImage: '/spaces/snow-mountain.jpg', previewImage: '/spaces/snow-mountain.jpg',
@@ -122,6 +130,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Soft Daylight', recommendedSound: 'Soft Daylight',
recommendedTime: '오후', recommendedTime: '오후',
vibeLabel: '가벼움', vibeLabel: '가벼움',
hubColor: '#F6EDC7',
activeMembers: 27, activeMembers: 27,
presence: { focus: 18, break: 6, away: 3 }, presence: { focus: 18, break: 6, away: 3 },
previewImage: '/spaces/sun-window.jpg', previewImage: '/spaces/sun-window.jpg',
@@ -136,6 +145,7 @@ export const ROOM_THEMES: RoomTheme[] = [
recommendedSound: 'Deep Drone', recommendedSound: 'Deep Drone',
recommendedTime: '심야', recommendedTime: '심야',
vibeLabel: '깊음', vibeLabel: '깊음',
hubColor: '#D4DCF4',
activeMembers: 23, activeMembers: 23,
presence: { focus: 17, break: 4, away: 2 }, presence: { focus: 17, break: 4, away: 2 },
previewImage: '/spaces/outer-space.jpg', previewImage: '/spaces/outer-space.jpg',

View File

@@ -14,6 +14,7 @@ export interface RoomTheme {
recommendedSound: string; recommendedSound: string;
recommendedTime: string; recommendedTime: string;
vibeLabel: string; vibeLabel: string;
hubColor: string;
activeMembers: number; activeMembers: number;
presence: RoomPresence; presence: RoomPresence;
previewImage: string; previewImage: string;

View File

@@ -1,5 +1,4 @@
import type { RoomTheme } from '@/entities/room'; import type { RoomTheme } from '@/entities/room';
import { getRoomBackgroundStyle } from '@/entities/room';
import { cn } from '@/shared/lib/cn'; import { cn } from '@/shared/lib/cn';
interface RoomPreviewCardProps { interface RoomPreviewCardProps {
@@ -20,33 +19,29 @@ export const RoomPreviewCard = ({
className={cn( className={cn(
'group relative overflow-hidden rounded-2xl border p-4 text-left transition-all duration-250 motion-reduce:transition-none', 'group relative overflow-hidden rounded-2xl border p-4 text-left transition-all duration-250 motion-reduce:transition-none',
selected selected
? 'border-sky-200/85 shadow-[0_0_0_1px_rgba(186,230,253,0.9)]' ? 'border-brand-dark/28 shadow-[0_0_0_1px_rgba(48,77,109,0.18)]'
: 'border-white/18 hover:border-white/35', : 'border-brand-dark/16 hover:border-brand-dark/28',
)} )}
> >
<div <div
aria-hidden aria-hidden
className="absolute inset-0" className="absolute inset-0"
style={{ style={{
...getRoomBackgroundStyle(room), backgroundColor: room.hubColor,
filter: 'brightness(1.06) saturate(0.9)',
}} }}
/> />
<div aria-hidden className="absolute inset-0 bg-slate-900/26" />
<div aria-hidden className="absolute inset-0 bg-[linear-gradient(to_top,rgba(2,6,23,0.62),rgba(2,6,23,0.22))]" />
<div aria-hidden className="absolute inset-0 bg-[radial-gradient(120%_80%_at_50%_0%,rgba(255,255,255,0.18),rgba(255,255,255,0)_58%)]" />
<div className="relative space-y-3 rounded-xl border border-white/18 bg-slate-950/24 p-3 backdrop-blur-[1.5px]"> <div className="relative space-y-3 rounded-xl border border-brand-dark/10 bg-white/42 p-3 backdrop-blur-[1px]">
<div> <div>
<h3 className="text-base font-semibold text-white">{room.name}</h3> <h3 className="text-base font-semibold text-brand-dark">{room.name}</h3>
<p className="mt-1 text-xs text-white/82">{room.description}</p> <p className="mt-1 text-xs text-brand-dark/72">{room.description}</p>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{room.tags.map((tag) => ( {room.tags.map((tag) => (
<span <span
key={tag} key={tag}
className="inline-flex items-center gap-1 rounded-full bg-white/14 px-3 py-1.5 text-xs font-medium text-white/92 ring-1 ring-white/24" className="inline-flex items-center gap-1 rounded-full bg-white/62 px-3 py-1.5 text-xs font-medium text-brand-dark/86 ring-1 ring-brand-dark/12"
> >
{tag} {tag}
</span> </span>
@@ -54,14 +49,14 @@ export const RoomPreviewCard = ({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs text-white/84"> <p className="text-xs text-brand-dark/78">
: <span className="font-medium text-white">{room.recommendedSound}</span> : <span className="font-medium text-brand-dark">{room.recommendedSound}</span>
</p> </p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<span className="inline-flex items-center gap-1 rounded-full bg-sky-300/22 px-3 py-1.5 text-xs font-medium text-sky-100 ring-1 ring-sky-200/48"> <span className="inline-flex items-center gap-1 rounded-full bg-white/58 px-3 py-1.5 text-xs font-medium text-brand-dark/84 ring-1 ring-brand-dark/12">
· {room.recommendedTime} · {room.recommendedTime}
</span> </span>
<span className="inline-flex items-center gap-1 rounded-full bg-white/13 px-3 py-1.5 text-xs font-medium text-white/92 ring-1 ring-white/22"> <span className="inline-flex items-center gap-1 rounded-full bg-white/58 px-3 py-1.5 text-xs font-medium text-brand-dark/84 ring-1 ring-brand-dark/12">
· {room.vibeLabel} · {room.vibeLabel}
</span> </span>
</div> </div>

View File

@@ -2,7 +2,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { getRoomBackgroundStyle, ROOM_THEMES } from '@/entities/room'; import { ROOM_THEMES } from '@/entities/room';
import { import {
GOAL_CHIPS, GOAL_CHIPS,
SOUND_PRESETS, SOUND_PRESETS,
@@ -93,23 +93,21 @@ export const AppHubWidget = () => {
<div className="relative min-h-screen overflow-hidden text-white"> <div className="relative min-h-screen overflow-hidden text-white">
<div <div
aria-hidden aria-hidden
className="absolute inset-0 scale-[1.03] blur-[3px]" className="absolute inset-0 transition-colors duration-300"
style={{ style={{
...getRoomBackgroundStyle(selectedRoom), backgroundColor: selectedRoom.hubColor,
filter: 'brightness(1.12) saturate(0.82)',
}} }}
/> />
<div aria-hidden className="absolute inset-0 bg-slate-900/14" />
<div <div
aria-hidden aria-hidden
className="absolute inset-0 bg-[radial-gradient(circle_at_14%_0%,rgba(255,255,255,0.5),transparent_45%),radial-gradient(circle_at_86%_18%,rgba(191,219,254,0.3),transparent_44%),linear-gradient(165deg,rgba(248,250,252,0.26)_0%,rgba(226,232,240,0.24)_52%,rgba(203,213,225,0.3)_100%)]" className="absolute inset-0 bg-[radial-gradient(circle_at_14%_0%,rgba(255,255,255,0.52),transparent_48%),radial-gradient(circle_at_86%_18%,rgba(255,255,255,0.34),transparent_46%),linear-gradient(165deg,rgba(248,250,252,0.24)_0%,rgba(226,232,240,0.2)_52%,rgba(203,213,225,0.28)_100%)]"
/> />
<div <div
aria-hidden aria-hidden
className="absolute inset-0 opacity-[0.18]" className="absolute inset-0 opacity-[0.12]"
style={{ style={{
backgroundImage: backgroundImage:
"url('/textures/grain.png'), repeating-linear-gradient(0deg, rgba(255,255,255,0.024) 0 1px, transparent 1px 2px)", "url('/textures/grain.png'), repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0 1px, transparent 1px 2px)",
mixBlendMode: 'soft-light', mixBlendMode: 'soft-light',
}} }}
/> />