diff --git a/docs/12_core_loop_execution_roadmap.md b/docs/12_core_loop_execution_roadmap.md index 6726c03..6f8d11f 100644 --- a/docs/12_core_loop_execution_roadmap.md +++ b/docs/12_core_loop_execution_roadmap.md @@ -207,7 +207,10 @@ VibeRoom은 아래 방식으로 진행한다. - 상세 기획 문서 작성 완료 - Session Routing Contract 구현 완료 -- 다음 구현 slice는 `/app` Paused Resume Gate +- `/app` Paused Resume Gate 구현 완료 +- `/space` Auto-Resume Handoff 구현 완료 +- `Paused Session Takeover Flow` 구현 완료 +- 남은 것은 browser QA와 takeover 문구 polish ### Phase 6. Premium Ambience System diff --git a/docs/17_product_alignment_findings.md b/docs/17_product_alignment_findings.md index bbf071c..552bac6 100644 --- a/docs/17_product_alignment_findings.md +++ b/docs/17_product_alignment_findings.md @@ -67,7 +67,7 @@ Last Updated: 2026-03-15 | ALN-007 | P2 | Weekly Review discoverability | review는 `/app`의 primary ritual이어야 함 | 데이터 gate와 currentSession 조건에 따라 사용자에게 “아예 없는 기능”처럼 느껴질 수 있음 | `src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx`, `docs/15_app_stats_entry_flow_spec.md` | open | low-data 상태와 resume 상태를 포함한 discoverability 정책 재정의 | | ALN-008 | P1 | `잠시 비우기`와 `Break`의 제품 의미 | break는 reward/reset, pause는 recovery로 분리돼야 함 | 현재는 카피와 트레이는 개선됐지만, 제품 차원의 최종 정의와 시각 분리까지 완전히 닫히진 않았음 | `docs/10_refocus_system_spec.md`, `docs/11_away_return_recovery_spec.md`, `src/widgets/space-focus-hud/ui/GoalCompleteSheet.tsx`, `src/widgets/space-focus-hud/ui/ReturnPrompt.tsx` | open | `잠시 비우기`, active break, return(break)를 하나의 최종 state model로 재정의 | | ALN-009 | P3 | Spec / current-state drift | 다음 세션 문서가 실제 구현과 맞아야 함 | intent card, goal complete, review entry 관련 오래된 표현이 여러 spec에 남아 있었음 | `docs/10_refocus_system_spec.md`, `docs/13_space_intent_card_collapsed_expanded_spec.md`, `docs/90_current_state.md`, `docs/session_brief.md`, `../../current_context.md` | fixed-awaiting-browser | 이후 라운드부터는 fix와 문서 갱신을 같은 커밋에서 닫는지 점검 | -| ALN-010 | P1 | paused session 재진입 정책 | running은 바로 `/space`, paused는 `/app` resume gate, explicit continue 이후에는 자동 resume이어야 함 | Session Routing Contract, paused resume gate, auto-resume handoff는 구현됐지만 takeover flow와 browser QA가 아직 남아 있어 최종 정책이 완전히 닫히진 않았음 | `docs/18_paused_session_reentry_spec.md`, `src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx`, `src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx`, `src/features/focus-session/model/useFocusSessionEngine.ts` | open | takeover flow를 구현하고 paused re-entry browser QA로 닫기 | +| ALN-010 | P1 | paused session 재진입 정책 | running은 바로 `/space`, paused는 `/app` resume gate, explicit continue 이후에는 자동 resume이어야 함 | Session Routing Contract, paused resume gate, auto-resume handoff, takeover flow까지 구현됐다. 남은 것은 browser QA와 takeover wording polish이다 | `docs/18_paused_session_reentry_spec.md`, `src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx`, `src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx`, `src/features/focus-session/model/useFocusSessionEngine.ts` | fixed-awaiting-browser | paused resume / refocus / takeover 3경로를 브라우저에서 확인 | --- diff --git a/docs/18_paused_session_reentry_spec.md b/docs/18_paused_session_reentry_spec.md index 066111c..13a2ffd 100644 --- a/docs/18_paused_session_reentry_spec.md +++ b/docs/18_paused_session_reentry_spec.md @@ -250,6 +250,7 @@ resume card 안에는 아래만 둔다. - new start가 아니다 - takeover flow 진입점이다 +- server도 current session이 남아 있으면 direct start를 거절해야 한다 --- diff --git a/docs/90_current_state.md b/docs/90_current_state.md index 66ae7c4..ab231e1 100644 --- a/docs/90_current_state.md +++ b/docs/90_current_state.md @@ -4,6 +4,11 @@ Last Updated: 2026-03-15 ## DONE +- `Paused Session Takeover Flow` 구현: + - `/app` paused gate에 `새 목표로 전환` 진입점을 추가했다 + - takeover confirm sheet에서만 current paused session을 정리하고 single-goal start 상태로 넘어간다 + - silent abandon을 막기 위해 server `startSession()`도 current session 존재 시 direct start를 거절하도록 정리했다 + - explicit confirm 이후에만 `abandon -> 새 목표 입력` 흐름이 가능하다 - `/app` single-goal commitment gate 재구성: - 2-step `goal -> ritual` flow 제거 - current session이 있으면 `Resume` UI를 우선 노출하고, `/space`로 바로 이어가기만 제안하되 review entry는 조용한 secondary link로 유지 diff --git a/docs/session_brief.md b/docs/session_brief.md index ade4316..1c4f96a 100644 --- a/docs/session_brief.md +++ b/docs/session_brief.md @@ -14,13 +14,18 @@ Last Updated: 2026-03-15 ## 현재 우선순위 -1. `Paused Session Takeover Flow` -2. `Core Loop Alignment` browser audit -3. `Weekly Review` ritual fit highlight -4. `Premium Ambience` +1. `Core Loop Alignment` browser audit +2. `Weekly Review` ritual fit highlight +3. `Premium Ambience` +4. `Pause / Break / Return` browser polish ## 최근 세션 상태 +- `Paused Session Takeover Flow`를 구현했다. + - `/app` paused gate에 `새 목표로 전환` 액션이 추가됐다. + - takeover confirm sheet에서만 기존 paused session을 정리하고 새로 시작할 수 있다. + - server `startSession()`은 더 이상 silent abandon을 하지 않고, current session이 남아 있으면 direct start를 거절한다. + - takeover confirm 후에만 `abandon -> single-goal start` 순서로 넘어간다. - `/space` Refocus System 첫 slice를 구현했다. - pause 직후 바로 편집 시트가 아니라 작은 recovery prompt를 먼저 띄운다. - 여기서 `한 조각 다시 잡기`를 누르면 refocus tray로 들어간다. diff --git a/docs/work.md b/docs/work.md index 3ac10f0..f98e259 100644 --- a/docs/work.md +++ b/docs/work.md @@ -103,6 +103,8 @@ - 완료 조건: - paused 상태에서 direct new start는 불가능하다 - 사용자는 기존 paused session을 어떻게 처리할지 먼저 고른다 +- 진행 상태: + - 완료 - 검증: - paused -> new start 브라우저 QA - 커밋 힌트: diff --git a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx index 504de19..56494ce 100644 --- a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx +++ b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx @@ -47,7 +47,16 @@ const entryCopy = { resumeRouting: '진행 중인 세션으로 돌아가는 중이에요.', resumeMicroStepLabel: '마지막 한 조각', resumePausedHint: '같은 목표를 바로 이어가거나, 다시 시작할 한 조각만 먼저 정리할 수 있어요.', - resumeNewGoalHint: '새 목표는 현재 세션을 마무리한 뒤 시작할 수 있어요.', + resumeNewGoalHint: '새 목표로 전환하려면 지금 멈춘 세션을 먼저 어떻게 정리할지 결정해야 해요.', + resumeTakeoverCta: '새 목표로 전환', + takeoverEyebrow: '새 목표로 전환', + takeoverTitle: '현재 멈춘 세션을 어떻게 할까요?', + takeoverBody: '지금 멈춘 흐름을 조용히 없애지 않고, 먼저 정리한 뒤 새 목표로 넘어가요.', + takeoverKeepCta: '이어서 하기', + takeoverConfirmCta: '이 세션은 여기서 정리하고 새로 시작', + takeoverCancelCta: '취소', + takeoverLoading: '세션을 정리하는 중...', + takeoverFailed: '멈춘 세션을 정리하지 못했어요. 잠시 후 다시 시도해 주세요.', loadFailed: '세션 상태를 불러오지 못했어요. 새로 시작은 계속 할 수 있어요.', reviewEyebrow: 'Weekly Review', reviewTitle: '이번 주 review를 잠깐 보고 갈까요?', @@ -141,6 +150,10 @@ export const FocusDashboardWidget = () => { const [currentSession, setCurrentSession] = useState(null); const [isCheckingSession, setIsCheckingSession] = useState(true); const [sessionLookupError, setSessionLookupError] = useState(null); + const [isTakeoverSheetOpen, setIsTakeoverSheetOpen] = useState(false); + const [isResolvingTakeover, setIsResolvingTakeover] = useState(false); + const [takeoverError, setTakeoverError] = useState(null); + const [focusGoalAfterTakeover, setFocusGoalAfterTakeover] = useState(false); const goalInputRef = useRef(null); @@ -223,6 +236,21 @@ export const FocusDashboardWidget = () => { } }, [isCheckingSession, isRunningSession, router]); + useEffect(() => { + if (!focusGoalAfterTakeover || currentSession) { + return; + } + + const frameId = window.requestAnimationFrame(() => { + goalInputRef.current?.focus(); + setFocusGoalAfterTakeover(false); + }); + + return () => { + window.cancelAnimationFrame(frameId); + }; + }, [currentSession, focusGoalAfterTakeover]); + const openPaywall = () => { if (!isPro) { setPaywallSource('app-entry-plan-pill'); @@ -256,6 +284,19 @@ export const FocusDashboardWidget = () => { router.push('/space'); return; } catch (error) { + const message = + error instanceof Error ? error.message : entryCopy.loadFailed; + setSessionLookupError(message); + + try { + const session = await focusSessionApi.getCurrentSession(); + if (session) { + setCurrentSession(session); + } + } catch (syncError) { + console.error('Failed to sync current session after /app start failure', syncError); + } + console.error('Failed to start focus session from /app', error); } @@ -270,6 +311,45 @@ export const FocusDashboardWidget = () => { router.push('/space?resume=refocus'); }; + const handleOpenTakeoverSheet = () => { + setTakeoverError(null); + setIsTakeoverSheetOpen(true); + }; + + const handleCloseTakeoverSheet = () => { + if (isResolvingTakeover) { + return; + } + + setIsTakeoverSheetOpen(false); + setTakeoverError(null); + }; + + const handleConfirmTakeover = async () => { + if (!currentSession || isResolvingTakeover) { + return; + } + + setIsResolvingTakeover(true); + setTakeoverError(null); + + try { + await focusSessionApi.abandonSession(); + setCurrentSession(null); + setIsTakeoverSheetOpen(false); + setSessionLookupError(null); + setGoalDraft(''); + setMicroStepDraft(''); + setFocusGoalAfterTakeover(true); + } catch (error) { + setTakeoverError( + error instanceof Error ? error.message : entryCopy.takeoverFailed, + ); + } finally { + setIsResolvingTakeover(false); + } + }; + const shouldShowWeeklyReviewTeaser = !isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn; const shouldShowResumeReviewEntry = @@ -365,6 +445,13 @@ export const FocusDashboardWidget = () => {

{entryCopy.resumePausedHint}

{entryCopy.resumeNewGoalHint}

+ {shouldShowResumeReviewEntry ? ( { ) : null} + + {isTakeoverSheetOpen ? ( +
+
+
+

+ {entryCopy.takeoverEyebrow} +

+

+ {entryCopy.takeoverTitle} +

+

+ {entryCopy.takeoverBody} +

+ + {currentSession ? ( +
+

+ {entryCopy.resumeEyebrow} +

+

+ {currentSession.goal} +

+ {currentSession.microStep ? ( +

+ {entryCopy.resumeMicroStepLabel} · {currentSession.microStep} +

+ ) : null} +
+ ) : null} + + {takeoverError ? ( +

{takeoverError}

+ ) : null} + +
+ + + +
+
+
+ ) : null}
); };