Skip to content

feat: 계획 관리 페이지 플랜 생성, 하위항목 추가 & 완료 기능 구현#44

Merged
GamjaIsMine02 merged 7 commits into
devfrom
feature/plan-management-page
Jan 22, 2026
Merged

feat: 계획 관리 페이지 플랜 생성, 하위항목 추가 & 완료 기능 구현#44
GamjaIsMine02 merged 7 commits into
devfrom
feature/plan-management-page

Conversation

@GamjaIsMine02
Copy link
Copy Markdown
Contributor

@GamjaIsMine02 GamjaIsMine02 commented Jan 22, 2026

1) 작업한 이슈번호

#38

2) 변경 요약 (What & Why)

  • 무엇을 변경했는지: 계획 관리 페이지에서 플랜 생성과 하위항목 추가 기능 & 하위항목 완료 기능 구현
  • 변경했는지(문제/목표):

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

image

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

  • Before:
  • After:

4) 상세 변경사항

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

5) 참고사항

Summary by CodeRabbit

  • 새로운 기능

    • 인라인 플랜 생성/편집 UI 추가 및 플랜별 인라인 할일 추가 폼 제공
    • 할일에 마감일 표시와 달력 아이콘 노출, 완료 상태 토글 지원
    • 플랜 삭제(확인 후) 및 편집/옵션 메뉴 추가
  • 개선 사항

    • 인증 기반 플랜 로드·동기화, 로딩·빈 상태 및 오류 처리 개선
    • 플랜 수를 반영한 KPI 업데이트

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 22, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

클라이언트 컴포넌트로 전환된 Plans 페이지에 Firebase Auth 연동과 Firestore 기반 plan 관리 서비스가 추가되었습니다. 인라인 계획/작업 폼과 PlanSection/TaskItem이 데이터 중심으로 재작성되어 계획 및 항목 CRUD, 토글, 삭제 흐름이 구현됩니다.

Changes

응집군 / 파일(s) 변경 요약
페이지 (클라이언트 전환)
app/(with-sidebar)/plans/page.tsx
클라이언트 컴포넌트화, onAuthStateChanged 구독, plans 상태 및 로딩/추가 플래그, fetch/add/delete 핸들러와 InlineAddPlanForm 토글 로직 추가
플랜 서비스
lib/planManageService.ts
Firestore 기반 타입/함수 추가: Plan, PlanItem, fetchPlans, fetchPlanItems, addPlan, addPlanItem, toggleItemStatus, deletePlan, deletePlanItem, updatePlanItem (타임스탬프 ↔ Date 변환 포함)
인라인 폼 컴포넌트
components/plans/InlineAddPlanForm.tsx, components/plans/InlineAddTaskForm.tsx
인라인 계획/작업 추가 UI 및 로직 도입(입력 검증, 키보드 이벤트, onSave/onCancel 콜백). 새 파일로 기본 export 추가
플랜 섹션 업데이트
components/plans/PlanSection.tsx
props가 userId, planId, title, onDelete 등으로 변경; planItems 로드, isLoading/isAdding 상태, InlineAddTaskForm 통합, 토글·추가·삭제 핸들러 및 More 메뉴 추가 (공개 API 변경)
작업 아이템 변경
components/plans/TaskItem.tsx
컴포넌트 시그니처에 id, deadline, onToggle 추가; 클릭 가능 체크박스·데드라인 표시 및 날짜 포맷 처리로 API 변경
스타일 변수 추가
app/globals.css
새로운 CSS 변수 --color-border-focus-blue 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page as Plans Page
    participant Auth as Firebase Auth
    participant Service as Plan Service
    participant Firestore as Firestore DB

    User->>Page: 페이지 방문
    Page->>Auth: onAuthStateChanged 구독
    Auth-->>Page: 사용자 uid 반환
    Page->>Service: fetchPlans(uid)
    Service->>Firestore: 쿼리 users/{uid}/plans
    Firestore-->>Service: 계획 목록 반환
    Service-->>Page: plans 배열 전달
    Page-->>User: 계획 목록 렌더링
