Skip to content

feat: 계획 관리 페이지 플랜 수정 기능, 페이지네이션, 상단 카드 stat 연동 구현#53

Merged
GamjaIsMine02 merged 10 commits into
devfrom
feature/plan-management-page
Jan 28, 2026
Merged

feat: 계획 관리 페이지 플랜 수정 기능, 페이지네이션, 상단 카드 stat 연동 구현#53
GamjaIsMine02 merged 10 commits into
devfrom
feature/plan-management-page

Conversation

@GamjaIsMine02
Copy link
Copy Markdown
Contributor

@GamjaIsMine02 GamjaIsMine02 commented Jan 27, 2026

1) 작업한 이슈번호

#38

2) 변경 요약 (What & Why)

  • 무엇을 변경했는지: 계획 관리 페이지 기능 구현(플랜 수정, 페이지네이션, 상단 카드 stat 연동)
  • 변경했는지(문제/목표):

3) 스크린샷/동영상 (UI 변경 시)

image image image

전/후 비교, 반응형(모바일/데스크톱) 캡쳐

  • Before:
  • After:

4) 상세 변경사항

  • 라우팅/페이지:
  • 컴포넌트:
  • 상태관리:
  • API 호출:
  • 스타일:
  • 기타:

5) 참고사항

하위 항목 수정 기능, 검색 기능 추가 예정

Summary by CodeRabbit

  • New Features

    • 클라이언트 사이드 페이지네이션 및 페이지 네비게이션 UI 추가
    • 계획 인라인 편집(제목/설명) 저장·취소 기능 및 편집 후 즉시 반영
    • 계획 추가/삭제/수정 시 통계 자동 갱신
  • Improvements

    • 상단 통계 카드에 실시간 계산된 진행/완료 수 반영
    • 데이터 로드 병렬화로 초기 로딩·동기화 개선
    • 마감일 표시 포맷 개선(로컬 날짜 형식 사용)

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 27, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

플랜 페이지에 클라이언트 페이지네이션, 병렬 데이터 로드 기반 플랜별 통계 계산, 인라인 플랜 편집(제목/설명) 및 서비스 API(fetchAllPlanItems, updatePlan) 추가; 플랜/아이템 변경 시 통계 재계산으로 UI 동기화.

Changes

Cohort / File(s) 변경 사항
플랜 페이지 및 데이터 로딩
app/(with-sidebar)/plans/page.tsx
클라이언트 페이지네이션(currentPage, itemsPerPage, paginatedPlans), fetchPlansfetchAllPlanItems 병렬 호출, fetchAndCalculate로 플랜별 통계(total/working/completed) 계산·갱신, 추가/삭제/수정 후 통계 재요청 및 페이지 보정, Pagination 컴포넌트 사용
PlanSection 컴포넌트 편집/콜백 추가
components/plans/PlanSection.tsx
인라인 편집 모드(isEditingPlan) 추가: 제목/설명 입력 UI, 저장/취소, 헤더 클릭 시 편집 중 아코디언 토글 방지. 새 props onUpdatePlan(planId, title, description)onChangeStats() 도입(아이템 토글/삭제 등 후 통계 트리거)
플랜 서비스 API 확장 및 타입 변경
services/plans/planManageService.service.ts
PlanItem.deadline을 optional로 변경; 새 함수 fetchAllPlanItems(uid): Promise<PlanItem[]>updatePlan(uid, planId, updates) 추가(플랜 제목/설명 업데이트)
TaskItem 날짜 포맷 변경
components/plans/TaskItem.tsx
Deadline이 Date일 때 toISOString 기반 슬라이스 대신 로컬 year/month/day 조합으로 포맷 변경 (문자열 deadline 처리 기존 유지)
렌더링·상태 동기화
components/..., app/...
PlanSection 렌더링을 paginatedPlans로 변경, 상단 통계 카드에 동적 stats 적용, 인증/로딩 useEffect 정리 및 isLoading 유지

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page as Plans Page
    participant Service as Plan Service
    participant DB as Firestore
    User->>Page: 페이지 진입 / 로그인
    activate Page
    Page->>Service: fetchPlans(uid) (병렬)
    Page->>Service: fetchAllPlanItems(uid) (병렬)
    activate Service
    Service->>DB: 쿼리: plans
    Service->>DB: 쿼리: plan items
    DB-->>Service: Plans + PlanItems
    Service-->>Page: 반환 (Plans, PlanItems)
    Page->>Page: fetchAndCalculate(uid) (플랜별 통계 계산)
    Page->>Page: paginate & render (Pagination + PlanSection)
    deactivate Page
