Skip to content

Commit 51c6572

Browse files
jammy0903claude
andcommitted
feat: Add streak optimistic update on lesson completion
레슨 완료 시 스트릭이 즉시 업데이트되도록 구현 Changes: - store.ts: streak를 전역 상태로 추가 - TopBar.tsx: useStreak 대신 store 사용 - LessonPage.tsx: 레슨 완료 후 refreshStreak() 호출 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6afe269 commit 51c6572

3 files changed

Lines changed: 41 additions & 4 deletions

File tree

packages/frontend/src/features/courses/LessonPage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ export function LessonPage() {
267267
}>();
268268

269269
const queryClient = useQueryClient();
270-
const { setPageTitle, appUser } = useStore();
270+
const { setPageTitle, appUser, refreshStreak } = useStore();
271271

272272
// TanStack Query: 레슨 데이터 + 챕터 데이터
273273
const { data: lesson, isLoading, isError, error } = useLesson(lessonId);
@@ -524,6 +524,8 @@ export function LessonPage() {
524524
// 캐시 무효화로 UI 즉시 업데이트
525525
queryClient.invalidateQueries({ queryKey: ['progress', appUser?.id] });
526526
queryClient.invalidateQueries({ queryKey: ['language'] }); // 챕터 progress도 업데이트
527+
// 스트릭 업데이트 (백엔드에서 이미 업데이트되었으므로 UI만 refresh)
528+
await refreshStreak();
527529
} catch (err) {
528530
console.error('[Progress] Failed to save:', err);
529531
}

packages/frontend/src/layouts/TopBar.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { motion } from 'framer-motion';
77
import { Code2, Sparkles, Menu } from 'lucide-react';
88
import { useStore } from '@/stores/store';
99
import { Link } from 'react-router-dom';
10-
import { StreakCard, useStreak } from '@/features/gamification';
10+
import { StreakCard } from '@/features/gamification';
1111
import { LanguageBadge } from '@/components/ui/LanguageBadge';
1212
import { useIsMobile } from '@/hooks'; // useIsMobile import
1313
import type { SupportedLanguage } from '@/types/simulator'; // SupportedLanguage import
14+
import { useEffect } from 'react';
1415

1516
// 언어 정보 (LanguageCoursePage.tsx에서 가져옴)
1617
const getLanguageInfo = (lang: SupportedLanguage | null) => {
@@ -25,12 +26,16 @@ const getLanguageInfo = (lang: SupportedLanguage | null) => {
2526
};
2627

2728
export function TopBar() {
28-
const { sidebarOpen, toggleSidebar, pageTitle, pageSubtitle, pageLanguage, appUser } = useStore();
29-
const { streak, loading: streakLoading } = useStreak();
29+
const { sidebarOpen, toggleSidebar, pageTitle, pageSubtitle, pageLanguage, appUser, streak, streakLoading, refreshStreak } = useStore();
3030
const isMobile = useIsMobile(); // isMobile 훅 사용
3131

3232
const langInfo = pageLanguage ? getLanguageInfo(pageLanguage) : null;
3333

34+
// 초기 로드 및 appUser 변경 시 스트릭 로드
35+
useEffect(() => {
36+
refreshStreak();
37+
}, [appUser, refreshStreak]);
38+
3439
return (
3540
<header
3641
className="shrink-0 backdrop-blur-xl overflow-visible shadow-sm"

packages/frontend/src/stores/store.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { create } from 'zustand';
77
import type { User as FirebaseUser } from 'firebase/auth';
88
import type { Message, RunResult, Step } from '@/types/index';
99
import type { SupportedLanguage } from '@/types/simulator';
10+
import type { StreakStatus } from '@/services/gamification';
11+
import { getStreak } from '@/services/gamification';
12+
import { logger } from '@/utils/logger';
1013

1114
// App User 타입 (우리 DB의 User)
1215
export interface OAuthAccountInfo {
@@ -44,6 +47,11 @@ interface Store {
4447
authLoading: boolean;
4548
setAuthLoading: (loading: boolean) => void;
4649

50+
// === 스트릭 (Gamification) ===
51+
streak: StreakStatus | null;
52+
streakLoading: boolean;
53+
refreshStreak: () => Promise<void>;
54+
4755
// === 채팅 ===
4856
messages: Message[];
4957
isAiLoading: boolean;
@@ -102,6 +110,28 @@ export const useStore = create<Store>((set, get) => ({
102110
authLoading: true,
103111
setAuthLoading: (loading) => set({ authLoading: loading }),
104112

113+
// === 스트릭 (Gamification) ===
114+
streak: null,
115+
streakLoading: false,
116+
refreshStreak: async () => {
117+
const { appUser } = get();
118+
119+
// 로그인 안 된 상태
120+
if (!appUser) {
121+
set({ streak: null, streakLoading: false });
122+
return;
123+
}
124+
125+
try {
126+
set({ streakLoading: true });
127+
const data = await getStreak();
128+
set({ streak: data, streakLoading: false });
129+
} catch (error) {
130+
logger.error('Failed to fetch streak:', error);
131+
set({ streak: null, streakLoading: false });
132+
}
133+
},
134+
105135
// === 채팅 ===
106136
messages: [],
107137
isAiLoading: false,

0 commit comments

Comments
 (0)