@@ -7,8 +7,11 @@ import {
77 addPlan ,
88 deletePlan ,
99 deletePlanItem ,
10+ fetchAllPlanItems ,
1011 fetchPlans ,
1112 Plan ,
13+ PlanItem ,
14+ updatePlan ,
1215} from '@/services/plans/planManageService.service' ;
1316
1417import PageHeader from '@/components/common/PageHeader' ;
@@ -18,25 +21,93 @@ import PlanSection from '@/components/plans/PlanSection';
1821import SearchBar from '@/components/plans/SearchBar' ;
1922import InlineAddPlanForm from '@/components/plans/InlineAddPlanForm' ;
2023import { Target , Calendar , CheckCircle2 } from 'lucide-react' ;
24+ import Pagination from '@/components/common/Pagination' ;
2125
2226const Page = ( ) => {
2327 const [ user , setUser ] = useState < User | null > ( null ) ;
2428 const [ plans , setPlans ] = useState < Plan [ ] > ( [ ] ) ;
2529 const [ isAdding , setIsAdding ] = useState ( false ) ;
2630 const [ isLoading , setIsLoading ] = useState ( true ) ;
2731
32+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
33+ const itemsPerPage = 4 ;
34+
35+ const [ stats , setStats ] = useState ( {
36+ total : 0 ,
37+ working : 0 , // 진행중
38+ completed : 0 , // 완료됨
39+ } ) ;
40+
41+ // 카드 업데이트를 위한 stats 가져오기 메서드
42+ const fetchAndCalculate = async (
43+ uid : string ,
44+ preload ?: { plans : Plan [ ] ; items : PlanItem [ ] }
45+ ) => {
46+ try {
47+ // 1. 데이터 병렬 로드
48+ const [ fetchedPlans , fetchedItems ] = preload
49+ ? [ preload . plans , preload . items ]
50+ : await Promise . all ( [ fetchPlans ( uid ) , fetchAllPlanItems ( uid ) ] ) ;
51+
52+ setPlans ( fetchedPlans ) ; // 플랜 목록 업데이트
53+
54+ // 2. 통계 계산 로직
55+ let completedCount = 0 ;
56+ let workingCount = 0 ;
57+
58+ fetchedPlans . forEach ( ( plan ) => {
59+ const myItems = fetchedItems . filter (
60+ ( item : PlanItem ) => item . planId === plan . id
61+ ) ;
62+ // 하위 항목이 하나라도 있는 경우에만
63+ if ( myItems . length > 0 ) {
64+ const isAllChecked = myItems . every (
65+ ( item : PlanItem ) => item . isChecked
66+ ) ;
67+
68+ if ( isAllChecked ) {
69+ completedCount ++ ; // 모두 완료됨
70+ } else {
71+ workingCount ++ ; // 항목은 있는데 아직 다 완료 안 됨 -> 진행중
72+ }
73+ }
74+ } ) ;
75+
76+ // 3. 통계 State 업데이트
77+ setStats ( {
78+ total : fetchedPlans . length ,
79+ completed : completedCount ,
80+ working : workingCount ,
81+ } ) ;
82+ } catch ( err ) {
83+ console . error ( err ) ;
84+ }
85+ } ;
86+
2887 // 사용자 인증 상태 리스너 및 초기 플랜 목록 로드
2988 useEffect ( ( ) => {
3089 const unsubscribe = onAuthStateChanged ( auth , async ( currentUser ) => {
3190 setUser ( currentUser ) ;
3291
3392 if ( currentUser ) {
3493 try {
35- const fetchedPlans = await fetchPlans ( currentUser . uid ) ;
94+ const [ fetchedPlans , fetchedItems ] = await Promise . all ( [
95+ fetchPlans ( currentUser . uid ) ,
96+ fetchAllPlanItems ( currentUser . uid ) ,
97+ ] ) ;
98+
3699 setPlans ( fetchedPlans ) ;
100+ setCurrentPage ( 1 ) ;
101+
102+ await fetchAndCalculate ( currentUser . uid , {
103+ plans : fetchedPlans ,
104+ items : fetchedItems ,
105+ } ) ;
37106 } catch ( err ) {
38107 console . error ( '플랜 목록 로딩 실패:' , err ) ;
39108 setPlans ( [ ] ) ;
109+
110+ setCurrentPage ( 1 ) ;
40111 }
41112 } else {
42113 setPlans ( [ ] ) ;
@@ -58,6 +129,9 @@ const Page = () => {
58129 // 목록 새로고침
59130 const fetchedPlans = await fetchPlans ( user . uid ) ;
60131 setPlans ( fetchedPlans ) ;
132+ setCurrentPage ( 1 ) ;
133+
134+ await fetchAndCalculate ( user . uid ) ;
61135
62136 setIsAdding ( false ) ; // 폼 닫기
63137 } catch ( err ) {
@@ -88,12 +162,60 @@ const Page = () => {
88162 // 목록 새로고침
89163 const fetchedPlans = await fetchPlans ( user . uid ) ;
90164 setPlans ( fetchedPlans ) ;
165+
166+ await fetchAndCalculate ( user . uid ) ;
167+
168+ // 페이지 조절
169+ if (
170+ currentPage > Math . ceil ( ( plans . length - 1 ) / itemsPerPage ) &&
171+ Math . ceil ( ( plans . length - 1 ) / itemsPerPage ) > 0
172+ ) {
173+ setCurrentPage ( Math . ceil ( ( plans . length - 1 ) / itemsPerPage ) ) ;
174+ } else if ( Math . ceil ( ( plans . length - 1 ) / itemsPerPage ) === 0 ) {
175+ setCurrentPage ( 1 ) ;
176+ }
91177 } catch ( err ) {
92178 console . error ( err ) ;
93179 }
94180 }
95181 } ;
96182
183+ // 플랜 수정 핸들러
184+ const handleUpdatePlan = async (
185+ planId : string ,
186+ title : string ,
187+ description : string
188+ ) => {
189+ if ( ! user ) {
190+ alert ( '로그인이 필요합니다.' ) ;
191+ return ;
192+ }
193+
194+ try {
195+ await updatePlan ( user . uid , planId , { title, description } ) ;
196+
197+ // 목록 새로고침
198+ const fetchedPlans = await fetchPlans ( user . uid ) ;
199+ setPlans ( fetchedPlans ) ;
200+ } catch ( err ) {
201+ console . error ( err ) ;
202+ }
203+ } ;
204+
205+ // 페이지네이션 파트
206+ const totalPages = Math . ceil ( plans . length / itemsPerPage ) ;
207+ const startIndex = ( currentPage - 1 ) * itemsPerPage ;
208+ const endIndex = startIndex + itemsPerPage ;
209+ const paginatedPlans = plans . slice ( startIndex , endIndex ) ;
210+
211+ useEffect ( ( ) => {
212+ if ( currentPage > totalPages && totalPages > 0 ) {
213+ setCurrentPage ( totalPages ) ;
214+ } else if ( totalPages === 0 && currentPage !== 1 ) {
215+ setCurrentPage ( 1 ) ;
216+ }
217+ } , [ plans . length , totalPages , currentPage ] ) ;
218+
97219 return (
98220 < div className = "bg-background min-h-screen p-11" >
99221 < PageHeader
@@ -126,7 +248,9 @@ const Page = () => {
126248 < span className = "text-text-sub text-sm font-medium" >
127249 진행중인 플랜 수
128250 </ span >
129- < span className = "text-4xl font-bold text-[#7B44C4]" > 1</ span >
251+ < span className = "text-4xl font-bold text-[#7B44C4]" >
252+ { stats . working }
253+ </ span >
130254 </ div >
131255
132256 { /* 오른쪽: 아이콘 영역 */ }
@@ -140,7 +264,9 @@ const Page = () => {
140264 < span className = "text-text-sub text-sm font-medium" >
141265 완료된 플랜 수
142266 </ span >
143- < span className = "text-4xl font-bold text-[#00841F]" > 1</ span >
267+ < span className = "text-4xl font-bold text-[#00841F]" >
268+ { stats . completed }
269+ </ span >
144270 </ div >
145271
146272 { /* 오른쪽: 아이콘 영역 */ }
@@ -153,41 +279,52 @@ const Page = () => {
153279 { /* 검색 바 */ }
154280 < SearchBar />
155281
156- { isLoading ? (
157- < p > 플랜을 불러오는 중...</ p >
158- ) : (
159- < div >
160- < section className = "space-y-6" >
161- { plans . length > 0 && user ? (
162- plans . map ( ( plan ) => (
282+ { /* 하단 추가 버튼 or 인라인 폼 */ }
283+ < div className = "mt-6 mb-4" >
284+ { isAdding ? (
285+ < InlineAddPlanForm
286+ onSave = { handleSavePlan }
287+ onCancel = { handleCancelAdd }
288+ />
289+ ) : (
290+ < div onClick = { ( ) => setIsAdding ( true ) } >
291+ < AddPlanButton />
292+ </ div >
293+ ) }
294+ </ div >
295+
296+ < section className = "space-y-6" >
297+ { isLoading ? (
298+ < p > 플랜을 불러오는 중...</ p >
299+ ) : (
300+ < div >
301+ { paginatedPlans . length > 0 && user ? (
302+ paginatedPlans . map ( ( plan ) => (
163303 < PlanSection
164304 key = { plan . id }
165305 userId = { user . uid }
166306 planId = { plan . id }
167307 title = { plan . title }
168308 description = { plan . description }
169309 onDeletePlan = { handleDeletePlan }
310+ onUpdatePlan = { handleUpdatePlan }
311+ onChangeStats = { ( ) => fetchAndCalculate ( user ! . uid ) }
170312 />
171313 ) )
172314 ) : (
173315 < p > 아직 생성된 플랜이 없습니다. 첫 플랜을 추가해보세요!</ p >
174316 ) }
175- </ section >
176-
177- { /* 하단 추가 버튼 or 인라인 폼 */ }
178- < div className = "mt-6" >
179- { isAdding ? (
180- < InlineAddPlanForm
181- onSave = { handleSavePlan }
182- onCancel = { handleCancelAdd }
183- />
184- ) : (
185- < div onClick = { ( ) => setIsAdding ( true ) } >
186- < AddPlanButton />
187- </ div >
188- ) }
189317 </ div >
190- </ div >
318+ ) }
319+ </ section >
320+
321+ { /* 페이지네이션 컴포넌트 */ }
322+ { totalPages > 1 && (
323+ < Pagination
324+ totalPages = { totalPages }
325+ currentPage = { currentPage }
326+ onPageChange = { setCurrentPage }
327+ />
191328 ) }
192329 </ div >
193330 ) ;
0 commit comments