fix(space): break와 recovery 상태의 완료 경로 복구

This commit is contained in:
2026-03-15 23:10:29 +09:00
parent 728330bf74
commit 3c5154178d
8 changed files with 104 additions and 1 deletions

View File

@@ -178,6 +178,25 @@ Refocus는 아래 5개 상태만 가진다.
## 8. 상태별 상세 UX ## 8. 상태별 상세 UX
### CTA Matrix
Refocus family에서 중요한 원칙은 하나다.
> 세션이 아직 살아 있는 상태라면, 사용자는 언제든 `계속 / 다시 잡기 / 마무리` 중 하나로 갈 수 있어야 한다.
상태별 CTA는 아래처럼 고정한다.
| 상태 | 보여야 하는 주 행동 | goal complete 접근 |
| --- | --- | --- |
| `Focused` | `수정`, `microStep 완료`, `이번 목표 완료`, timer `pause/reset` | base intent card에서 직접 노출 |
| `Paused` | `한 조각 다시 잡기`, `바로 이어가기` | pause tray의 low-emphasis `여기서 마무리하기` |
| `Return (focus)` | `멈춘 자리에서 이어가기`, `한 조각 다시 잡기` | return tray의 low-emphasis `여기서 마무리하기` |
| `Break` | break 유지, 다음 블록으로 이어가기, goal closure | base intent card에서 직접 노출 |
| `Return (break)` | `쉬기 이어가기`, `다음 블록 이어가기`, `한 조각 다시 잡기` | return tray의 low-emphasis `여기서 마무리하기` |
| `Next Beat` | `목표만 두고 계속하기`, `다음 단계 적기` | next-beat tray의 low-emphasis `이 목표는 여기서 마무리하기` |
`Refocus` 편집 시트 자체는 편집에만 집중하고, complete 액션은 별도 decision tray나 base intent card에서 수행한다.
### 8.1 Focused ### 8.1 Focused
목표: 목표:

View File

@@ -61,6 +61,10 @@ Last Updated: 2026-03-15
- `Pause` tray는 빠르게 다시 붙잡는 recovery reveal로 조정 - `Pause` tray는 빠르게 다시 붙잡는 recovery reveal로 조정
- `Return(focus)`는 짧은 re-entry settle motion으로, `Return(break)`는 더 느슨한 release reveal로 분리 - `Return(focus)`는 짧은 re-entry settle motion으로, `Return(break)`는 더 느슨한 release reveal로 분리
- `Goal Complete`도 같은 recovery family 안에서 가장 느린 closure motion을 가지도록 조정 - `Goal Complete`도 같은 recovery family 안에서 가장 느린 closure motion을 가지도록 조정
- `/space` goal closure CTA matrix 정렬:
- `break` phase에서도 expanded intent card 안에 `이번 목표 완료`를 유지하도록 수정
- base card가 잠기는 recovery overlay(`pause / return / next-beat`) 안에서도 low-emphasis `여기서 마무리하기` 경로를 추가
- 이제 active session 상태에서는 `계속 / 다시 잡기 / 마무리` 중 최소 한 경로가 항상 보이도록 정리
- `/space` intent HUD collapsed / expanded 재설계: - `/space` intent HUD collapsed / expanded 재설계:
- 상시 큰 goal 카드 대신 idle에서는 goal 1줄만 남는 collapsed glass rail 구조로 변경 - 상시 큰 goal 카드 대신 idle에서는 goal 1줄만 남는 collapsed glass rail 구조로 변경
- hover / focus / rail tap에서만 expanded card로 열리며, 이때만 microStep과 `이번 목표 완료` 액션이 노출됨 - hover / focus / rail tap에서만 expanded card로 열리며, 이때만 microStep과 `이번 목표 완료` 액션이 노출됨

View File