Loading
sequenceDiagram
    participant User
    participant PlanSection
    participant Page as Plans Page
    participant Service as Plan Service
    participant DB as Firestore
    User->>PlanSection: 편집 클릭 -> 제목/설명 수정 -> 저장
    activate PlanSection
    PlanSection->>Page: onUpdatePlan(planId, newTitle, newDesc)
    activate Page
    Page->>Service: updatePlan(uid, planId, {title, description})
    activate Service
    Service->>DB: Update plan document
    DB-->>Service: 성공 응답
    Service-->>Page: 성공
    Page->>Service: fetchPlans(uid)
    Service->>DB: 쿼리: 업데이트된 plans
    DB-->>Service: Updated plans
    Service-->>Page: 반환
    Page->>Page: fetchAndCalculate(uid) (통계 재계산)
    Page->>PlanSection: Re-render with updated data
    deactivate Page
    deactivate PlanSection
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 당근 한 입에 페이지를 넘기네,
제목 고치고 설명 다듬어 반짝이게,
아이템 셈해 통계로 춤추면,
페이지마다 질서 정리된 꿈 한 접시,
토끼가 박수—배포 준비 끝! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 주요 변경사항(플랜 수정, 페이지네이션, stat 연동)을 명확하게 요약하고 있으며, raw_summary의 모든 주요 기능 추가와 일치한다.
Description check ✅ Passed 필수 섹션 대부분(이슈번호, 변경 요약, 스크린샷, 참고사항)이 포함되었으나, 상세 변경사항(라우팅/페이지, 컴포넌트, 상태관리, API 호출, 스타일)이 구체적으로 작성되지 않았다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/plans/PlanSection.tsx (1)

153-168: 삭제 취소 시에도 통계 갱신이 호출됩니다.
사용자가 취소한 경우에도 onChangeStats()가 실행됩니다. 확인 후 실제 삭제 성공 시에만 호출되도록 이동해주세요.

🛠️ 수정 제안
-  if (confirm(`'${title}' 하위 항목을 정말 삭제하시겠습니까?`)) {
+  if (confirm(`'${title}' 하위 항목을 정말 삭제하시겠습니까?`)) {
     try {
       await deletePlanItem(userId, itemId);

       // 목록 새로고침
       const fetchedPlanItems = await fetchPlanItems(userId, planId);
       setTasks(fetchedPlanItems);
     } catch (err) {
       console.error(err);
     }
-  }
-
-  onChangeStats();
+    onChangeStats();
+  }
🤖 Fix all issues with AI agents
In `@app/`(with-sidebar)/plans/page.tsx:
- Around line 41-83: fetchAndCalculate currently re-queries Firestore with
fetchPlans/fetchAllPlanItems causing duplicate reads; change it to accept
optional preloaded data (e.g., parameters preloadedPlans?: Plan[],
preloadedItems?: PlanItem[]) and use those when provided, otherwise fall back to
calling fetchPlans/fetchAllPlanItems; update internal logic that currently calls
fetchPlans/fetchAllPlanItems so callers pass existing state (setPlans/plans)
into fetchAndCalculate instead of letting it refetch; ensure setPlans is only
called once by the place that originally loads plans and remove redundant
fetches at the other call sites (the other occurrences you flagged at the later
ranges).

