feat(stats): recovery 통계를 서버 계약으로 연결
This commit is contained in:
@@ -21,6 +21,12 @@ export interface FocusStatsSummary {
|
||||
completedSessions: number;
|
||||
carriedOverCount: number;
|
||||
};
|
||||
recovery: {
|
||||
pausedSessions: number;
|
||||
resumedSessions: number;
|
||||
pauseRecoveryRate: number;
|
||||
awayRecoveryReady: boolean;
|
||||
};
|
||||
trend: FocusTrendPoint[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: '마무리 품질',
|
||||
|
||||
@@ -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: '마무리 품질',
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user