From 6bf3336aec74d5a01d79f0a0d67476083f30e69c Mon Sep 17 00:00:00 2001 From: corpi Date: Sun, 15 Mar 2026 11:46:21 +0900 Subject: [PATCH] =?UTF-8?q?fix(flow):=20=EA=B8=B0=ED=9A=8D-=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B6=88=EC=9D=BC=EC=B9=98=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/10_refocus_system_spec.md | 8 +- docs/12_core_loop_execution_roadmap.md | 6 +- ...ace_intent_card_collapsed_expanded_spec.md | 8 +- docs/15_app_stats_entry_flow_spec.md | 6 +- docs/90_current_state.md | 18 +- docs/session_brief.md | 17 +- src/shared/i18n/messages/app.ts | 2 +- src/shared/i18n/messages/space.ts | 24 +- .../ui/FocusDashboardWidget.tsx | 360 ++++++++++-------- .../ui/SpaceFocusHudWidget.tsx | 10 +- .../ui/SpaceWorkspaceWidget.tsx | 2 +- 11 files changed, 262 insertions(+), 199 deletions(-) diff --git a/docs/10_refocus_system_spec.md b/docs/10_refocus_system_spec.md index 732945b..b48f231 100644 --- a/docs/10_refocus_system_spec.md +++ b/docs/10_refocus_system_spec.md @@ -1,6 +1,6 @@ # 10. Refocus System Spec -Last Updated: 2026-03-14 +Last Updated: 2026-03-15 이 문서는 VibeRoom의 `Refocus System`을 제품 대표 경험으로 설계하기 위한 상세 기준 문서다. @@ -263,9 +263,9 @@ UI: 행동: -- 여기까지 끝내기 -- 다음 목표로 이어가기 -- 잠깐 쉬기 +- 여기서 마무리하기 +- 다음 블록 이어가기 +- 잠시 비우기 완료는 celebration보다 **closure quality**가 중요하다. diff --git a/docs/12_core_loop_execution_roadmap.md b/docs/12_core_loop_execution_roadmap.md index 5ac92a4..e579a77 100644 --- a/docs/12_core_loop_execution_roadmap.md +++ b/docs/12_core_loop_execution_roadmap.md @@ -1,6 +1,6 @@ # 12. Core Loop Execution Roadmap -Last Updated: 2026-03-14 +Last Updated: 2026-03-15 이 문서는 VibeRoom의 핵심 제품 기획을 **어떤 순서로 구현까지 연결할지**를 정의하는 실행 로드맵이다. @@ -176,6 +176,8 @@ VibeRoom은 아래 방식으로 진행한다. - 상세 기획 문서 작성 완료 - 1차 snapshot 구현 완료 +- `/app -> /stats -> /app` entry flow 구현 완료 +- `/space` complete 이후 secondary teaser 구현 완료 - 남은 것은 recovery 집계 연결, ritual fit, Free / Pro gating 문서: @@ -371,4 +373,4 @@ Away / Return이 끼어들기 전, 다음으로 예정된 축은 아래 두 가 현재 위치: -> `3. Break refinement`를 마무리했고, `4. Weekly Review`의 entry flow 구현을 시작했다. +> `3. Break refinement`를 마무리했고, `4. Weekly Review`의 entry flow 구현까지 마친 상태다. diff --git a/docs/13_space_intent_card_collapsed_expanded_spec.md b/docs/13_space_intent_card_collapsed_expanded_spec.md index 61f0d89..0f5aa71 100644 --- a/docs/13_space_intent_card_collapsed_expanded_spec.md +++ b/docs/13_space_intent_card_collapsed_expanded_spec.md @@ -1,6 +1,6 @@ # 13. `/space` Intent Card Collapsed / Expanded Spec -Last Updated: 2026-03-14 +Last Updated: 2026-03-15 이 문서는 `/space` 좌상단 목표 카드의 **collapsed / expanded 구조**를 정의한다. @@ -120,8 +120,8 @@ Intent Card는 아래 2개 상태만 가진다. - decision tray는 반드시 명시적 액션으로만 닫는다 - `취소` - `적용` - - `여기까지 끝내기` - - `잠깐 쉬기` + - `여기서 마무리하기` + - `잠시 비우기` - `다음 블록 이어가기` - 즉, `/space`에서 가볍게 접히는 것은 `expanded rail`뿐이고, 실질적인 state change layer는 dismissible popover로 취급하지 않는다 @@ -185,7 +185,7 @@ Intent Card는 아래 2개 상태만 가진다. - 우측 정렬된 quiet text action 1개 - `이번 목표 완료` - `다시 방향` 상시 버튼은 두지 않는다 -- refocus는 goal 클릭을 통해 진입한다 +- refocus는 expanded 상태의 명시적 `수정` 액션으로만 진입한다 --- diff --git a/docs/15_app_stats_entry_flow_spec.md b/docs/15_app_stats_entry_flow_spec.md index 5a4dc34..38c754e 100644 --- a/docs/15_app_stats_entry_flow_spec.md +++ b/docs/15_app_stats_entry_flow_spec.md @@ -1,6 +1,6 @@ # 15. `/app -> /stats -> /app` Weekly Review Entry Flow Spec -Last Updated: 2026-03-14 +Last Updated: 2026-03-15 이 문서는 VibeRoom의 `Weekly Review`를 **어디서, 왜, 어떤 타이밍에 열어야 하는지**를 정의하는 진입 플로우 문서다. @@ -170,12 +170,12 @@ review가 가장 유의미한 순간도 바로 여기다. - 첫 1~2회 사용 - 세션 기록이 거의 없는 주 -- current session이 있고 resume가 primary인 상황에서 above-the-fold 경쟁이 심할 때 +- current session이 있고 review 데이터도 거의 없는 상황 이 경우: - `/app` 메인 hero는 single-goal commitment에 집중 -- review teaser는 숨기거나 below-the-fold에 둔다 +- resume 상태에서는 hero 아래 큰 teaser 대신, resume card 안의 조용한 secondary entry로 review를 연다 --- diff --git a/docs/90_current_state.md b/docs/90_current_state.md index 1e0b9f1..be26113 100644 --- a/docs/90_current_state.md +++ b/docs/90_current_state.md @@ -1,12 +1,12 @@ # 90. Current State -Last Updated: 2026-03-14 +Last Updated: 2026-03-15 ## DONE - `/app` single-goal commitment gate 재구성: - 2-step `goal -> ritual` flow 제거 - - current session이 있으면 `Resume` UI를 우선 노출하고, `/space`로 바로 이어가기만 제안 + - current session이 있으면 `Resume` UI를 우선 노출하고, `/space`로 바로 이어가기만 제안하되 review entry는 조용한 secondary link로 유지 - current session이 없으면 `goal 1개 + optional microStep 1개 + primary CTA`만 남긴 direct start 구조로 단순화 - `환경 세팅`, `블록 정리`, scene/sound/timer 선택을 메인 진입 경로에서 제거 - suggestion chip은 planner가 아니라 입력 마찰을 줄이는 용도로만 유지 @@ -20,12 +20,12 @@ Last Updated: 2026-03-14 - 한 번에 하나의 recovery tray만 열리도록 hierarchy를 고정 - `/space` Refocus System slice 2 구현: - pause prompt의 `이대로 이어가기`가 실제 resume 동작으로 연결 - - goal complete tray에 `여기까지 끝내기` 경로 추가 + - goal complete tray에 `여기서 마무리하기` 경로 추가 - 현재 세션을 다음 목표 입력 없이도 정상 완료 처리할 수 있게 연결 - goal complete / rest / next-goal의 세 분기가 UI와 동작 모두에서 분리됨 - `/space` Refocus System slice 3 구현: - goal complete tray가 초기부터 input form을 강요하지 않도록 progressive disclosure 구조로 변경 - - `여기까지 끝내기 / 잠깐 쉬기 / 다음 목표 이어가기`를 먼저 제안하고, 다음 목표 입력은 선택 시에만 펼쳐지게 정리 + - `여기서 마무리하기 / 잠시 비우기 / 다음 목표 이어가기`를 먼저 제안하고, 다음 목표 입력은 선택 시에만 펼쳐지게 정리 - next-beat prompt에 현재 goal 문맥을 함께 보여주도록 보강 - `/space` Refocus System slice 4 구현: - pause / next-beat / complete / refocus tray의 glass material, hairline, spacing을 공통 규칙으로 정리 @@ -37,7 +37,7 @@ Last Updated: 2026-03-14 - 짧은 탭 전환에는 반응하지 않도록 hidden threshold를 둠 - 돌아왔을 때 focus가 아직 running이면 `Return` tray에서 `이어서 하기 / 한 조각 다시 잡기`를 제안 - 자리를 비운 사이 focus가 끝나 break phase가 되었으면 standard break 대신 `Return` tray를 먼저 띄움 - - 이 경우 `지금부터 쉬기 / 다음 목표 이어가기 / 한 조각 다시 잡기`를 선택할 수 있음 + - 이 경우 `쉬기 이어가기 / 다음 목표 이어가기 / 한 조각 다시 잡기`를 선택할 수 있음 - `다음 목표 이어가기`는 `Goal Complete` next view로 바로 연결됨 - `/space` Pause tray premium polish: - tray 폭과 열림 높이를 키워 긴 한국어 카피가 잘리지 않게 조정 @@ -45,12 +45,12 @@ Last Updated: 2026-03-14 - option row spacing, radius, chevron 위치를 보정해 급조된 버튼 묶음 느낌을 완화 - `/space` Pause / Break / Return tone 분리 1차 구현: - `Return(focus)`와 `Return(break)`가 같은 tray처럼 보이지 않도록 break tray에 emerald tint release tone 도입 - - `Goal Complete`의 `잠깐 쉬기` 선택도 같은 break 계열 material로 연결 + - `Goal Complete`의 `잠시 비우기` 선택도 같은 break 계열 material로 연결 - timer HUD는 break phase에서 더 가벼운 emerald 계열 glass로 보정해 focus/pause와 구분되게 조정 - `/space` Pause / Break / Return copy + interaction polish: - `Pause`는 `멈춘 이유` 대신 `다시 시작할 한 줄`을 중심으로 카피를 다시 정리 - - `Return(focus)`는 `멈춘 자리에서 이어가기`, `Return(break)`는 `지금부터 쉬기 / 다음 블록 이어가기` 중심으로 재서술 - - `Goal Complete`는 `다음 블록 이어가기 / 잠깐 쉬기 / 여기까지 끝내기` 순의 선택 tray를 먼저 보여주고, 다음 블록 입력은 이후 단계에서만 열리게 정리 + - `Return(focus)`는 `멈춘 자리에서 이어가기`, `Return(break)`는 `쉬기 이어가기 / 다음 블록 이어가기` 중심으로 재서술 + - `Goal Complete`는 `다음 블록 이어가기 / 잠시 비우기 / 여기서 마무리하기` 순의 선택 tray를 먼저 보여주고, 다음 블록 입력은 이후 단계에서만 열리게 정리 - choice/next view의 헤더와 설명도 각각 다른 감정 상태에 맞춰 분리 - `/space` Pause / Break / Return motion polish 1차 구현: - `Pause` tray는 빠르게 다시 붙잡는 recovery reveal로 조정 @@ -127,7 +127,7 @@ Last Updated: 2026-03-14 - `/app` teaser와 review return hint도 Pro에서 더 구체적인 next-session handoff 톤으로 표시된다 - `/space` secondary review teaser 4차 연결: - goal complete로 setup 상태로 돌아왔을 때만 setup drawer 아래에 low-emphasis review teaser가 보인다 - - teaser는 `주간 review 보기`로 `/stats?review=weekly&origin=space-complete`를 연다 + - teaser는 `주간 review 보기`로 `/stats`를 열고, 방금 끝낸 흐름 반영을 과장하지 않는 카피만 사용한다 - 다시 시작하거나 dismiss하면 사라지며, live execution 중에는 보이지 않는다 - paywall / plan / landing 메시지 재정렬: - paywall 가치 포인트를 multi-queue, rituals, weekly review 중심으로 재작성 diff --git a/docs/session_brief.md b/docs/session_brief.md index 455e797..ff1a7fc 100644 --- a/docs/session_brief.md +++ b/docs/session_brief.md @@ -1,6 +1,6 @@ # Session Brief -Last Updated: 2026-03-14 +Last Updated: 2026-03-15 세션 시작 시 항상 읽는 초소형 스냅샷 문서. @@ -27,7 +27,7 @@ Last Updated: 2026-03-14 - microStep 완료 후에는 checklist가 아니라 `다음 한 조각이 있나요?` prompt로만 이어진다. - recovery UI는 `paused / refocus / next-beat / complete` 중 하나만 열리도록 단일 overlay 상태로 묶였다. - `/space` goal complete 종료 경로를 복구했다. - - `여기까지 끝내기`로 현재 목표를 다음 목표 입력 없이도 정상 완료 처리할 수 있다. + - `여기서 마무리하기`로 현재 목표를 다음 목표 입력 없이도 정상 완료 처리할 수 있다. - pause prompt의 `이대로 이어가기`는 단순 닫기가 아니라 실제 resume으로 연결된다. - `/space` goal complete / next beat를 덜 form스럽게 정리했다. - goal complete는 처음부터 input을 요구하지 않고, 선택지를 먼저 보여준 뒤 `다음 목표 이어가기`를 선택했을 때만 입력이 열린다. @@ -35,14 +35,14 @@ Last Updated: 2026-03-14 - `/space` recovery tray material과 선택 위계를 같은 패밀리로 맞추기 시작했다. - pause / next-beat / complete tray가 공통 dark-glass shell을 공유한다. - inline 링크 중심이던 선택지를 quiet option row 구조로 바꿔, checklist보다 recovery decision처럼 읽히게 정리했다. - - `Goal Complete`는 `여기까지 끝내기 / 잠깐 쉬기 / 다음 목표 이어가기`를 같은 tray 안의 선택 행으로 제시한다. +- `Goal Complete`는 `다음 블록 이어가기 / 잠시 비우기 / 여기서 마무리하기`를 같은 tray 안의 선택 행으로 제시한다. - `Refocus`는 같은 shell 안에서 field / action 톤을 통일해 다른 tray와 같은 제품군처럼 보이게 맞추는 중이다. - `/space` Away / Return Recovery를 구현했다. - `visibilitychange`, `pagehide`, sleep/wake gap 기반 감지를 추가했다. - 짧은 탭 전환에는 반응하지 않도록 hidden threshold를 둬 오탐을 줄였다. - 돌아왔을 때 focus가 계속 running이면 `Return` tray가 `이어서 하기 / 한 조각 다시 잡기`를 제안한다. - 자리를 비운 사이 focus가 끝나 break phase가 되었으면 standard break 대신 `Return` tray가 먼저 뜬다. - - 이 경우 `지금부터 쉬기 / 다음 목표 이어가기 / 한 조각 다시 잡기` 중 하나를 고를 수 있다. + - 이 경우 `쉬기 이어가기 / 다음 목표 이어가기 / 한 조각 다시 잡기` 중 하나를 고를 수 있다. - `다음 목표 이어가기`는 goal complete next view로 바로 연결된다. - pause tray의 visual polish를 진행했다. - tray 폭과 max-height를 늘려 한국어 제목/설명 잘림을 줄였다. @@ -50,12 +50,12 @@ Last Updated: 2026-03-14 - option row의 radius, padding, chevron 정렬을 보정해 더 차분한 recovery panel처럼 읽히게 했다. - `Pause / Break / Return`의 감정 톤 분리를 시작했다. - `Return(break)`은 focus 복귀 tray와 같은 재질을 쓰지 않고, 더 부드러운 emerald tint release tone으로 분리했다. - - `Goal Complete`의 `잠깐 쉬기` 선택도 같은 release tone으로 연결했다. + - `Goal Complete`의 `잠시 비우기` 선택도 같은 release tone으로 연결했다. - timer HUD도 break phase에서는 더 가벼운 emerald 계열 material로 바뀌어 pause/focus와 다르게 읽히도록 정리 중이다. - `Pause / Break / Return`의 카피와 CTA 위계를 2차로 분리했다. - `Pause`는 `멈춘 이유`보다 `다시 시작할 한 줄`에 집중하는 recovery tone으로 다시 썼다. - - `Return(focus)`는 `이어가기`, `Return(break)`는 `지금부터 쉬기 / 다음 블록 이어가기` 중심으로 문구를 분리했다. - - `Goal Complete`는 `마무리 / 쉬기 / 이어가기`의 선택 tray가 먼저 보이고, 다음 블록 입력은 이후 단계에서만 열리도록 더 선명해졌다. + - `Return(focus)`는 `이어가기`, `Return(break)`는 `쉬기 이어가기 / 다음 블록 이어가기` 중심으로 문구를 분리했다. +- `Goal Complete`는 `이어가기 / 잠시 비우기 / 마무리하기`의 선택 tray가 먼저 보이고, 다음 블록 입력은 이후 단계에서만 열리도록 더 선명해졌다. - `Pause / Break / Return`의 motion polish 1차를 반영했다. - `Pause`는 빠르게 다시 붙잡는 recovery reveal로, - `Return(focus)`는 재진입에 맞는 짧은 settle motion으로, @@ -86,6 +86,8 @@ Last Updated: 2026-03-14 - `/app`에서 `/stats`로 들어가는 primary path 1차가 생겼다. - current session이 없고 최근 7일 데이터가 충분하면 hero 아래에 weekly review teaser가 보인다. - teaser는 `/stats`로 이동시키되, main start CTA보다 낮은 강조로 유지한다. +- `/app` Resume 상태에서도 weekly review entry가 보이게 정리했다. + - review primary entry가 active session 상태에서 사라지지 않도록, resume card 안에 조용한 secondary review link를 추가했다. - `/stats` 마지막 CTA의 `/app` return handoff가 연결됐다. - carry-forward CTA는 `/app?review=weekly&carryHint=...`로 돌아온다. - `/app`은 review-aware return hint를 먼저 보여주되, goal은 사용자가 직접 입력하게 유지한다. @@ -95,6 +97,7 @@ Last Updated: 2026-03-14 - `/space` complete 이후 secondary review teaser까지 연결됐다. - goal complete로 setup 상태로 돌아왔을 때만 setup drawer 아래에 작은 review teaser가 보인다. - full review 강제 이동 없이 `/stats`를 여는 secondary entry로만 동작한다. + - 방금 끝낸 흐름을 반영한다고 과장하지 않는 카피로 정리했다. - 다음 구현은 weekly review의 실제 recovery 집계 연결이다. - 유료화 포지셔닝을 `Calm Session OS`로 재정의했다. - Free는 기본 집중 시작, Pro는 더 잘 이어가기라는 메시지로 정리했다. diff --git a/src/shared/i18n/messages/app.ts b/src/shared/i18n/messages/app.ts index a74a35a..b36cdf6 100644 --- a/src/shared/i18n/messages/app.ts +++ b/src/shared/i18n/messages/app.ts @@ -73,7 +73,7 @@ export const app = { reviewCarryTryClosure: '시작은 있었지만 마무리가 약했어요. 다음 주에는 완료 직전에 다른 블록으로 넘어가지 않는 흐름을 한 번 만들어 보세요.', reviewCarryTryStart: '시작 횟수가 적었어요. 다음 주에는 길이를 늘리기보다 첫 세션을 한 번 더 여는 것에 집중해 보세요.', reviewCarryCta: '이 흐름으로 다음 세션 시작', - reviewCarryCtaPro: '가장 잘 맞은 ritual로 /app 돌아가기', + reviewCarryCtaPro: '추천 ritual과 함께 /app 돌아가기', reviewCarryKeepTitle: '다음 주에 유지할 것', reviewCarryTryTitle: '다음 주에 바꿔볼 것', reviewCarryPresetLabel: '추천 ritual', diff --git a/src/shared/i18n/messages/space.ts b/src/shared/i18n/messages/space.ts index faae62e..4a3522d 100644 --- a/src/shared/i18n/messages/space.ts +++ b/src/shared/i18n/messages/space.ts @@ -17,10 +17,10 @@ export const space = { timerLabel: '타이머', soundLabel: '사운드', reviewTeaserEyebrow: 'Weekly Review', - reviewTeaserTitle: '방금 끝낸 흐름까지 review에 담아둘까요?', - reviewTeaserTitlePro: '방금 끝낸 흐름까지 포함해 이번 주 리듬을 다시 볼까요?', + reviewTeaserTitle: '이번 주 review를 다시 볼까요?', + reviewTeaserTitlePro: '이번 주 흐름과 잘 맞았던 ritual을 다시 볼까요?', reviewTeaserHelper: '지금은 바로 다시 시작해도 괜찮고, 원하면 주간 review를 잠깐 보고 갈 수 있어요.', - reviewTeaserHelperPro: '방금 마친 흐름과 가장 잘 맞는 ritual을 같이 보고 다음 세션으로 이어갈 수 있어요.', + reviewTeaserHelperPro: '원하면 이번 주 흐름과 추천 ritual을 다시 보고 다음 세션으로 이어갈 수 있어요.', reviewTeaserCta: '주간 review 보기', reviewTeaserDismiss: '나중에', readyHint: '목표를 적으면 시작할 수 있어요.', @@ -65,12 +65,12 @@ export const space = { returnPromptEyebrow: '다시 돌아왔어요', returnPromptFocusTitle: '흐름은 아직 남아 있어요.', returnPromptFocusDescription: '멈춘 자리에서 바로 이어가거나, 다시 시작할 한 조각만 조용히 다듬을 수 있어요.', - returnPromptBreakTitle: '자리를 비운 사이 이 블록이 끝났어요.', - returnPromptBreakDescription: '지금부터 쉬거나, 다음 블록으로 부드럽게 넘어갈 수 있어요.', + returnPromptBreakTitle: '자리를 비운 사이 쉬는 시간이 시작됐어요.', + returnPromptBreakDescription: 'break를 그대로 이어가거나, 다음 블록으로 부드럽게 넘어갈 수 있어요.', returnPromptContinue: '멈춘 자리에서 이어가기', returnPromptContinueHint: '타이머와 현재 흐름을 그대로 두고 다시 집중으로 복귀합니다.', - returnPromptRest: '지금부터 쉬기', - returnPromptRestHint: '지금부터 break를 시작한 것처럼 천천히 숨을 고릅니다.', + returnPromptRest: '쉬기 이어가기', + returnPromptRestHint: '이미 시작된 break를 그대로 두고, 조금 더 천천히 숨을 고릅니다.', returnPromptNext: '다음 블록 이어가기', returnPromptNextHint: '다음 한 조각을 정하고, 같은 흐름 안에서 부드럽게 이어갑니다.', returnPromptRefocus: '한 조각 다시 잡기', @@ -90,8 +90,8 @@ export const space = { suggestions: ['리뷰 코멘트 2개 처리', '문서 1문단 다듬기', '이슈 1개 정리', '메일 2개 회신'], placeholderFallback: '다음 한 조각을 적어보세요', placeholderExample: (goal: string) => `예: ${goal}`, - title: '이 블록을 어떻게 닫을까요?', - description: '지금은 끝내기, 쉬기, 이어가기 중 하나만 고르면 돼요.', + title: '이 블록을 어떻게 이어갈까요?', + description: '다음으로 이어가기, 잠시 비우기, 여기서 마무리하기 중 하나만 고르면 돼요.', nextTitle: '좋아요. 다음 한 조각만 정해요.', nextDescription: '너무 크게 잡지 말고, 바로 손을 올릴 한 줄만 남겨요.', currentGoalLabel: '방금 끝낸 블록', @@ -100,10 +100,10 @@ export const space = { chooseNextDescription: '다음 한 조각을 정하고 같은 흐름 안에서 계속 갑니다.', backButton: '돌아가기', closeAriaLabel: '닫기', - finishButton: '여기까지 끝내기', + finishButton: '여기서 마무리하기', finishDescription: '이 블록은 여기서 닫고, 다음 진입은 가볍게 남겨둡니다.', - restButton: '잠깐 쉬기', - restDescription: '이 블록은 닫고, 지금부터는 잠깐 쉬는 리듬으로 넘어갑니다.', + restButton: '잠시 비우기', + restDescription: '이 블록은 아직 닫지 않고, 잠깐 멈춘 뒤 돌아오라고 알려드려요.', confirmButton: '다음 목표로 바로 시작', confirmPending: '시작 중…', finishPending: '마무리 중…', diff --git a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx index da0b5ad..d4fd943 100644 --- a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx +++ b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx @@ -17,6 +17,14 @@ import { cn } from '@/shared/lib/cn'; const DEFAULT_SCENE_ID = getSceneById('forest')?.id ?? SCENE_THEMES[0].id; const DEFAULT_SOUND_ID = SOUND_PRESETS.find((preset) => preset.id === 'forest-birds')?.id ?? SOUND_PRESETS[0].id; const DEFAULT_TIMER_ID = '50-10'; +const REVIEW_ENTRY_PRESETS = { + 'forest-50-10': { + sceneId: DEFAULT_SCENE_ID, + soundPresetId: DEFAULT_SOUND_ID, + timerPresetId: DEFAULT_TIMER_ID, + label: '숲 · 50/10 · Forest Birds', + }, +} as const; const GOAL_SUGGESTIONS = copy.session.goalChips.slice(0, 4); const entryCopy = { @@ -45,6 +53,9 @@ const entryCopy = { reviewTitlePro: '나에게 잘 맞았던 흐름을 다시 보고 갈까요?', reviewCtaPro: '나에게 맞는 흐름 보기', reviewHelperPro: '가장 잘 맞았던 ritual과 carry-forward를 보고 돌아올 수 있어요.', + resumeReviewEyebrow: 'Weekly Review', + resumeReviewTitle: '잠깐 review를 보고 다시 들어갈 수 있어요.', + resumeReviewHelper: '현재 세션은 그대로 두고, 이번 주 흐름만 짧게 확인합니다.', reviewReturnEyebrow: '방금 본 review 기준', reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.', reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.', @@ -109,6 +120,15 @@ export const FocusDashboardWidget = () => { const { sceneAssetMap } = useMediaCatalog(); const { review, summary: weeklySummary } = useFocusStats(); + const reviewEntryPreset = searchParams.get('entryPreset'); + const reviewEntryPresetConfig = useMemo(() => { + if (!reviewEntryPreset) { + return null; + } + + return REVIEW_ENTRY_PRESETS[reviewEntryPreset as keyof typeof REVIEW_ENTRY_PRESETS] ?? null; + }, [reviewEntryPreset]); + const [goalDraft, setGoalDraft] = useState(''); const [microStepDraft, setMicroStepDraft] = useState(''); const [isStartingSession, setIsStartingSession] = useState(false); @@ -120,8 +140,8 @@ export const FocusDashboardWidget = () => { const goalInputRef = useRef(null); const activeScene = useMemo(() => { - return getSceneById(currentSession?.sceneId ?? DEFAULT_SCENE_ID) ?? SCENE_THEMES[0]; - }, [currentSession?.sceneId]); + return getSceneById(currentSession?.sceneId ?? reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID) ?? SCENE_THEMES[0]; + }, [currentSession?.sceneId, reviewEntryPresetConfig?.sceneId]); const activeRitualMeta = useMemo(() => { const timerLabel = timerLabelById[currentSession?.timerPresetId ?? DEFAULT_TIMER_ID] ?? '50/10'; @@ -138,7 +158,6 @@ export const FocusDashboardWidget = () => { review.recoveryQuality.availability === 'ready'); const reviewSource = searchParams.get('review'); const reviewCarryHint = searchParams.get('carryHint'); - const reviewEntryPreset = searchParams.get('entryPreset'); const normalizedReviewCarryHint: ReviewCarryHint | null = reviewCarryHint === 'steady' || reviewCarryHint === 'smaller' || @@ -151,11 +170,12 @@ export const FocusDashboardWidget = () => { const reviewReturnCopy = normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null; const reviewReturnRitualLabel = - isPro && reviewEntryPreset === 'forest-50-10' ? entryCopy.reviewReturnRitualLabel : null; + isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null; const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle; const reviewTeaserSummary = isPro ? review.carryForward.keepDoing : review.snapshotSummary; const reviewTeaserHelper = isPro ? entryCopy.reviewHelperPro : entryCopy.reviewHelper; const reviewTeaserCta = isPro ? entryCopy.reviewCtaPro : entryCopy.reviewCta; + const entryRitualHint = reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : entryCopy.ritualHint; useEffect(() => { let cancelled = false; @@ -215,9 +235,9 @@ export const FocusDashboardWidget = () => { await focusSessionApi.startSession({ goal: trimmedGoal, microStep: microStepDraft.trim() || null, - sceneId: DEFAULT_SCENE_ID, - soundPresetId: DEFAULT_SOUND_ID, - timerPresetId: DEFAULT_TIMER_ID, + sceneId: reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID, + soundPresetId: reviewEntryPresetConfig?.soundPresetId ?? DEFAULT_SOUND_ID, + timerPresetId: reviewEntryPresetConfig?.timerPresetId ?? DEFAULT_TIMER_ID, entryPoint: 'space-setup', }); router.push('/space'); @@ -235,6 +255,8 @@ export const FocusDashboardWidget = () => { const shouldShowWeeklyReviewTeaser = !isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn; + const shouldShowResumeReviewEntry = + !isCheckingSession && Boolean(currentSession) && hasEnoughWeeklyData; return (
@@ -264,37 +286,6 @@ export const FocusDashboardWidget = () => {

세션 상태를 불러오는 중이에요.

- ) : currentSession ? ( -
-
-

- {entryCopy.resumeEyebrow} -

-

- {currentSession.goal} -

-

- {currentSession.state === 'paused' ? entryCopy.resumePaused : entryCopy.resumeRunning} -

- {currentSession.microStep ? ( -
-

- {entryCopy.resumeMicroStepLabel} -

-

{currentSession.microStep}

-
- ) : null} -
- -
- -

{activeRitualMeta}

-
- -

{entryCopy.resumeNewGoalHint}

-
) : (
{reviewReturnCopy ? ( @@ -314,129 +305,188 @@ export const FocusDashboardWidget = () => {
) : null} -
-
-

- {entryCopy.title} -

-

- {entryCopy.description} -

-
- -
- - - - -

{entryCopy.microStepHelper}

-
- -
- {GOAL_SUGGESTIONS.map((suggestion) => { - const isActive = trimmedGoal === suggestion.label; - - return ( - - ); - })} -
- -
- -
-

- {entryCopy.ritualHint} + {currentSession ? ( +

+
+

+ {entryCopy.resumeEyebrow}

-

{entryCopy.ritualHelper}

+

+ {currentSession.goal} +

+

+ {currentSession.state === 'paused' ? entryCopy.resumePaused : entryCopy.resumeRunning} +

+ {currentSession.microStep ? ( +
+

+ {entryCopy.resumeMicroStepLabel} +

+

{currentSession.microStep}

+
+ ) : null}
+ +
+ +

{activeRitualMeta}

+
+ +

{entryCopy.resumeNewGoalHint}

+ + {shouldShowResumeReviewEntry ? ( + +
+
+

+ {entryCopy.resumeReviewEyebrow} +

+

+ {entryCopy.resumeReviewTitle} +

+

+ {isPro ? review.carryForward.keepDoing : entryCopy.resumeReviewHelper} +

+
+ + {reviewTeaserCta} + +
+ + ) : null}
- - {sessionLookupError ? ( -

{entryCopy.loadFailed}

- ) : null} -
- - {shouldShowWeeklyReviewTeaser ? ( - -
-
-

- {entryCopy.reviewEyebrow} + ) : ( + <> +

+
+

+ {entryCopy.title} +

+

+ {entryCopy.description}

-

- {reviewTeaserTitle} -

-

- {reviewTeaserSummary} -

-

{reviewTeaserHelper}

- - {reviewTeaserCta} - + +
+ + + + +

{entryCopy.microStepHelper}

+
+ +
+ {GOAL_SUGGESTIONS.map((suggestion) => { + const isActive = trimmedGoal === suggestion.label; + + return ( + + ); + })} +
+ +
+ +
+

+ {entryRitualHint} +

+

{entryCopy.ritualHelper}

+
+
+ + {sessionLookupError ? ( +

{entryCopy.loadFailed}

+ ) : null}
- - ) : null} + + {shouldShowWeeklyReviewTeaser ? ( + +
+
+

+ {entryCopy.reviewEyebrow} +

+

+ {reviewTeaserTitle} +

+

+ {reviewTeaserSummary} +

+

{reviewTeaserHelper}

+
+ + {reviewTeaserCta} + +
+ + ) : null} + + )}
)}
diff --git a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx index b6b807d..5662201 100644 --- a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx +++ b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx @@ -63,6 +63,7 @@ export const SpaceFocusHudWidget = ({ const visibleRef = useRef(false); const resumePlaybackStateRef = useRef<'running' | 'paused'>(playbackState); const pausePlaybackStateRef = useRef<'running' | 'paused'>(playbackState); + const suppressNextPausePromptRef = useRef(false); const restReminderTimerRef = useRef(null); const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.focusHud.goalFallback; const normalizedMicroStep = microStep?.trim() ? microStep.trim() : null; @@ -147,6 +148,12 @@ export const SpaceFocusHudWidget = ({ hasActiveSession && overlay === 'none' ) { + if (suppressNextPausePromptRef.current) { + suppressNextPausePromptRef.current = false; + pausePlaybackStateRef.current = playbackState; + return; + } + setIntentError(null); setOverlay('paused'); onStatusMessage({ @@ -292,7 +299,6 @@ export const SpaceFocusHudWidget = ({ }} onRest={() => { handleDismissReturnPrompt(); - onStatusMessage({ message: copy.space.focusHud.restReminder }); }} onNextGoal={() => { handleDismissReturnPrompt(); @@ -352,6 +358,8 @@ export const SpaceFocusHudWidget = ({ onFinish={() => Promise.resolve(onGoalFinish())} onRest={() => { setOverlay('none'); + suppressNextPausePromptRef.current = true; + onPauseRequested?.(); if (restReminderTimerRef.current) { window.clearTimeout(restReminderTimerRef.current); diff --git a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx index f97e6ed..035155b 100644 --- a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx +++ b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx @@ -230,7 +230,7 @@ export const SpaceWorkspaceWidget = () => { summary: isPro ? review.carryForward.keepDoing : copy.space.setup.reviewTeaserHelper, - ctaHref: "/stats?review=weekly&origin=space-complete", + ctaHref: "/stats", ctaLabel: copy.space.setup.reviewTeaserCta, onDismiss: () => setShowReviewTeaserAfterComplete(false), }