style(space-hud): 30초 복귀 액션을 숨 고르기 톤으로 리브랜딩
맥락:
- /space 하단 HUD의 30초 복귀 액션을 저자극 감성 톤으로 정리해 복귀 행동의 심리적 부담을 낮춘다.
변경사항:
- restart-30s feature에 카피 상수(model/copy)를 추가하고 버튼/모드/안내 문구를 중앙 관리로 전환했다.
- useRestart30s에 더미 상태 전환(isBreatheMode, hintMessage)을 추가해 클릭 후 2초 내 안내 노출과 자동 복귀를 구현했다.
- HUD에서 30초 진입 중 상태 라벨을 BREATHE로 표시하고, 안내 문구를 목표 라인에 저대비로 잠깐 노출하도록 조정했다.
- 30초 액션 버튼을 "숨 고르기 30초" + 🌬️ 형태의 보조 액션 톤으로 변경했다.
- 세션 복구 문서(90_current_state, session_brief)에 이번 작업 상태/리스크를 반영했다.
검증:
- npx tsc --noEmit
세션-상태: /space 30초 복귀 액션 카피 리브랜딩 및 HUD 더미 안내 반영 완료
세션-다음: RoomSheet/도크 인원수 기반 카피를 큐레이션형 표현으로 전환
세션-리스크: HUD 목표 문구와 안내 문구가 교체 노출되어 정보 우선순위 점검 필요
This commit is contained in:
@@ -36,6 +36,10 @@ Last Updated: 2026-02-27
|
||||
- 눌렀을 때 즉시 fill이 보이도록 CSS keyframes 기반으로 교체
|
||||
- 완료 후 fill이 0으로 역방향 축소되는 현상 제거(짧은 유지 후 언마운트)
|
||||
- fill 끝단을 직선 형태로 정리(rounded 캡 제거)
|
||||
- 30초 복귀 액션 카피를 감성 라운지 톤으로 리브랜딩:
|
||||
- 버튼 라벨: `숨 고르기 30초`
|
||||
- 진입 시 HUD 모드 라벨: `BREATHE`
|
||||
- 클릭 시 저자극 안내 문구 노출(2초 이내 미니 안내 + 토스트)
|
||||
- 몰입 모드 ON 시 `/space` 크롬 정리:
|
||||
- 상단 `Current Room` 블록 숨김
|
||||
- 우상단 허브 버튼 소형 아이콘화
|
||||
@@ -56,6 +60,7 @@ Last Updated: 2026-02-27
|
||||
3. `/space` 헤더 최소화 스타일을 테마별(밝은 배경) 대비 점검
|
||||
4. 롱프레스 나가기 버튼의 터치 환경 힌트(첫 진입 안내) 필요 여부 판단
|
||||
5. Room 시트 인원수 기반 카피를 분위기형 카피로 치환
|
||||
6. 30초 복귀 안내 카피의 노출 위치(HUD 내 vs 토스트 전용) AB 점검
|
||||
|
||||
## RISKS
|
||||
|
||||
@@ -65,6 +70,7 @@ Last Updated: 2026-02-27
|
||||
- safe-area 값이 작은 기기에서는 HUD가 너무 낮게 느껴질 수 있어 세부 조정 여지 존재
|
||||
- 롱프레스 인터랙션은 첫 사용자에게 즉시 인지되지 않을 수 있어 시각적 힌트 필요 가능성 있음
|
||||
- bar/ring 진행 표시는 서로 다른 구현(JS/CSS)이라 동기화 규칙 변경 시 회귀 점검이 필요
|
||||
- 안내 카피가 HUD 목표 문구와 교체 표시되므로 정보 밀도 균형 점검 필요
|
||||
|
||||
## CHANGED FILES
|
||||
|
||||
@@ -107,6 +113,11 @@ Last Updated: 2026-02-27
|
||||
- `src/features/exit-hold/model/useHoldToConfirm.ts`
|
||||
- `src/features/exit-hold/ui/ExitHoldButton.tsx`
|
||||
- `src/app/globals.css`
|
||||
- `src/features/restart-30s/model/copy.ts`
|
||||
- `src/features/restart-30s/model/useRestart30s.ts`
|
||||
- `src/features/restart-30s/ui/Restart30sAction.tsx`
|
||||
- `src/features/restart-30s/index.ts`
|
||||
- `src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx`
|
||||
|
||||
## QUICK VERIFY
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ Last Updated: 2026-02-27
|
||||
- 롱프레스 bar 진행 표시를 CSS keyframes 기반으로 교체해 즉시 가시성을 개선했다.
|
||||
- 완료 후 fill이 0으로 역방향 축소되는 현상을 제거했다.
|
||||
- fill 끝단은 직선 형태로 정리했다.
|
||||
- 30초 복귀 액션을 감성 라운지 톤으로 리브랜딩했다.
|
||||
- 버튼 라벨을 `숨 고르기 30초`로 변경했다.
|
||||
- HUD 모드 라벨은 진입 시 `BREATHE`로 표시된다.
|
||||
- 클릭 시 저자극 안내 문구를 HUD 미니 안내 + 토스트로 노출한다.
|
||||
- 몰입 모드 ON 시 상단 룸 블록 숨김, 레일 미니화, HUD 저대비, 비네팅 강화가 적용된다.
|
||||
- 이후 작업은 `docs/work.md`를 기준으로 실행한다.
|
||||
|
||||
@@ -40,6 +44,7 @@ Last Updated: 2026-02-27
|
||||
- safe-area가 작은 기기에서는 HUD 하단 간격 체감이 과도할 수 있어 미세 조정이 필요할 수 있음
|
||||
- 롱프레스 인터랙션은 신규 사용자에게 즉시 인지되지 않을 수 있어 보조 카피가 필요할 수 있음
|
||||
- bar/ring 진행 구현 방식이 달라 향후 진행 규칙 변경 시 회귀 확인이 필요함
|
||||
- HUD 안내 문구와 목표 문구가 교체 노출되므로 정보 우선순위 점검이 필요함
|
||||
|
||||
## 상세 원문 위치
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './model/copy';
|
||||
export * from './model/useRestart30s';
|
||||
export * from './ui/Restart30sAction';
|
||||
|
||||
4
src/features/restart-30s/model/copy.ts
Normal file
4
src/features/restart-30s/model/copy.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const RECOVERY_30S_BUTTON_LABEL = '숨 고르기 30초';
|
||||
export const RECOVERY_30S_MODE_LABEL = 'BREATHE';
|
||||
export const RECOVERY_30S_TOAST_MESSAGE = '잠깐 숨 고르고, 다시 천천히 시작해요.';
|
||||
export const RECOVERY_30S_COMPLETE_MESSAGE = '준비됐어요. 집중으로 돌아가요.';
|
||||
@@ -1,18 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useToast } from '@/shared/ui';
|
||||
import {
|
||||
RECOVERY_30S_BUTTON_LABEL,
|
||||
RECOVERY_30S_TOAST_MESSAGE,
|
||||
} from './copy';
|
||||
|
||||
const MODE_DURATION_MS = 2000;
|
||||
const HINT_DURATION_MS = 1800;
|
||||
|
||||
export const useRestart30s = () => {
|
||||
const { pushToast } = useToast();
|
||||
const [isBreatheMode, setBreatheMode] = useState(false);
|
||||
const [hintMessage, setHintMessage] = useState<string | null>(null);
|
||||
const resetTimerRef = useRef<number | null>(null);
|
||||
const hintTimerRef = useRef<number | null>(null);
|
||||
|
||||
const clearTimers = () => {
|
||||
if (resetTimerRef.current !== null) {
|
||||
window.clearTimeout(resetTimerRef.current);
|
||||
resetTimerRef.current = null;
|
||||
}
|
||||
|
||||
if (hintTimerRef.current !== null) {
|
||||
window.clearTimeout(hintTimerRef.current);
|
||||
hintTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimers();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const triggerRestart = () => {
|
||||
clearTimers();
|
||||
setBreatheMode(true);
|
||||
setHintMessage(RECOVERY_30S_TOAST_MESSAGE);
|
||||
|
||||
pushToast({
|
||||
title: '30초 리스타트(더미)',
|
||||
description: '실제 리스타트 동작은 아직 연결되지 않았어요.',
|
||||
title: RECOVERY_30S_BUTTON_LABEL,
|
||||
description: RECOVERY_30S_TOAST_MESSAGE,
|
||||
});
|
||||
|
||||
hintTimerRef.current = window.setTimeout(() => {
|
||||
setHintMessage(null);
|
||||
}, HINT_DURATION_MS);
|
||||
|
||||
resetTimerRef.current = window.setTimeout(() => {
|
||||
setBreatheMode(false);
|
||||
}, MODE_DURATION_MS);
|
||||
};
|
||||
|
||||
return {
|
||||
isBreatheMode,
|
||||
hintMessage,
|
||||
triggerRestart,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import { useRestart30s } from '../model/useRestart30s';
|
||||
import { RECOVERY_30S_BUTTON_LABEL } from '../model/copy';
|
||||
|
||||
interface Restart30sActionProps {
|
||||
onTrigger: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Restart30sAction = ({ className }: Restart30sActionProps) => {
|
||||
const { triggerRestart } = useRestart30s();
|
||||
|
||||
export const Restart30sAction = ({
|
||||
onTrigger,
|
||||
className,
|
||||
}: Restart30sActionProps) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={triggerRestart}
|
||||
onClick={onTrigger}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 text-xs text-white/66 transition hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80',
|
||||
'inline-flex items-center gap-1.5 text-xs text-white/66 transition hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span aria-hidden className="text-[13px]">
|
||||
↻
|
||||
</span>
|
||||
<span>다시 시작</span>
|
||||
<span className="rounded-full border border-white/25 bg-white/8 px-2 py-0.5 text-[10px] text-white/72">
|
||||
30초
|
||||
🌬️
|
||||
</span>
|
||||
<span>{RECOVERY_30S_BUTTON_LABEL}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import { Restart30sAction } from '@/features/restart-30s';
|
||||
import {
|
||||
RECOVERY_30S_MODE_LABEL,
|
||||
Restart30sAction,
|
||||
useRestart30s,
|
||||
} from '@/features/restart-30s';
|
||||
|
||||
interface SpaceTimerHudWidgetProps {
|
||||
timerLabel: string;
|
||||
@@ -20,6 +26,8 @@ export const SpaceTimerHudWidget = ({
|
||||
className,
|
||||
isImmersionMode = false,
|
||||
}: SpaceTimerHudWidgetProps) => {
|
||||
const { isBreatheMode, hintMessage, triggerRestart } = useRestart30s();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -45,7 +53,7 @@ export const SpaceTimerHudWidget = ({
|
||||
isImmersionMode ? 'text-white/45' : 'text-white/62',
|
||||
)}
|
||||
>
|
||||
Focus
|
||||
{isBreatheMode ? RECOVERY_30S_MODE_LABEL : 'Focus'}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
@@ -59,9 +67,15 @@ export const SpaceTimerHudWidget = ({
|
||||
{timerLabel}
|
||||
</span>
|
||||
</div>
|
||||
{hintMessage ? (
|
||||
<p className={cn('truncate text-[11px]', isImmersionMode ? 'text-white/54' : 'text-white/64')}>
|
||||
{hintMessage}
|
||||
</p>
|
||||
) : (
|
||||
<p className={cn('truncate text-[11px]', isImmersionMode ? 'text-white/44' : 'text-white/58')}>
|
||||
목표: {goal}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
@@ -83,7 +97,7 @@ export const SpaceTimerHudWidget = ({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Restart30sAction />
|
||||
<Restart30sAction onTrigger={triggerRestart} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user