style(custom-entry-modal): 커스텀 입장 모달 톤과 크기를 안정화

맥락:
- 커스텀 입장 모달이 허브 대비 과도하게 어두워 화면 톤 일관성이 떨어졌다.
- 탭(공간/사운드/타이머) 전환 시 본문 높이가 달라져 모달 외곽 크기가 흔들리는 UX 이슈가 있었다.

변경사항:
- Modal 오버레이/패널/헤더/푸터 스타일을 밝은 글래스 톤으로 조정해 허브 톤과 맞췄다.
- CustomEntryModal의 탭 콘텐츠 영역을 고정 높이 컨테이너로 변경해 탭 전환 시 모달 전체 크기가 변하지 않도록 했다.
- 공간/사운드/타이머 옵션 버튼과 커스텀 타이머 입력 필드를 밝은 팔레트로 재정의했다.
- Tabs 컴포넌트를 밝은 표면 톤에 맞게 보정했다.
- 세션 문서(90_current_state, session_brief)에 이번 작업 내용/리스크를 반영했다.

검증:
- npx tsc --noEmit

세션-상태: 커스텀 입장 모달의 밝은 톤 정렬 및 탭 전환 크기 고정 반영 완료
세션-다음: RoomSheet/도크 패널의 인원수 기반 표현을 분위기형 정보로 전환
세션-리스크: 모달 고정 높이 적용으로 작은 화면에서 탭 본문 내부 스크롤 의존도가 증가할 수 있음
This commit is contained in:
2026-02-28 23:29:17 +09:00
parent 105c5785b8
commit 3280df7aa1
5 changed files with 127 additions and 90 deletions

View File

@@ -52,6 +52,10 @@ Last Updated: 2026-02-28
- 허브 전역 오버레이를 밝은 워시 중심으로 조정하고 그레인 강도 축소
- `RoomPreviewCard` 내부 콘텐츠 영역에 반투명 패널을 추가해 배경 변동과 텍스트 대비를 분리
- `GlassCard` 표면을 조금 더 밝고 가벼운 글래스 톤으로 보정
- 커스텀 입장 모달 톤/레이아웃 안정화:
- 모달 표면/오버레이를 허브 밝은 톤과 유사한 저대비 글래스로 전환
- `공간/사운드/타이머` 탭 콘텐츠 영역을 고정 높이로 통일해 전환 시 모달 크기 흔들림 제거
- 탭/옵션 버튼과 입력 필드를 밝은 팔레트로 정리해 가독성 개선
- 몰입 모드 ON 시 `/space` 크롬 정리:
- 상단 `Current Room` 블록 숨김
- 우상단 허브 버튼 소형 아이콘화
@@ -85,6 +89,7 @@ Last Updated: 2026-02-28
- 안내 카피가 HUD 목표 문구와 교체 표시되므로 정보 밀도 균형 점검 필요
- 밝아진 배경 구간에서 일부 white 텍스트의 대비가 환경(디스플레이 밝기)에 따라 약해질 수 있음
- 배경 필터/블러 적용으로 저사양 환경에서 스크롤 시 미세한 페인팅 비용 증가 가능성 존재
- 모달 본문 고정 높이 적용으로 작은 화면에서 내부 스크롤 의존도가 이전보다 높아질 수 있음
## CHANGED FILES
@@ -141,6 +146,9 @@ Last Updated: 2026-02-28
- `src/shared/ui/GlassCard.tsx`
- `src/widgets/app-hub/ui/AppHubWidget.tsx`
- `src/features/room-select/ui/RoomPreviewCard.tsx`
- `src/shared/ui/Modal.tsx`
- `src/shared/ui/Tabs.tsx`
- `src/features/custom-entry-modal/ui/CustomEntryModal.tsx`
## QUICK VERIFY
@@ -150,3 +158,4 @@ Last Updated: 2026-02-28
4. `/space`: 몰입 모드 ON 시 상단 룸 블록 숨김 + 레일 미니화 + HUD 저대비 적용
5. `/app`: 콘솔에 `button cannot be a descendant of button` hydration 에러가 재발하지 않음
6. `/app`: 숲/벽난로처럼 텍스처가 많은 룸 선택 시에도 카드 내부 텍스트 시인성이 유지됨
7. 커스텀 입장 모달 탭 전환(공간/사운드/타이머) 시 외곽 모달 크기가 유지됨

View File