Loading
sequenceDiagram
    participant User
    participant UI as TaskItem
    participant Section as PlanSection
    participant Service as Plan Service
    participant Firestore as Firestore DB

    User->>UI: 체크박스 클릭
    UI->>Section: onToggle(itemId, currentStatus)
    Section->>Service: toggleItemStatus(uid, itemId, !currentStatus)
    Service->>Firestore: planItems/{itemId} 업데이트
    Firestore-->>Service: 업데이트 완료
    Section->>Service: fetchPlanItems(uid, planId)
    Service->>Firestore: planItems 재조회
    Firestore-->>Service: 최신 항목 반환
    Service-->>Section: 항목 배열 전달
    Section-->>UI: UI 갱신
Loading
sequenceDiagram
    participant User
    participant Form as InlineAddPlanForm
    participant Page as Plans Page
    participant Service as Plan Service
    participant Firestore as Firestore DB

    User->>Form: 제목 입력 및 저장 클릭
    Form->>Form: 입력 검증
    Form->>Page: onSave(title, description)
    Page->>Service: addPlan(uid, title, description)
    Service->>Firestore: 새 plan 문서 생성
    Firestore-->>Service: 문서 ID 반환
    Service-->>Page: 성공 응답
    Page->>Service: fetchPlans(uid)  // 갱신
    Service->>Firestore: plans 재조회
    Firestore-->>Service: 계획 배열 반환
    Service-->>Page: plans 업데이트
    Page-->>User: 갱신된 목록 렌더링
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🐰 새 계획이 폴더에 춤추네,
입력창에 꿈을 몰아 넣고,
체크하면 불빛이 깜박,
Firestore에 살며시 묻고,
인라인으로 한 걸음 더! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
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.
Description check ❓ Inconclusive PR 설명이 이슈 번호, 변경 요약, 스크린샷을 포함하고 있으나, 상세 변경사항의 주요 섹션(라우팅/페이지, 컴포넌트, 상태관리, API 호출, 스타일, 기타)이 비워져 있습니다. 라우팅/페이지, 컴포넌트, 상태관리, API 호출, 스타일, 기타 섹션을 작성하여 변경사항의 상세 내용을 구체적으로 설명해주세요.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 계획 관리 페이지에 플랜 생성, 하위항목 추가, 완료 기능을 구현한 변경사항을 명확하게 설명하고 있으며, raw_summary의 변경 내용과 완벽하게 일치합니다.

✏️ 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: 4

🤖 Fix all issues with AI agents
In `@components/plans/InlineAddPlanForm.tsx`:
- Around line 29-31: The placeholder prop in InlineAddPlanForm.tsx is split
across two source lines causing an unintended newline character in the rendered
placeholder; fix by collapsing the placeholder into a single-line string (update
the placeholder attribute on the input/textarea in InlineAddPlanForm.tsx) so it
reads as one continuous string like "플랜 제목 (예: React Hooks 마스터하기)" without
embedded line breaks.

In `@lib/planManageService.ts`:
- Around line 53-57: The map over snapshot.docs can throw if createdAt is
null/undefined; update the createdAt accessor to use optional chaining and a
safe fallback (e.g., doc.data().createdAt?.toDate() or null) so it matches the
existing behavior in fetchPlanItems; locate the return in planManageService.ts
where snapshot.docs.map is used and replace the direct .toDate() call on
createdAt with an optional-chained conversion and consistent fallback.
- Around line 24-32: The PlanItem.deadline declaration is incompatible with
actual usage in fetchPlanItems and addPlanItem; change the deadline type to
accept absent/null values (e.g., make it optional and allow null: deadline?:
Date | null) on the PlanItem interface, then update any code in fetchPlanItems
and addPlanItem to treat deadline as possibly undefined/null (handle with null
checks or normalization) so type and runtime behavior match.
- Line 61: Rename the misspelled function fecthPlanItems to fetchPlanItems in
its declaration and update every usage to match (update the import in
PlanSection component and the four call sites where fecthPlanItems is invoked),
ensuring exported name and all references (fetchPlanItems and any default/named
export statements) are consistent to avoid runtime import errors.
🧹 Nitpick comments (6)
app/(with-sidebar)/plans/page.tsx (2)

96-96: 하드코딩된 통계 값이 있습니다.

"진행중인 플랜 수"와 "완료된 플랜 수"(line 96, 110)가 하드코딩되어 있어 line 72의 TODO 주석과 맞지 않습니다. 전체 플랜 수는 plans.length로 동적 처리되었으나 나머지는 아직입니다.

♻️ 동적 계산 예시
// 상태 계산 예시 (Plan 타입에 status 필드가 있다고 가정)
const inProgressCount = plans.filter(p => p.status === 'in_progress').length;
const completedCount = plans.filter(p => p.status === 'completed').length;

