feat: 계획 관리 페이지 플랜 생성, 하위항목 추가 & 완료 기능 구현#44
Conversation
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthrough클라이언트 컴포넌트로 전환된 Plans 페이지에 Firebase Auth 연동과 Firestore 기반 plan 관리 서비스가 추가되었습니다. 인라인 계획/작업 폼과 PlanSection/TaskItem이 데이터 중심으로 재작성되어 계획 및 항목 CRUD, 토글, 삭제 흐름이 구현됩니다. Changes
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: 계획 목록 렌더링
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 갱신
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: 갱신된 목록 렌더링
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
시
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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 개선이 필요합니다.현재 로직에서
user가null일 때도 "아직 생성된 플랜이 없습니다"라는 메시지가 표시됩니다. 비로그인 사용자에게는 로그인 안내 메시지를 보여주는 것이 더 적절합니다.♻️ 조건 분기 개선 제안
{isLoading ? ( <p>플랜을 불러오는 중...</p> + ) : !user ? ( + <p>플랜을 보려면 로그인이 필요합니다.</p> ) : plans.length > 0 && user ? ( plans.map((plan) => ( ... )) ) : ( <p>아직 생성된 플랜이 없습니다. 첫 플랜을 추가해보세요!</p> )}components/plans/TaskItem.tsx (1)
33-42: 체크박스 접근성 개선을 고려해 주세요.현재
div에onClick을 사용하고 있어 키보드 사용자가 탭으로 포커스하거나 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:addPlan과addPlanItem에서 생성된 문서 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 호출에서 에러 핸들링이 없습니다. 네트워크 오류나 권한 문제 발생 시 호출부에서 적절히 처리해야 합니다.
에러 처리를 각 호출부에서 하는 것이 의도라면 괜찮지만, 공통 에러 로깅이나 재시도 로직이 필요하다면 이 서비스 레이어에서 처리하는 것을 고려하세요.
| // 하위 항목 데이터 타입 | ||
| export interface PlanItem { | ||
| id: string; | ||
| planId: string; | ||
| text: string; | ||
| isChecked: boolean; | ||
| deadline: Date; | ||
| createdAt: Date; | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
| 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); |
There was a problem hiding this comment.
재로그인/계정 전환 시 로딩 상태 초기화 필요
현재는 인증 상태가 바뀌어도 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.
| <button | ||
| onClick={(e) => { | ||
| e.stopPropagation(); // 부모 클릭(아코디언 토글) 방지 | ||
| setShowMenu(!showMenu); | ||
| }} | ||
| className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100" | ||
| > | ||
| <MoreVertical size={20} /> |
There was a problem hiding this comment.
아이콘 버튼 접근성 레이블 추가 필요
아이콘만 있는 버튼은 스크린리더에서 의미를 알기 어렵습니다.
♿️ 개선 예시
- <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.
| <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> |
There was a problem hiding this comment.
‘수정’ 메뉴가 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.
1) 작업한 이슈번호
#38
2) 변경 요약 (What & Why)
3) 스크린샷/동영상 (UI 변경 시)
4) 상세 변경사항
5) 참고사항
Summary by CodeRabbit
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.