@@ -45,6 +45,9 @@ Last Updated: 2026-02-28
- 허브 배경에 blur + 밝기 보정 + 저채도 필터를 적용해 배경 복잡도를 낮췄다.
- 전역 오버레이를 밝은 워시 중심으로 조정하고 그레인 강도를 낮췄다.
- `RoomPreviewCard` 내부에 반투명 정보 패널을 추가해 텍스트 대비를 안정화했다.
- 커스텀 입장 모달을 허브 톤과 맞춰 밝게 정리했다.
- `공간/사운드/타이머` 탭 콘텐츠 영역을 고정 높이로 바꿔 전환 시 모달 크기 변화가 없게 했다.
- 탭/옵션/입력 필드 스타일을 밝은 팔레트 기준으로 통일했다.
- 몰입 모드 ON 시 상단 룸 블록 숨김, 레일 미니화, HUD 저대비, 비네팅 강화가 적용된다.
- 이후 작업은 `docs/work.md`를 기준으로 실행한다.
@@ -58,6 +61,7 @@ Last Updated: 2026-02-28
- HUD 안내 문구와 목표 문구가 교체 노출되므로 정보 우선순위 점검이 필요함
- 밝은 배경 구간에서 white 텍스트 대비가 낮아질 수 있어 기기별 시인성 점검이 필요함
- 배경 blur/filter 적용으로 저사양 환경에서 렌더링 비용이 소폭 증가할 수 있음
- 모달 고정 높이로 인해 작은 화면에서는 탭 본문 내부 스크롤 사용 빈도가 늘 수 있음
## 상세 원문 위치

View File

@@ -2,7 +2,8 @@
import { ROOM_THEMES } from '@/entities/room';
import { SOUND_PRESETS, TIMER_PRESETS } from '@/entities/session';
import { Button, Chip, Modal, Tabs } from '@/shared/ui';
import { Button, Modal, Tabs } from '@/shared/ui';
import { cn } from '@/shared/lib/cn';
import {
type CustomEntrySelection,
useCustomEntryForm,
@@ -64,96 +65,119 @@ export const CustomEntryModal = ({
<div className="flex flex-col gap-2 sm:flex-row sm:justify-end">
<Button
variant="secondary"
className="!bg-white/10 !text-white hover:!bg-white/15"
className="!border !border-brand-dark/16 !bg-white/72 !text-brand-dark hover:!bg-white"
onClick={handleClose}
>
</Button>
<Button onClick={handleEnter}> </Button>
<Button className="!shadow-none" onClick={handleEnter}>
</Button>
</div>
}
>
<div className="space-y-5">
<Tabs value={activeTab} options={tabOptions} onChange={(value) => setActiveTab(value as 'theme' | 'sound' | 'timer')} />
<div className="h-[350px] overflow-hidden rounded-2xl border border-brand-dark/12 bg-white/50 p-3 sm:h-[390px]">
{activeTab === 'theme' ? (
<div className="h-full overflow-y-auto pr-1">
<div className="grid gap-2 sm:grid-cols-2">
{ROOM_THEMES.map((room) => (
<button
key={room.id}
type="button"
onClick={() => onSelectRoom(room.id)}
className={`rounded-xl border px-3 py-3 text-left transition-colors ${
className={cn(
'rounded-xl border px-3 py-3 text-left transition-colors',
selectedRoomId === room.id
? 'border-sky-200 bg-sky-300/22 text-sky-50'
: 'border-white/16 bg-white/5 text-white/85 hover:bg-white/10'
}`}
? 'border-brand-primary/45 bg-brand-soft/58 text-brand-dark'
: 'border-brand-dark/14 bg-white/72 text-brand-dark/84 hover:bg-white',
)}
>
<p className="text-sm font-medium">{room.name}</p>
<p className="mt-1 text-xs text-white/70">{room.description}</p>
<p className="mt-1 text-xs text-brand-dark/62">{room.description}</p>
</button>
))}
</div>
</div>
) : null}
{activeTab === 'sound' ? (
<div className="h-full overflow-y-auto pr-1">
<div className="flex flex-wrap gap-2">
{SOUND_PRESETS.map((preset) => (
<Chip
<button
key={preset.id}
active={selectedSoundId === preset.id}
type="button"
onClick={() => setSelectedSoundId(preset.id)}
className={cn(
'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
selectedSoundId === preset.id
? 'border-brand-primary/45 bg-brand-soft/58 text-brand-dark'
: 'border-brand-dark/14 bg-white/75 text-brand-dark/82 hover:bg-white',
)}
>
{preset.label}
</Chip>
</button>
))}
</div>
</div>
) : null}
{activeTab === 'timer' ? (
<div className="h-full overflow-y-auto pr-1">
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{TIMER_PRESETS.map((preset) => (
<Chip
<button
key={preset.id}
active={selectedTimerId === preset.id}
type="button"
onClick={() => setSelectedTimerId(preset.id)}
className={cn(
'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
selectedTimerId === preset.id
? 'border-brand-primary/45 bg-brand-soft/58 text-brand-dark'
: 'border-brand-dark/14 bg-white/75 text-brand-dark/82 hover:bg-white',
)}
>
{preset.label}
</Chip>
</button>
))}
</div>
{selectedTimerId === 'custom' ? (
<div className="grid gap-3 sm:grid-cols-2">
<label className="space-y-1 text-sm text-white/75">
<label className="space-y-1 text-sm text-brand-dark/76">
<span>()</span>
<input
type="number"
min={1}
value={customFocusMinutes}
onChange={(event) => setCustomFocusMinutes(event.target.value)}
className="w-full rounded-xl border border-white/20 bg-slate-900/70 px-3 py-2 text-white placeholder:text-white/45"
className="w-full rounded-xl border border-brand-dark/18 bg-white/86 px-3 py-2 text-brand-dark placeholder:text-brand-dark/45"
placeholder="25"
/>
</label>
<label className="space-y-1 text-sm text-white/75">
<label className="space-y-1 text-sm text-brand-dark/76">
<span>()</span>
<input
type="number"
min={1}
value={customBreakMinutes}
onChange={(event) => setCustomBreakMinutes(event.target.value)}
className="w-full rounded-xl border border-white/20 bg-slate-900/70 px-3 py-2 text-white placeholder:text-white/45"
className="w-full rounded-xl border border-brand-dark/18 bg-white/86 px-3 py-2 text-brand-dark placeholder:text-brand-dark/45"
placeholder="5"
/>
</label>
</div>
) : null}
</div>
</div>
) : null}
</div>
</div>
</Modal>
);
};

