Skip to content

Commit df88c84

Browse files
Merge pull request #53 from DeveloperBlog-Devflow/feature/plan-management-page
feat: 계획 관리 페이지 플랜 수정 기능, 페이지네이션, 상단 카드 stat 연동 구현
2 parents 8d824c2 + 82935a2 commit df88c84

4 files changed

Lines changed: 308 additions & 41 deletions

File tree

app/(with-sidebar)/plans/page.tsx

Lines changed: 162 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1417
import PageHeader from '@/components/common/PageHeader';
@@ -18,25 +21,93 @@ import PlanSection from '@/components/plans/PlanSection';
1821
import SearchBar from '@/components/plans/SearchBar';
1922
import InlineAddPlanForm from '@/components/plans/InlineAddPlanForm';
2023
import { Target, Calendar, CheckCircle2 } from 'lucide-react';
24+
import Pagination from '@/components/common/Pagination';
2125

2226
const 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

Comments
 (0)