In `@services/plans/planManageService.service.ts`:
- Around line 215-226: fetchAllPlanItems returns Firestore Timestamps for date
fields causing type mismatch; update fetchAllPlanItems to convert
doc.data().createdAt and doc.data().deadline (if present/non-null) from
Timestamp to JS Date objects before returning, mirror the behavior used in
fetchPlanItems, and ensure the returned PlanItem has deadline set to null or a
Date as appropriate so the function signature and runtime usage align.
🧹 Nitpick comments (3)
components/plans/PlanSection.tsx (2)

142-151: 플랜 삭제 시 통계 갱신이 중복 호출됩니다.
상위 핸들러에서 이미 통계를 재계산하는데 여기서도 onChangeStats()를 호출해 중복 조회/깜빡임이 생길 수 있습니다. 삭제는 상위에서만 갱신하도록 정리하는 편이 좋습니다.

♻️ 정리 제안
-  onDeletePlan(planId, title);
-
-  setShowPlanMenu(false);
-
-  onChangeStats();
+  onDeletePlan(planId, title);
+  setShowPlanMenu(false);

181-189: 플랜 제목 공백 저장 방지 처리가 필요합니다.
추가 폼과 동일하게 공백 입력을 막지 않으면 빈 제목이 저장될 수 있습니다.

🛠️ 수정 제안
   const handleSaveEdit = (e: React.MouseEvent) => {
     e.stopPropagation();
+    if (!editTitle.trim()) {
+      alert('플랜 제목을 입력해주세요.');
+      return;
+    }

     // 부모 컴포넌트에게 변경된 내용 전달
     onUpdatePlan(planId, editTitle, editDesc);

     setIsEditingPlan(false); // 수정 모드 종료
   };
app/(with-sidebar)/plans/page.tsx (1)

160-171: 페이지 보정 계산이 현재 상태와 어긋날 수 있습니다.
삭제 직후 plans.length는 이전 값이라 페이지 계산이 불안정합니다. fetchedPlans.length를 쓰거나, 아래의 페이지네이션 useEffect 하나로 일원화하는 편이 안전합니다.

♻️ 간단 수정 예시
- if (
-   currentPage > Math.ceil((plans.length - 1) / itemsPerPage) &&
-   Math.ceil((plans.length - 1) / itemsPerPage) > 0
- ) {
-   setCurrentPage(Math.ceil((plans.length - 1) / itemsPerPage));
- } else if (Math.ceil((plans.length - 1) / itemsPerPage) === 0) {
-   setCurrentPage(1);
- }
+const totalAfterDelete = Math.ceil(fetchedPlans.length / itemsPerPage);
+if (currentPage > totalAfterDelete && totalAfterDelete > 0) {
+  setCurrentPage(totalAfterDelete);
+} else if (totalAfterDelete === 0) {
+  setCurrentPage(1);
+}