View File

@@ -52,7 +52,7 @@ export const Modal = ({
type="button"
aria-label="모달 닫기"
onClick={onClose}
className="absolute inset-0 bg-slate-950/78 backdrop-blur-sm"
className="absolute inset-0 bg-slate-900/52 backdrop-blur-[2px]"
/>
<div
@@ -60,36 +60,36 @@ export const Modal = ({
aria-modal="true"
aria-labelledby="custom-entry-modal-title"
className={cn(
'relative z-10 w-full max-w-3xl overflow-hidden rounded-3xl border border-white/20 bg-slate-950/92 shadow-[0_30px_100px_rgba(2,6,23,0.65)]',
'relative z-10 w-full max-w-3xl overflow-hidden rounded-3xl border border-white/35 bg-[linear-gradient(160deg,rgba(248,250,252,0.9)_0%,rgba(226,232,240,0.84)_52%,rgba(203,213,225,0.86)_100%)] shadow-[0_24px_90px_rgba(15,23,42,0.32)]',
reducedMotion
? 'transition-none'
: 'transition-transform duration-300 ease-out motion-reduce:transition-none',
)}
>
<header className="border-b border-white/12 px-6 py-5 sm:px-7">
<header className="border-b border-brand-dark/14 px-6 py-5 sm:px-7">
<div className="flex items-start justify-between gap-4">
<div>
<h2 id="custom-entry-modal-title" className="text-lg font-semibold text-white">
<h2 id="custom-entry-modal-title" className="text-lg font-semibold text-brand-dark">
{title}
</h2>
{description ? (
<p className="mt-1 text-sm text-white/65">{description}</p>
<p className="mt-1 text-sm text-brand-dark/64">{description}</p>
) : null}
</div>
<button
type="button"
onClick={onClose}
className="rounded-lg border border-white/20 px-2.5 py-1.5 text-xs text-white/80 transition hover:bg-white/10 hover:text-white"
className="rounded-lg border border-brand-dark/16 bg-white/58 px-2.5 py-1.5 text-xs text-brand-dark/82 transition hover:bg-white/84 hover:text-brand-dark"
>
</button>
</div>
</header>
<div className="max-h-[68vh] overflow-y-auto px-6 py-5 sm:px-7">{children}</div>
<div className="max-h-[72vh] overflow-y-auto px-6 py-5 sm:px-7">{children}</div>
{footer ? (
<footer className="border-t border-white/12 bg-slate-900/80 px-6 py-4 sm:px-7">{footer}</footer>
<footer className="border-t border-brand-dark/12 bg-white/56 px-6 py-4 sm:px-7">{footer}</footer>
) : null}
</div>
</div>

View File

@@ -15,7 +15,7 @@ interface TabsProps {
export const Tabs = ({ value, options, onChange }: TabsProps) => {
return (
<div className="inline-flex w-full rounded-xl bg-white/6 p-1 ring-1 ring-white/15">
<div className="inline-flex w-full rounded-xl bg-white/66 p-1 ring-1 ring-brand-dark/14">
{options.map((option) => (
<button
key={option.value}
@@ -24,8 +24,8 @@ export const Tabs = ({ value, options, onChange }: TabsProps) => {
className={cn(
'flex-1 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 motion-reduce:transition-none',
option.value === value
? 'bg-sky-300/22 text-sky-100'
: 'text-white/65 hover:bg-white/8 hover:text-white/90',
? 'bg-brand-soft/62 text-brand-dark'
: 'text-brand-dark/62 hover:bg-white hover:text-brand-dark/90',
)}
>
{option.label}