@@ -66,6 +66,10 @@ Last Updated: 2026-03-15
- `Pause`는 빠르게 다시 붙잡는 recovery reveal로, - `Pause`는 빠르게 다시 붙잡는 recovery reveal로,
- `Return(focus)`는 재진입에 맞는 짧은 settle motion으로, - `Return(focus)`는 재진입에 맞는 짧은 settle motion으로,
- `Return(break)``Goal Complete`는 더 느슨한 release/closure reveal로 분리했다. - `Return(break)``Goal Complete`는 더 느슨한 release/closure reveal로 분리했다.
- `/space` active session에서는 goal closure 경로가 항상 남도록 정리했다.
- `break`에서도 expanded goal card 안에 `이번 목표 완료`가 보인다.
- `pause / return / next-beat`처럼 base card가 잠기는 overlay 안에는 low-emphasis `여기서 마무리하기`가 추가됐다.
- 그래서 사용자는 recovery 상태에서도 `계속 / 다시 잡기 / 마무리` 중 하나를 바로 고를 수 있다.
- `/space` 목표 카드를 collapsed / expanded 구조로 재설계했다. - `/space` 목표 카드를 collapsed / expanded 구조로 재설계했다.
- idle에서는 goal 1줄만 남는 얇은 glass rail로 줄였다. - idle에서는 goal 1줄만 남는 얇은 glass rail로 줄였다.
- microStep과 `이번 목표 완료`는 expanded 상태에서만 드러난다. - microStep과 `이번 목표 완료`는 expanded 상태에서만 드러난다.

View File