Comment thread app/(with-sidebar)/plans/page.tsx
Comment thread services/plans/planManageService.service.ts Outdated
@GamjaIsMine02 GamjaIsMine02 merged commit df88c84 into dev Jan 28, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@services/plans/planManageService.service.ts`:
- Around line 215-229: fetchAllPlanItems returns null for missing deadline
(using "deadline: data.deadline?.toDate() ?? null") which mismatches the
PlanItem type (deadline?: Date) and is inconsistent with fetchPlanItems; also
the query lacks ordering so results aren’t deterministic. Update
fetchAllPlanItems to mirror fetchPlanItems: use "deadline:
data.deadline?.toDate()" (so undefined when absent), keep createdAt conversion
consistent, and add the same orderBy clause used in fetchPlanItems to the query
(referencing fetchAllPlanItems, fetchPlanItems, PlanItem, and orderBy) so
ordering and types match.
🧹 Nitpick comments (1)
app/(with-sidebar)/plans/page.tsx (1)

211-217: 페이지 조정 로직이 중복될 수 있습니다.

handleDeletePlan의 페이지 조정 로직과 이 useEffect가 동일한 역할을 합니다. handleDeletePlan에서 페이지 조정을 제거하고 이 useEffect에만 의존하거나, 둘 중 하나만 유지하는 것이 좋습니다.

Comment on lines +215 to 229
// 10. 하위 항목 모두 가져오기
export const fetchAllPlanItems = async (uid: string): Promise<PlanItem[]> => {
const allPlanItemsRef = collection(db, 'users', uid, 'planItems');
const q = query(allPlanItemsRef);
const snapshot = await getDocs(q);

return snapshot.docs.map((docSnap) => {
const data = docSnap.data();
return snapshot.docs.map((doc) => {
const data = doc.data();
return {
id: docSnap.id,
id: doc.id,
...data,
createdAt: data.createdAt?.toDate(),
deadline: data.deadline?.toDate(),
deadline: data.deadline?.toDate() ?? null,
};
}) as PlanItem[];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

deadline 반환 값이 타입과 불일치합니다.

Line 227에서 ?? null을 사용하면 deadline이 없을 때 null을 반환하지만, PlanItem 타입은 deadline?: Date (즉, Date | undefined)로 정의되어 있습니다. fetchPlanItems(line 84)는 deadline: data.deadline?.toDate()undefined를 반환하므로 동작이 일관되지 않습니다.

또한, fetchPlanItems와 달리 정렬 기준(orderBy)이 없어 결과 순서가 보장되지 않습니다.

🔧 수정 제안
 export const fetchAllPlanItems = async (uid: string): Promise<PlanItem[]> => {
   const allPlanItemsRef = collection(db, 'users', uid, 'planItems');
-  const q = query(allPlanItemsRef);
+  const q = query(allPlanItemsRef, orderBy('createdAt', 'asc'));
   const snapshot = await getDocs(q);

   return snapshot.docs.map((doc) => {
     const data = doc.data();
     return {
       id: doc.id,
       ...data,
       createdAt: data.createdAt?.toDate(),
-      deadline: data.deadline?.toDate() ?? null,
+      deadline: data.deadline?.toDate(),
     };
   }) as PlanItem[];
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 10. 하위 항목 모두 가져오기
export const fetchAllPlanItems = async (uid: string): Promise<PlanItem[]> => {
const allPlanItemsRef = collection(db, 'users', uid, 'planItems');
const q = query(allPlanItemsRef);
const snapshot = await getDocs(q);
return snapshot.docs.map((docSnap) => {
const data = docSnap.data();
return snapshot.docs.map((doc) => {
const data = doc.data();
return {
id: docSnap.id,
id: doc.id,
...data,
createdAt: data.createdAt?.toDate(),
deadline: data.deadline?.toDate(),
deadline: data.deadline?.toDate() ?? null,
};
}) as PlanItem[];
// 10. 하위 항목 모두 가져오기
export const fetchAllPlanItems = async (uid: string): Promise<PlanItem[]> => {
const allPlanItemsRef = collection(db, 'users', uid, 'planItems');
const q = query(allPlanItemsRef, orderBy('createdAt', 'asc'));
const snapshot = await getDocs(q);
return snapshot.docs.map((doc) => {
const data = doc.data();
return {
id: doc.id,
...data,
createdAt: data.createdAt?.toDate(),
deadline: data.deadline?.toDate(),
};
}) as PlanItem[];
};
🤖 Prompt for AI Agents
In `@services/plans/planManageService.service.ts` around lines 215 - 229,
fetchAllPlanItems returns null for missing deadline (using "deadline:
data.deadline?.toDate() ?? null") which mismatches the PlanItem type (deadline?:
Date) and is inconsistent with fetchPlanItems; also the query lacks ordering so
results aren’t deterministic. Update fetchAllPlanItems to mirror fetchPlanItems:
use "deadline: data.deadline?.toDate()" (so undefined when absent), keep
createdAt conversion consistent, and add the same orderBy clause used in
fetchPlanItems to the query (referencing fetchAllPlanItems, fetchPlanItems,
PlanItem, and orderBy) so ordering and types match.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant