From 313ef88961b8724d4a8bbd448fe13ef10f55cd1b Mon Sep 17 00:00:00 2001 From: corpi Date: Fri, 27 Feb 2026 14:58:35 +0900 Subject: [PATCH] =?UTF-8?q?style(space-hud):=2030=EC=B4=88=20=EB=B3=B5?= =?UTF-8?q?=EA=B7=80=20=EC=95=A1=EC=85=98=EC=9D=84=20=EC=88=A8=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=20=ED=86=A4=EC=9C=BC=EB=A1=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 맥락: - /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 목표 문구와 안내 문구가 교체 노출되어 정보 우선순위 점검 필요 --- docs/90_current_state.md | 11 +++++ docs/session_brief.md | 5 ++ src/features/restart-30s/index.ts | 2 + src/features/restart-30s/model/copy.ts | 4 ++ .../restart-30s/model/useRestart30s.ts | 48 ++++++++++++++++++- .../restart-30s/ui/Restart30sAction.tsx | 21 ++++---- .../ui/SpaceTimerHudWidget.tsx | 26 +++++++--- 7 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 src/features/restart-30s/model/copy.ts diff --git a/docs/90_current_state.md b/docs/90_current_state.md index 01509a1..3b05453 100644 --- a/docs/90_current_state.md +++ b/docs/90_current_state.md @@ -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 diff --git a/docs/session_brief.md b/docs/session_brief.md index 7530b91..583e764 100644 --- a/docs/session_brief.md +++ b/docs/session_brief.md @@ -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 안내 문구와 목표 문구가 교체 노출되므로 정보 우선순위 점검이 필요함 ## 상세 원문 위치 diff --git a/src/features/restart-30s/index.ts b/src/features/restart-30s/index.ts index 2437d17..e1a0fbd 100644 --- a/src/features/restart-30s/index.ts +++ b/src/features/restart-30s/index.ts @@ -1 +1,3 @@ +export * from './model/copy'; +export * from './model/useRestart30s'; export * from './ui/Restart30sAction'; diff --git a/src/features/restart-30s/model/copy.ts b/src/features/restart-30s/model/copy.ts new file mode 100644 index 0000000..8f2b761 --- /dev/null +++ b/src/features/restart-30s/model/copy.ts @@ -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 = '준비됐어요. 집중으로 돌아가요.'; diff --git a/src/features/restart-30s/model/useRestart30s.ts b/src/features/restart-30s/model/useRestart30s.ts index a51e030..0d640cc 100644 --- a/src/features/restart-30s/model/useRestart30s.ts +++ b/src/features/restart-30s/model/useRestart30s.ts @@ -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(null); + const resetTimerRef = useRef(null); + const hintTimerRef = useRef(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, }; }; diff --git a/src/features/restart-30s/ui/Restart30sAction.tsx b/src/features/restart-30s/ui/Restart30sAction.tsx index 03b0707..f85647a 100644 --- a/src/features/restart-30s/ui/Restart30sAction.tsx +++ b/src/features/restart-30s/ui/Restart30sAction.tsx @@ -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 ( ); }; diff --git a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx index eed6a53..27dbe7a 100644 --- a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx +++ b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx @@ -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 (
- Focus + {isBreatheMode ? RECOVERY_30S_MODE_LABEL : 'Focus'}
-

- 목표: {goal} -

+ {hintMessage ? ( +

+ {hintMessage} +

+ ) : ( +

+ 목표: {goal} +

+ )}
@@ -83,7 +97,7 @@ export const SpaceTimerHudWidget = ({ ))}
- +