feat(stats): recovery 통계를 서버 계약으로 연결

This commit is contained in:
2026-03-15 19:18:05 +09:00
parent 1b01ceaa8b
commit 3aba789c97
9 changed files with 122 additions and 42 deletions

View File

@@ -21,6 +21,12 @@ export interface FocusStatsSummary {
completedSessions: number;
carriedOverCount: number;
};
recovery: {
pausedSessions: number;
resumedSessions: number;
pauseRecoveryRate: number;
awayRecoveryReady: boolean;
};
trend: FocusTrendPoint[];
}

View File

@@ -49,6 +49,12 @@ const buildMockSummary = (): FocusStatsSummary => {
completedSessions: 4,
carriedOverCount: 1,
},
recovery: {
pausedSessions: 3,
resumedSessions: 2,
pauseRecoveryRate: 2 / 3,
awayRecoveryReady: true,
},
trend: [],
};
};
@@ -145,6 +151,83 @@ const buildCompletionSummary = (summary: FocusStatsSummary) => {
);
};
const buildRecoverySummary = (summary: FocusStatsSummary, source: StatsSource) => {
if (source === 'mock') {
return copy.stats.reviewRecoveryMockSummary;
}
if (summary.recovery.pausedSessions === 0) {
return copy.stats.reviewRecoveryNoPauseSummary;
}
return copy.stats.reviewRecoveryApiSummary(
formatPercent(summary.recovery.pauseRecoveryRate),
summary.recovery.resumedSessions,
summary.recovery.pausedSessions,
);
};
const buildRecoverySection = (
summary: FocusStatsSummary,
source: StatsSource,
): WeeklyReviewSection => {
if (source === 'mock') {
return {
title: copy.stats.reviewRecoveryTitle,
summary: copy.stats.reviewRecoveryMockSummary,
availability: 'ready',
metrics: [
{
id: 'recovery-pause',
label: copy.stats.reviewPauseRecovery,
value: '67%',
hint: copy.stats.reviewPauseRecoveryHint,
},
{
id: 'recovery-away',
label: copy.stats.reviewAwayRecovery,
value: '50%',
hint: copy.stats.reviewAwayRecoveryHint,
},
],
note: copy.stats.reviewRecoveryMockNote,
};
}
const availability = summary.recovery.awayRecoveryReady ? 'ready' : 'limited';
const metrics =
summary.recovery.pausedSessions > 0
? [
{
id: 'recovery-pause-rate',
label: copy.stats.reviewPauseRecovery,
value: formatPercent(summary.recovery.pauseRecoveryRate),
hint: copy.stats.reviewPauseRecoveryHint,
},
{
id: 'recovery-resumed-sessions',
label: copy.stats.reviewResumedSessions,
value: `${summary.recovery.resumedSessions}${copy.stats.countUnit}`,
hint: copy.stats.reviewResumedSessionsHint,
},
{
id: 'recovery-paused-sessions',
label: copy.stats.reviewPausedSessions,
value: `${summary.recovery.pausedSessions}${copy.stats.countUnit}`,
hint: copy.stats.reviewPausedSessionsHint,
},
]
: [];
return {
title: copy.stats.reviewRecoveryTitle,
summary: buildRecoverySummary(summary, source),
availability,
metrics,
note: availability === 'limited' ? copy.stats.reviewRecoveryPartialNote : undefined,
};
};
const buildCarryForward = (summary: FocusStatsSummary): WeeklyReviewViewModel['carryForward'] => {
const completionRate =
summary.last7Days.startedSessions > 0
@@ -255,35 +338,7 @@ const buildReviewFromSummary = (
},
],
},
recoveryQuality: {
title: copy.stats.reviewRecoveryTitle,
summary:
source === 'mock'
? copy.stats.reviewRecoveryMockSummary
: copy.stats.reviewRecoveryLimitedSummary,
availability: source === 'mock' ? 'ready' : 'limited',
metrics:
source === 'mock'
? [
{
id: 'recovery-pause',
label: copy.stats.reviewPauseRecovery,
value: '67%',
hint: copy.stats.reviewPauseRecoveryHint,
},
{
id: 'recovery-away',
label: copy.stats.reviewAwayRecovery,
value: '50%',
hint: copy.stats.reviewAwayRecoveryHint,
},
]
: [],
note:
source === 'mock'
? copy.stats.reviewRecoveryMockNote
: copy.stats.reviewRecoveryLimitedNote,
},
recoveryQuality: buildRecoverySection(summary, source),
completionQuality: {
title: copy.stats.reviewCompletionTitle,
summary: buildCompletionSummary(summary),

View File

@@ -52,11 +52,17 @@ export const app = {
reviewBestDayHint: (minutes: number) => `${minutes}분 동안 가장 오래 집중했어요.`,
reviewRecoveryTitle: '복귀 품질',
reviewRecoveryMockSummary: '이번 주에는 pause 뒤에도 다시 올라탄 흐름이 있었어요. 복귀는 적었지만 분명히 존재했어요.',
reviewRecoveryLimitedSummary: '복귀 패턴은 아직 이 review에 충분히 합쳐지지 않았어요. 지금은 시작과 마무리 흐름을 먼저 봅니다.',
reviewRecoveryNoPauseSummary: '이번 주에는 pause가 거의 없어 복귀보다 시작과 마무리 흐름이 더 선명했어요.',
reviewRecoveryApiSummary: (rate: string, resumedSessions: number, pausedSessions: number) =>
`이번 주엔 pause가 있었던 ${pausedSessions}개 세션 중 ${resumedSessions}개를 다시 이어갔어요. pause 뒤 복귀율은 ${rate}였어요.`,
reviewRecoveryMockNote: 'mock 기준으로 pause 뒤 복귀와 away 뒤 복귀를 함께 보여줍니다.',
reviewRecoveryLimitedNote: 'pause / away 복귀 집계는 다음 연결 단계에서 이 review에 포함됩니다.',
reviewRecoveryPartialNote: '현재는 pause 복귀 집계하고 있어요. 자리 비움 뒤 복귀는 다음 서버 연결 단계에서 추가됩니다.',
reviewPauseRecovery: 'pause 뒤 복귀',
reviewPauseRecoveryHint: '멈춘 뒤 다시 돌아온 비율',
reviewResumedSessions: '다시 이어간 세션',
reviewResumedSessionsHint: 'pause가 있었던 세션 중 다시 focus로 돌아온 세션',
reviewPausedSessions: '멈춘 세션',
reviewPausedSessionsHint: 'pause가 한 번 이상 있었던 세션',
reviewAwayRecovery: '자리 비움 뒤 복귀',
reviewAwayRecoveryHint: '앱을 떠났다가 다시 돌아온 비율',
reviewCompletionTitle: '마무리 품질',

View File

@@ -52,11 +52,17 @@ export const stats = {
reviewBestDayHint: (minutes: number) => `${minutes}분 동안 가장 오래 집중했어요.`,
reviewRecoveryTitle: '복귀 품질',
reviewRecoveryMockSummary: '이번 주에는 pause 뒤에도 다시 올라탄 흐름이 있었어요. 복귀는 적었지만 분명히 존재했어요.',
reviewRecoveryLimitedSummary: '복귀 패턴은 아직 이 review에 충분히 합쳐지지 않았어요. 지금은 시작과 마무리 흐름을 먼저 봅니다.',
reviewRecoveryNoPauseSummary: '이번 주에는 pause가 거의 없어 복귀보다 시작과 마무리 흐름이 더 선명했어요.',
reviewRecoveryApiSummary: (rate: string, resumedSessions: number, pausedSessions: number) =>
`이번 주엔 pause가 있었던 ${pausedSessions}개 세션 중 ${resumedSessions}개를 다시 이어갔어요. pause 뒤 복귀율은 ${rate}였어요.`,
reviewRecoveryMockNote: 'mock 기준으로 pause 뒤 복귀와 away 뒤 복귀를 함께 보여줍니다.',
reviewRecoveryLimitedNote: 'pause / away 복귀 집계는 다음 연결 단계에서 이 review에 포함됩니다.',
reviewRecoveryPartialNote: '현재는 pause 복귀 집계하고 있어요. 자리 비움 뒤 복귀는 다음 서버 연결 단계에서 추가됩니다.',
reviewPauseRecovery: 'pause 뒤 복귀',
reviewPauseRecoveryHint: '멈춘 뒤 다시 돌아온 비율',
reviewResumedSessions: '다시 이어간 세션',
reviewResumedSessionsHint: 'pause가 있었던 세션 중 다시 focus로 돌아온 세션',
reviewPausedSessions: '멈춘 세션',
reviewPausedSessionsHint: 'pause가 한 번 이상 있었던 세션',
reviewAwayRecovery: '자리 비움 뒤 복귀',
reviewAwayRecoveryHint: '앱을 떠났다가 다시 돌아온 비율',
reviewCompletionTitle: '마무리 품질',

View File

@@ -160,7 +160,7 @@ export const FocusDashboardWidget = () => {
const hasEnoughWeeklyData =
weeklySummary.last7Days.startedSessions >= 3 &&
(weeklySummary.last7Days.completedSessions >= 2 ||
review.recoveryQuality.availability === 'ready');
weeklySummary.recovery.pausedSessions > 0);
const reviewSource = searchParams.get('review');
const reviewCarryHint = searchParams.get('carryHint');
const normalizedReviewCarryHint: ReviewCarryHint | null =

View File

@@ -220,7 +220,7 @@ export const SpaceWorkspaceWidget = () => {
const hasEnoughWeeklyData =
weeklySummary.last7Days.startedSessions >= 3 &&
(weeklySummary.last7Days.completedSessions >= 2 ||
review.recoveryQuality.availability === "ready");
weeklySummary.recovery.pausedSessions > 0);
const shouldShowSecondaryReviewTeaser =
workspaceMode === "setup" &&
showReviewTeaserAfterComplete &&