@@ -14,7 +14,7 @@ import { CourseGrid } from './components/CourseGrid';
1414import { LessonCard } from './components/LessonCard' ;
1515import { useStore } from '@/stores/store' ;
1616import { ChevronLeft , BookOpen , Target , CheckCircle2 , Circle , Lock } from 'lucide-react' ;
17- import { useIsMobile , useLanguageCourse } from '@/hooks' ;
17+ import { useIsMobile , useChapter , useChapterProgress } from '@/hooks' ;
1818import { getLessonFull } from '@/services/courses' ;
1919
2020type LessonProgressSnapshot = {
@@ -27,14 +27,6 @@ type LessonWithProgress = {
2727 progress ?: LessonProgressSnapshot | null ;
2828} ;
2929
30- type ChapterWithAggregatedProgress = {
31- progress ?: {
32- total ?: number ;
33- completed ?: number ;
34- percentage ?: number ;
35- } ;
36- } ;
37-
3830// 언어별 색상 (챕터 페이지용)
3931const getLanguageColor = ( lang : string | undefined ) => {
4032 switch ( lang ) {
@@ -59,89 +51,43 @@ export function ChapterLessonsPage() {
5951 const isMobile = useIsMobile ( ) ;
6052 const locale = i18n . resolvedLanguage || i18n . language ;
6153
62- // TanStack Query: 언어 코스 데이터 + 진행 상태
63- const { data : languageData , isLoading, isError, error } = useLanguageCourse ( lang ) ;
64- const chapter = useMemo (
65- ( ) => languageData ?. chapters ?. find ( ( ch ) => ch . id === chapterId ) ,
66- [ languageData , chapterId ]
67- ) ;
54+ // TanStack Query: 챕터 단건 + 진행 상태 단건(로그인 시)
55+ const { data : chapter , isLoading, isError, error } = useChapter ( chapterId ) ;
56+ const { data : chapterProgressData } = useChapterProgress ( chapterId ) ;
6857
6958 // 구독 시스템 제거됨 - 챕터 2 이상은 로그인만 필요
7059 const accessDenied = ! ! chapter && ! appUser && chapter . order >= 2 ;
7160 const accessReason = accessDenied ? t ( 'chapter.login_required_for_chapter' ) : '' ;
7261
62+ const progressByLessonId = useMemo ( ( ) => {
63+ const map = new Map < string , LessonProgressSnapshot | null > ( ) ;
64+ for ( const lesson of chapterProgressData ?. lessons ?? [ ] ) {
65+ map . set ( lesson . id , lesson . progress ?? null ) ;
66+ }
67+ return map ;
68+ } , [ chapterProgressData ] ) ;
69+
7370 const getLessonProgress = useCallback ( ( lesson : LessonWithProgress ) : LessonProgressSnapshot | null => {
74- return lesson . progress ?? null ;
75- } , [ ] ) ;
71+ return progressByLessonId . get ( lesson . id ) ?? null ;
72+ } , [ progressByLessonId ] ) ;
7673
77- // 챕터 레슨 프리페치
78- // - 1순위: 첫 미완료 레슨
79- // - 2순위: 나머지 레슨 순서대로
80- // - 동시성 제한: 2
81- // - 페이지 이탈 시 중단
74+ // 챕터 레슨 프리페치 (경량)
75+ // - 첫 미완료 레슨 1개만 예열
76+ // WHY: 이전에는 레슨 전체를 순차 프리페치해서 이동 시 체감 지연을 유발함
8277 useEffect ( ( ) => {
8378 if ( ! chapter ?. lessons || chapter . lessons . length === 0 || accessDenied ) return ;
8479
8580 const lessons = [ ...chapter . lessons ] . sort ( ( a , b ) => a . order - b . order ) ;
8681 const firstIncomplete = lessons . find ( ( l ) => getLessonProgress ( l as LessonWithProgress ) ?. status !== 'completed' ) ;
82+ const targetLessonId = firstIncomplete ?. id ?? lessons [ 0 ] ?. id ;
83+ if ( ! targetLessonId ) return ;
8784
88- const orderedIds : string [ ] = [ ] ;
89- if ( firstIncomplete ) orderedIds . push ( firstIncomplete . id ) ;
90- for ( const lesson of lessons ) {
91- if ( lesson . id !== firstIncomplete ?. id ) {
92- orderedIds . push ( lesson . id ) ;
93- }
94- }
95-
96- // 중복 방지
97- const uniqueOrderedIds = Array . from ( new Set ( orderedIds ) ) ;
98- const concurrency = Math . min ( 2 , uniqueOrderedIds . length ) ;
99-
100- // 저우선순위: 다음 챕터의 첫 레슨 1개
101- const sortedChapters = [ ...( languageData ?. chapters ?? [ ] ) ] . sort ( ( a , b ) => a . order - b . order ) ;
102- const currentChapterIndex = sortedChapters . findIndex ( ( ch ) => ch . id === chapter . id ) ;
103- const nextChapter = currentChapterIndex >= 0 ? sortedChapters [ currentChapterIndex + 1 ] : undefined ;
104- const nextChapterFirstLessonId = nextChapter ?. lessons ?. length
105- ? [ ...nextChapter . lessons ] . sort ( ( a , b ) => a . order - b . order ) [ 0 ] ?. id
106- : undefined ;
107-
108- let cancelled = false ;
109- let cursor = 0 ;
110-
111- const worker = async ( ) => {
112- while ( ! cancelled && cursor < uniqueOrderedIds . length ) {
113- const index = cursor ;
114- cursor += 1 ;
115- const lessonId = uniqueOrderedIds [ index ] ;
116- if ( ! lessonId ) continue ;
117-
118- await queryClient . prefetchQuery ( {
119- queryKey : [ 'lesson' , lessonId , locale ] ,
120- queryFn : ( ) => getLessonFull ( lessonId ) ,
121- staleTime : 5 * 60 * 1000 ,
122- } ) ;
123- }
124- } ;
125-
126- const runPrefetchQueue = async ( ) => {
127- await Promise . all ( Array . from ( { length : concurrency } , ( ) => worker ( ) ) ) ;
128-
129- // 현재 챕터 예열이 끝난 뒤, 다음 챕터 첫 레슨 1개만 예열
130- if ( ! cancelled && nextChapterFirstLessonId && ! uniqueOrderedIds . includes ( nextChapterFirstLessonId ) ) {
131- await queryClient . prefetchQuery ( {
132- queryKey : [ 'lesson' , nextChapterFirstLessonId , locale ] ,
133- queryFn : ( ) => getLessonFull ( nextChapterFirstLessonId ) ,
134- staleTime : 5 * 60 * 1000 ,
135- } ) ;
136- }
137- } ;
138-
139- void runPrefetchQueue ( ) ;
140-
141- return ( ) => {
142- cancelled = true ;
143- } ;
144- } , [ chapter , queryClient , accessDenied , languageData , getLessonProgress , locale ] ) ;
85+ void queryClient . prefetchQuery ( {
86+ queryKey : [ 'lesson' , targetLessonId , locale ] ,
87+ queryFn : ( ) => getLessonFull ( targetLessonId ) ,
88+ staleTime : 5 * 60 * 1000 ,
89+ } ) ;
90+ } , [ chapter , queryClient , accessDenied , getLessonProgress , locale ] ) ;
14591
14692 // hover 시 해당 레슨 프리페치
14793 const handlePrefetch = useCallback ( ( lessonId : string ) => {
@@ -159,15 +105,14 @@ export function ChapterLessonsPage() {
159105 }
160106 } , [ chapter , setPageTitle , lang ] ) ;
161107
162- // 진행률: 백엔드에서 계산된 chapter. progress 단일 소스 사용
108+ // 진행률: 로그인 사용자는 /chapters/:id/ progress 값 사용, 비로그인은 0%
163109 const chapterProgress = useMemo ( ( ) => {
164110 if ( ! chapter ) return { total : 0 , completed : 0 , percentage : 0 } ;
165- const chapterWithProgress = chapter as typeof chapter & ChapterWithAggregatedProgress ;
166- const total = chapterWithProgress . progress ?. total ?? chapter . lessons ?. length ?? 0 ;
167- const completed = chapterWithProgress . progress ?. completed ?? 0 ;
168- const percentage = chapterWithProgress . progress ?. percentage ?? ( total > 0 ? Math . round ( ( completed / total ) * 100 ) : 0 ) ;
111+ const total = chapterProgressData ?. totalCount ?? chapter . lessons ?. length ?? 0 ;
112+ const completed = chapterProgressData ?. completedCount ?? 0 ;
113+ const percentage = total > 0 ? Math . round ( ( completed / total ) * 100 ) : 0 ;
169114 return { total, completed, percentage } ;
170- } , [ chapter ] ) ;
115+ } , [ chapter , chapterProgressData ] ) ;
171116
172117 const totalLessons = chapterProgress . total ;
173118 const completedLessons = chapterProgress . completed ;
0 commit comments