124-138: 비로그인 사용자에 대한 UX 개선이 필요합니다.

현재 로직에서 usernull일 때도 "아직 생성된 플랜이 없습니다"라는 메시지가 표시됩니다. 비로그인 사용자에게는 로그인 안내 메시지를 보여주는 것이 더 적절합니다.

♻️ 조건 분기 개선 제안
         {isLoading ? (
           <p>플랜을 불러오는 중...</p>
+        ) : !user ? (
+          <p>플랜을 보려면 로그인이 필요합니다.</p>
         ) : plans.length > 0 && user ? (
           plans.map((plan) => (
             ...
           ))
         ) : (
           <p>아직 생성된 플랜이 없습니다. 첫 플랜을 추가해보세요!</p>
         )}
components/plans/TaskItem.tsx (1)

33-42: 체크박스 접근성 개선을 고려해 주세요.

현재 divonClick을 사용하고 있어 키보드 사용자가 탭으로 포커스하거나 Enter/Space로 토글할 수 없습니다.

♻️ 접근성 개선 제안
         <div
           onClick={() => onToggle(id, isCompleted)}
+          onKeyDown={(e) => {
+            if (e.key === 'Enter' || e.key === ' ') {
+              e.preventDefault();
+              onToggle(id, isCompleted);
+            }
+          }}
+          role="checkbox"
+          aria-checked={isCompleted}
+          tabIndex={0}
           className={`flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-md border transition-colors ${
lib/planManageService.ts (3)

116-137: Firestore 배치 작업 500개 제한 주의

Firestore의 writeBatch는 최대 500개의 작업만 처리할 수 있습니다. 플랜에 499개 이상의 하위 항목이 있을 경우 배치 커밋이 실패합니다.

현재 사용 시나리오에서 이 제한에 도달할 가능성이 낮다면, 주석으로 제한사항을 명시해두거나, 항목이 많을 경우 청크 단위로 처리하는 로직을 고려하세요.

♻️ 청크 처리 예시 (필요시)
export const deletePlan = async (uid: string, planId: string) => {
  const itemsRef = collection(db, 'users', uid, 'planItems');
  const q = query(itemsRef, where('planId', '==', planId));
  const snapshot = await getDocs(q);

  // Firestore batch limit is 500 operations
  const BATCH_SIZE = 499; // Reserve 1 for the plan itself
  const docs = snapshot.docs;

  for (let i = 0; i < docs.length; i += BATCH_SIZE) {
    const batch = writeBatch(db);
    const chunk = docs.slice(i, i + BATCH_SIZE);
    chunk.forEach((doc) => batch.delete(doc.ref));
    
    // Add plan deletion only in the last batch
    if (i + BATCH_SIZE >= docs.length) {
      const planRef = doc(db, 'users', uid, 'plans', planId);
      batch.delete(planRef);
    }
    
    await batch.commit();
  }

  // If no items, still delete the plan
  if (docs.length === 0) {
    const planRef = doc(db, 'users', uid, 'plans', planId);
    await deleteDoc(planRef);
  }
};

34-45: addPlanaddPlanItem에서 생성된 문서 ID 반환 고려

현재 두 함수 모두 반환값이 없습니다. 생성된 문서의 ID를 반환하면 UI에서 낙관적 업데이트나 후속 작업에 활용할 수 있습니다.

♻️ 수정 제안
 export const addPlan = async (
   uid: string,
   title: string,
   description: string = ''
-) => {
-  await addDoc(collection(db, 'users', uid, 'plans'), {
+): Promise<string> => {
+  const docRef = await addDoc(collection(db, 'users', uid, 'plans'), {
     title,
     description,
     createdAt: Timestamp.now(),
   });
+  return docRef.id;
 };
 export const addPlanItem = async (
   uid: string,
   planId: string,
   text: string,
   deadline?: Date
-) => {
-  await addDoc(collection(db, 'users', uid, 'planItems'), {
+): Promise<string> => {
+  const docRef = await addDoc(collection(db, 'users', uid, 'planItems'), {
     planId,
     text,
     isChecked: false,
     deadline: deadline ? Timestamp.fromDate(deadline) : null,
     createdAt: Timestamp.now(),
   });
+  return docRef.id;
 };

Also applies to: 88-102


1-185: 전역 에러 핸들링 부재

모든 Firestore 호출에서 에러 핸들링이 없습니다. 네트워크 오류나 권한 문제 발생 시 호출부에서 적절히 처리해야 합니다.

에러 처리를 각 호출부에서 하는 것이 의도라면 괜찮지만, 공통 에러 로깅이나 재시도 로직이 필요하다면 이 서비스 레이어에서 처리하는 것을 고려하세요.

Comment thread components/plans/InlineAddPlanForm.tsx Outdated
Comment thread lib/planManageService.ts
Comment on lines +24 to +32
// 하위 항목 데이터 타입
export interface PlanItem {
id: string;
planId: string;
text: string;
isChecked: boolean;
deadline: Date;
createdAt: Date;
}
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 | 🟠 Major

deadline 타입이 실제 데이터와 불일치합니다.

deadline이 필수 Date 타입으로 선언되어 있지만, fecthPlanItems (line 83)에서 undefined를 반환할 수 있고, addPlanItem (line 99)에서는 null로 저장됩니다. 이로 인해 런타임에서 타입 불일치 오류가 발생할 수 있습니다.

🔧 타입 수정 제안
 export interface PlanItem {
   id: string;
   planId: string;
   text: string;
   isChecked: boolean;
-  deadline: Date;
+  deadline?: Date | null;
   createdAt: Date;
 }
🤖 Prompt for AI Agents
In `@lib/planManageService.ts` around lines 24 - 32, The PlanItem.deadline
declaration is incompatible with actual usage in fetchPlanItems and addPlanItem;
change the deadline type to accept absent/null values (e.g., make it optional
and allow null: deadline?: Date | null) on the PlanItem interface, then update
any code in fetchPlanItems and addPlanItem to treat deadline as possibly
undefined/null (handle with null checks or normalization) so type and runtime
behavior match.

Comment thread lib/planManageService.ts
Comment thread lib/planManageService.ts Outdated
@GamjaIsMine02 GamjaIsMine02 merged commit 2868abd into dev Jan 22, 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: 3

🤖 Fix all issues with AI agents
In `@app/`(with-sidebar)/plans/page.tsx:
- Around line 23-38: When auth state changes in the useEffect's
onAuthStateChanged handler, we never reset loading before starting fetchPlans
causing a flash of empty state; update the handler inside useEffect (the
callback passed to onAuthStateChanged) to call setIsLoading(true) immediately
after setUser(currentUser) (or at the very start of the callback) and then
setIsLoading(false) after plans are set/cleared (already present), ensuring
setIsLoading(true) is invoked before awaiting fetchPlans(currentUser.uid) so UI
shows a loading state during re-login/account switch.

In `@components/plans/PlanSection.tsx`:
- Around line 158-166: The "수정" button in PlanSection.tsx currently calls a
placeholder alert in its onClick handler (the button element with Edit2 icon),
which should not appear in production; either remove the onClick alert and hide
the button until the edit flow exists, or replace the onClick with a disabled
state/aria-disabled and visually muted styles and prevent click propagation, or
wire it to the actual edit handler (e.g., openEditModal / onEditPlan) if that
flow exists—update the button element (the one rendering Edit2 and "수정")
accordingly to remove the alert and implement one of these safe alternatives.
- Around line 145-152: The icon-only button in PlanSection.tsx (the button
toggling showMenu via setShowMenu and rendering <MoreVertical />) lacks an
accessible label; update the button to include a clear accessible name (e.g.,
aria-label="More options" or a visually hidden text node) and also reflect the
menu state with aria-expanded={showMenu} so screen readers know the control and
its state, preserving the existing e.stopPropagation() behavior and styling.
🧹 Nitpick comments (1)
app/(with-sidebar)/plans/page.tsx (1)

170-181: 비로그인 상태에서 추가 폼 노출 제어 고려
현재는 로그인하지 않아도 추가 버튼을 눌러 폼을 열 수 있고, 저장 시점에만 경고가 뜹니다. UX 개선을 위해 user가 없으면 버튼을 숨기거나 클릭 시 로그인 유도 처리 고려가 좋습니다.

🔧 개선 예시
-      <div className="mt-6">
-        {isAdding ? (
-          <InlineAddPlanForm
-            onSave={handleSavePlan}
-            onCancel={handleCancelAdd}
-          />
-        ) : (
-          <div onClick={() => setIsAdding(true)}>
-            <AddPlanButton />
-          </div>
-        )}
-      </div>
+      <div className="mt-6">
+        {user ? (
+          isAdding ? (
+            <InlineAddPlanForm
+              onSave={handleSavePlan}
+              onCancel={handleCancelAdd}
+            />
+          ) : (
+            <div onClick={() => setIsAdding(true)}>
+              <AddPlanButton />
+            </div>
+          )
+        ) : (
+          <p className="text-sm text-gray-400">
+            로그인 후 플랜을 추가할 수 있습니다.
+          </p>
+        )}
+      </div>

Comment on lines +23 to +38
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
setUser(currentUser);

if (currentUser) {
try {
const fetchedPlans = await fetchPlans(currentUser.uid);
setPlans(fetchedPlans);
} catch (err) {
console.error('플랜 목록 로딩 실패:', err);
setPlans([]);
}
} else {
setPlans([]);
}
setIsLoading(false);
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

재로그인/계정 전환 시 로딩 상태 초기화 필요
현재는 인증 상태가 바뀌어도 fetchPlans 시작 전에 isLoading을 다시 true로 돌리지 않아서, 로그아웃→로그인 시 빈 상태 문구가 잠깐 보일 수 있습니다.

🔧 개선 예시
     const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
+      setIsLoading(true);
       setUser(currentUser);
 
       if (currentUser) {
         try {
           const fetchedPlans = await fetchPlans(currentUser.uid);
           setPlans(fetchedPlans);
🤖 Prompt for AI Agents
In `@app/`(with-sidebar)/plans/page.tsx around lines 23 - 38, When auth state
changes in the useEffect's onAuthStateChanged handler, we never reset loading
before starting fetchPlans causing a flash of empty state; update the handler
inside useEffect (the callback passed to onAuthStateChanged) to call
setIsLoading(true) immediately after setUser(currentUser) (or at the very start
of the callback) and then setIsLoading(false) after plans are set/cleared
(already present), ensuring setIsLoading(true) is invoked before awaiting
fetchPlans(currentUser.uid) so UI shows a loading state during re-login/account
switch.

Comment on lines +145 to +152
<button
onClick={(e) => {
e.stopPropagation(); // 부모 클릭(아코디언 토글) 방지
setShowMenu(!showMenu);
}}
className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100"
>
<MoreVertical size={20} />
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

아이콘 버튼 접근성 레이블 추가 필요
아이콘만 있는 버튼은 스크린리더에서 의미를 알기 어렵습니다.

♿️ 개선 예시
-            <button
+            <button
+              aria-label="플랜 메뉴"
+              aria-haspopup="menu"
+              aria-expanded={showMenu}
               onClick={(e) => {
                 e.stopPropagation(); // 부모 클릭(아코디언 토글) 방지
                 setShowMenu(!showMenu);
               }}
🤖 Prompt for AI Agents
In `@components/plans/PlanSection.tsx` around lines 145 - 152, The icon-only
button in PlanSection.tsx (the button toggling showMenu via setShowMenu and
rendering <MoreVertical />) lacks an accessible label; update the button to
include a clear accessible name (e.g., aria-label="More options" or a visually
hidden text node) and also reflect the menu state with aria-expanded={showMenu}
so screen readers know the control and its state, preserving the existing
e.stopPropagation() behavior and styling.

Comment on lines +158 to +166
<button
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50"
onClick={(e) => {
e.stopPropagation();
alert('수정 기능');
}}
>
<Edit2 size={14} /> 수정
</button>
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

‘수정’ 메뉴가 placeholder alert로 남아 있습니다.
프로덕션에서 alert만 뜨는 액션은 혼란을 줄 수 있으니, 구현 전까지는 숨김/비활성 처리 또는 실제 편집 플로우 연결을 고려해 주세요.

🤖 Prompt for AI Agents
In `@components/plans/PlanSection.tsx` around lines 158 - 166, The "수정" button in
PlanSection.tsx currently calls a placeholder alert in its onClick handler (the
button element with Edit2 icon), which should not appear in production; either
remove the onClick alert and hide the button until the edit flow exists, or
replace the onClick with a disabled state/aria-disabled and visually muted
styles and prevent click propagation, or wire it to the actual edit handler
(e.g., openEditModal / onEditPlan) if that flow exists—update the button element
(the one rendering Edit2 and "수정") accordingly to remove the alert and implement
one of these safe alternatives.

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