@@ -62,6 +62,8 @@ export const space = {
pausePromptRefocusHint: '목표는 그대로 두고, 지금 다시 시작할 한 줄만 정리해요.', pausePromptRefocusHint: '목표는 그대로 두고, 지금 다시 시작할 한 줄만 정리해요.',
pausePromptKeep: '바로 이어가기', pausePromptKeep: '바로 이어가기',
pausePromptKeepHint: '지금 방향을 유지한 채, 멈춘 자리에서 다시 시작해요.', pausePromptKeepHint: '지금 방향을 유지한 채, 멈춘 자리에서 다시 시작해요.',
pausePromptFinish: '여기서 마무리하기',
pausePromptFinishHint: '다시 이어가기보다, 지금 블록을 여기서 조용히 닫습니다.',
returnPromptEyebrow: '다시 돌아왔어요', returnPromptEyebrow: '다시 돌아왔어요',
returnPromptFocusTitle: '흐름은 아직 남아 있어요.', returnPromptFocusTitle: '흐름은 아직 남아 있어요.',
returnPromptFocusDescription: '멈춘 자리에서 바로 이어가거나, 다시 시작할 한 조각만 조용히 다듬을 수 있어요.', returnPromptFocusDescription: '멈춘 자리에서 바로 이어가거나, 다시 시작할 한 조각만 조용히 다듬을 수 있어요.',
@@ -75,6 +77,8 @@ export const space = {
returnPromptNextHint: '다음 한 조각을 정하고, 같은 흐름 안에서 부드럽게 이어갑니다.', returnPromptNextHint: '다음 한 조각을 정하고, 같은 흐름 안에서 부드럽게 이어갑니다.',
returnPromptRefocus: '한 조각 다시 잡기', returnPromptRefocus: '한 조각 다시 잡기',
returnPromptRefocusHint: '왜 멈췄는지는 건너뛰고, 지금 다시 시작할 한 줄만 남깁니다.', returnPromptRefocusHint: '왜 멈췄는지는 건너뛰고, 지금 다시 시작할 한 줄만 남깁니다.',
returnPromptFinish: '여기서 마무리하기',
returnPromptFinishHint: '이 흐름은 여기서 닫고, 다음 진입은 가볍게 남겨둡니다.',
microStepCompleteAriaLabel: '현재 한 조각 완료', microStepCompleteAriaLabel: '현재 한 조각 완료',
microStepPromptEyebrow: '다음 단계', microStepPromptEyebrow: '다음 단계',
microStepPromptTitle: '바로 이어서 할 다음 단계가 있나요?', microStepPromptTitle: '바로 이어서 할 다음 단계가 있나요?',
@@ -83,6 +87,8 @@ export const space = {
microStepPromptKeepHint: '다음 단계는 비워두고, 같은 목표 안에서 이어서 집중해요.', microStepPromptKeepHint: '다음 단계는 비워두고, 같은 목표 안에서 이어서 집중해요.',
microStepPromptDefine: '다음 단계 적기', microStepPromptDefine: '다음 단계 적기',
microStepPromptDefineHint: '바로 손을 움직일 수 있는 가장 작은 다음 행동을 한 줄로 남겨요.', microStepPromptDefineHint: '바로 손을 움직일 수 있는 가장 작은 다음 행동을 한 줄로 남겨요.',
microStepPromptFinish: '이 목표는 여기서 마무리하기',
microStepPromptFinishHint: '다음 단계를 늘리지 않고, 지금 블록을 깔끔하게 닫습니다.',
microStepCleared: '지금 할 한 조각을 비우고 목표만 유지해요.', microStepCleared: '지금 할 한 조각을 비우고 목표만 유지해요.',
completeAction: '이번 목표 완료', completeAction: '이번 목표 완료',
}, },

View File

@@ -22,6 +22,7 @@ interface NextMicroStepPromptProps {
error: string | null; error: string | null;
onKeepGoalOnly: () => void; onKeepGoalOnly: () => void;
onDefineNext: () => void; onDefineNext: () => void;
onFinish: () => void;
} }
export const NextMicroStepPrompt = ({ export const NextMicroStepPrompt = ({
@@ -31,6 +32,7 @@ export const NextMicroStepPrompt = ({
error, error,
onKeepGoalOnly, onKeepGoalOnly,
onDefineNext, onDefineNext,
onFinish,
}: NextMicroStepPromptProps) => { }: NextMicroStepPromptProps) => {
const trimmedGoal = goal.trim(); const trimmedGoal = goal.trim();
@@ -106,6 +108,20 @@ export const NextMicroStepPrompt = ({
<span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-[2px]')}></span> <span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-[2px]')}></span>
</button> </button>
</div> </div>
<div className="mt-4 border-t border-white/8 pt-3">
<button
type="button"
onClick={onFinish}
disabled={isSubmitting}
className="text-[12px] font-medium tracking-[0.08em] text-white/58 underline decoration-white/12 underline-offset-4 transition-colors hover:text-white/82 hover:decoration-white/24 disabled:cursor-default disabled:text-white/26 disabled:no-underline"
>
{copy.space.focusHud.microStepPromptFinish}
</button>
<p className="mt-1.5 max-w-[20.5rem] text-[12px] leading-[1.55] text-white/42">
{copy.space.focusHud.microStepPromptFinishHint}
</p>
</div>
</div> </div>
</section> </section>
</div> </div>

View File

@@ -23,6 +23,7 @@ interface PauseRefocusPromptProps {
isBusy: boolean; isBusy: boolean;
onRefocus: () => void; onRefocus: () => void;
onKeepCurrent: () => void; onKeepCurrent: () => void;
onFinish: () => void;
} }
export const PauseRefocusPrompt = ({ export const PauseRefocusPrompt = ({
@@ -30,6 +31,7 @@ export const PauseRefocusPrompt = ({
isBusy, isBusy,
onRefocus, onRefocus,
onKeepCurrent, onKeepCurrent,
onFinish,
}: PauseRefocusPromptProps) => { }: PauseRefocusPromptProps) => {
return ( return (
<div <div
@@ -88,6 +90,20 @@ export const PauseRefocusPrompt = ({
<span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-[2px]')}></span> <span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-[2px]')}></span>
</button> </button>
</div> </div>
<div className="mt-4 border-t border-white/8 pt-3">
<button
type="button"
onClick={onFinish}
disabled={isBusy}
className="text-[12px] font-medium tracking-[0.08em] text-white/58 underline decoration-white/12 underline-offset-4 transition-colors hover:text-white/82 hover:decoration-white/24 disabled:cursor-default disabled:text-white/26 disabled:no-underline"
>
{copy.space.focusHud.pausePromptFinish}
</button>
<p className="mt-1.5 max-w-[20.5rem] text-[12px] leading-[1.55] text-white/42">
{copy.space.focusHud.pausePromptFinishHint}
</p>
</div>
</div> </div>
</section> </section>
</div> </div>

View File

@@ -30,6 +30,7 @@ interface ReturnPromptProps {
onRefocus: () => void; onRefocus: () => void;
onRest?: () => void; onRest?: () => void;
onNextGoal?: () => void; onNextGoal?: () => void;
onFinish: () => void;
} }
export const ReturnPrompt = ({ export const ReturnPrompt = ({
@@ -40,6 +41,7 @@ export const ReturnPrompt = ({
onRefocus, onRefocus,
onRest, onRest,
onNextGoal, onNextGoal,
onFinish,
}: ReturnPromptProps) => { }: ReturnPromptProps) => {
const isBreakReturn = mode === 'break'; const isBreakReturn = mode === 'break';
@@ -166,6 +168,30 @@ export const ReturnPrompt = ({
</> </>
)} )}
</div> </div>
<div className="mt-4 border-t border-white/8 pt-3">
<button
type="button"
onClick={onFinish}
disabled={isBusy}
className={cn(
'text-[12px] font-medium tracking-[0.08em] underline underline-offset-4 transition-colors disabled:cursor-default disabled:no-underline',
isBreakReturn
? 'text-emerald-50/68 decoration-emerald-100/18 hover:text-emerald-50/92 hover:decoration-emerald-100/32 disabled:text-emerald-50/28'
: 'text-white/58 decoration-white/12 hover:text-white/82 hover:decoration-white/24 disabled:text-white/26',
)}
>
{copy.space.focusHud.returnPromptFinish}
</button>
<p
className={cn(
'mt-1.5 max-w-[20.5rem] text-[12px] leading-[1.55]',
isBreakReturn ? 'text-emerald-50/46' : 'text-white/42',
)}
>
{copy.space.focusHud.returnPromptFinishHint}
</p>
</div>
</div> </div>
</section> </section>
</div> </div>

View File

@@ -285,7 +285,7 @@ export const SpaceFocusHudWidget = ({
goal={normalizedGoal} goal={normalizedGoal}
microStep={microStep} microStep={microStep}
canRefocus={Boolean(hasActiveSession)} canRefocus={Boolean(hasActiveSession)}
canComplete={hasActiveSession && sessionPhase === 'focus'} canComplete={hasActiveSession && (sessionPhase === 'focus' || sessionPhase === 'break')}
showActions={!isIntentOverlayOpen} showActions={!isIntentOverlayOpen}
onOpenRefocus={() => openRefocus('goal', 'manual')} onOpenRefocus={() => openRefocus('goal', 'manual')}
onMicroStepDone={() => { onMicroStepDone={() => {
@@ -317,6 +317,10 @@ export const SpaceFocusHudWidget = ({
handleDismissReturnPrompt(); handleDismissReturnPrompt();
handleOpenCompleteSheet('next'); handleOpenCompleteSheet('next');
}} }}
onFinish={() => {
handleDismissReturnPrompt();
handleOpenCompleteSheet('choice');
}}
/> />
<PauseRefocusPrompt <PauseRefocusPrompt
open={isPausedPromptOpen} open={isPausedPromptOpen}
@@ -326,6 +330,10 @@ export const SpaceFocusHudWidget = ({
setOverlay('none'); setOverlay('none');
onStartRequested?.(); onStartRequested?.();
}} }}
onFinish={() => {
setOverlay('none');
handleOpenCompleteSheet('choice');
}}
/> />
<RefocusSheet <RefocusSheet
open={isRefocusOpen} open={isRefocusOpen}
@@ -362,6 +370,10 @@ export const SpaceFocusHudWidget = ({
void handleKeepGoalOnly(); void handleKeepGoalOnly();
}} }}
onDefineNext={handleDefineNextMicroStep} onDefineNext={handleDefineNextMicroStep}
onFinish={() => {
setIntentError(null);
handleOpenCompleteSheet('choice');
}}
/> />
<GoalCompleteSheet <GoalCompleteSheet
open={isCompleteOpen} open={isCompleteOpen}