From 14ffc888676779e4d10c6d5870bd37933dae76f4 Mon Sep 17 00:00:00 2001 From: GGGGGGGGGGGGGGG_J <115770858+NaturalSoda4552@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:25:47 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EA=B3=84=ED=9A=8D=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=94=8C=EB=9E=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/plans/planManageService.service.ts | 27 ++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/services/plans/planManageService.service.ts b/services/plans/planManageService.service.ts index f22abc8..551f02d 100644 --- a/services/plans/planManageService.service.ts +++ b/services/plans/planManageService.service.ts @@ -142,7 +142,32 @@ export const deletePlanItem = async (uid: string, itemId: string) => { await deleteDoc(itemRef); }; -// 8. 하위 항목 수정 (제목, 설명, 마감일) +// 8. 플랜 수정 (제목, 설명) +export const updatePlan = async ( + uid: string, + planId: string, + updates: { text?: string; description?: string } +) => { + const planRef = doc(db, 'users', uid, 'plans', planId); + + const updatePayload: { + text?: string; + description?: string; + } = {}; + + if (updates.text !== undefined) { + updatePayload.text = updates.text; + } + if (updates.description !== undefined) { + updatePayload.description = updates.description; + } + + if (Object.keys(updatePayload).length > 0) { + await updateDoc(planRef, updatePayload); + } +}; + +// 9. 하위 항목 수정 (제목, 설명, 마감일) export const updatePlanItem = async ( uid: string, itemId: string, From 3e06dd84c719a47df445f8c325f411d032cd11be Mon Sep 17 00:00:00 2001 From: GGGGGGGGGGGGGGG_J <115770858+NaturalSoda4552@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:34:14 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EA=B3=84=ED=9A=8D=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=94=8C=EB=9E=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(with-sidebar)/plans/page.tsx | 24 ++++++ components/plans/PlanSection.tsx | 94 ++++++++++++++++++--- services/plans/planManageService.service.ts | 8 +- 3 files changed, 112 insertions(+), 14 deletions(-) diff --git a/app/(with-sidebar)/plans/page.tsx b/app/(with-sidebar)/plans/page.tsx index 701cce3..448fce6 100644 --- a/app/(with-sidebar)/plans/page.tsx +++ b/app/(with-sidebar)/plans/page.tsx @@ -9,6 +9,7 @@ import { deletePlanItem, fetchPlans, Plan, + updatePlan, } from '@/services/plans/planManageService.service'; import PageHeader from '@/components/common/PageHeader'; @@ -94,6 +95,28 @@ const Page = () => { } }; + // 플랜 수정 핸들러 + const handleUpdatePlan = async ( + planId: string, + title: string, + description: string + ) => { + if (!user) { + alert('로그인이 필요합니다.'); + return; + } + + try { + await updatePlan(user.uid, planId, { title, description }); + + // 목록 새로고침 + const fetchedPlans = await fetchPlans(user.uid); + setPlans(fetchedPlans); + } catch (err) { + console.error(err); + } + }; + return (
{ title={plan.title} description={plan.description} onDeletePlan={handleDeletePlan} + onUpdatePlan={handleUpdatePlan} /> )) ) : ( diff --git a/components/plans/PlanSection.tsx b/components/plans/PlanSection.tsx index 924dcbb..aeaac94 100644 --- a/components/plans/PlanSection.tsx +++ b/components/plans/PlanSection.tsx @@ -2,12 +2,14 @@ import { useState, useEffect, useRef } from 'react'; import { + Check, ChevronDown, ChevronUp, Edit2, MoreVertical, Plus, Trash2, + X, } from 'lucide-react'; import Card from '../home/Card'; import TaskItem from './TaskItem'; @@ -26,6 +28,11 @@ interface PlanSectionProps { title: string; description?: string; onDeletePlan: (planId: string, title: string) => void; + onUpdatePlan: ( + planId: string, + newTitle: string, + newDescription: string + ) => void; } export default function PlanSection({ @@ -34,14 +41,18 @@ export default function PlanSection({ title, description, onDeletePlan, + onUpdatePlan, }: PlanSectionProps) { const [isOpen, setIsOpen] = useState(true); const [tasks, setTasks] = useState([]); const [isTasksLoading, setIsTasksLoading] = useState(true); const [isAddingTask, setIsAddingTask] = useState(false); - const [showPlanMenu, setShowPlanMenu] = useState(false); + const [isEditingPlan, setIsEditingPlan] = useState(false); + const [editTitle, setEditTitle] = useState(title); + const [editDesc, setEditDesc] = useState(description || ''); + const [showPlanMenu, setShowPlanMenu] = useState(false); const menuRef = useRef(null); // 1. 초기 데이터 로드 @@ -148,6 +159,33 @@ export default function PlanSection({ } }; + // 6. 플랜 수정 핸들러 + const handleStartEdit = (e: React.MouseEvent) => { + e.stopPropagation(); // 아코디언 토글 방지 + + setEditTitle(title); // props로 받은 최신값으로 초기화 + setEditDesc(description || ''); + + setIsEditingPlan(true); + setShowPlanMenu(false); // 메뉴 닫기 + }; + + const handleSaveEdit = (e: React.MouseEvent) => { + e.stopPropagation(); + + // 부모 컴포넌트에게 변경된 내용 전달 + onUpdatePlan(planId, editTitle, editDesc); + + setIsEditingPlan(false); // 수정 모드 종료 + }; + + // 7. 플랜 수정 취소 핸들러 + const handleCancelEdit = (e: React.MouseEvent) => { + e.stopPropagation(); + + setIsEditingPlan(false); // 수정 모드 종료 + }; + // 완료된 할 일 개수 계산 const completedCount = tasks.filter((t) => t.isChecked).length; const totalCount = tasks.length; @@ -163,12 +201,51 @@ export default function PlanSection({ {/* 헤더 영역 */}
setIsOpen(!isOpen)} + onClick={() => !isEditingPlan && setIsOpen(!isOpen)} > -
-

{title}

-

{description}

-
+ {isEditingPlan ? ( +
e.stopPropagation()} + > + {/* 제목 입력창 */} + setEditTitle(e.target.value)} + className="w-full border-b-2 border-[#556BD6] bg-transparent text-xl font-bold text-gray-900 focus:outline-none" + placeholder="플랜 제목" + autoFocus + /> + {/* 설명 입력창 */} + setEditDesc(e.target.value)} + className="w-full border-b border-gray-300 bg-transparent text-sm text-gray-600 focus:border-[#556BD6] focus:outline-none" + placeholder="설명 (선택사항)" + /> +
+ + +
+
+ ) : ( +
+

{title}

+

{description}

+
+ )}
@@ -193,10 +270,7 @@ export default function PlanSection({
diff --git a/services/plans/planManageService.service.ts b/services/plans/planManageService.service.ts index 551f02d..1cae0b7 100644 --- a/services/plans/planManageService.service.ts +++ b/services/plans/planManageService.service.ts @@ -146,17 +146,17 @@ export const deletePlanItem = async (uid: string, itemId: string) => { export const updatePlan = async ( uid: string, planId: string, - updates: { text?: string; description?: string } + updates: { title?: string; description?: string } ) => { const planRef = doc(db, 'users', uid, 'plans', planId); const updatePayload: { - text?: string; + title?: string; description?: string; } = {}; - if (updates.text !== undefined) { - updatePayload.text = updates.text; + if (updates.title !== undefined) { + updatePayload.title = updates.title; } if (updates.description !== undefined) { updatePayload.description = updates.description; From c198732cd95e13cc3b1a2433b109ae825a6b65d3 Mon Sep 17 00:00:00 2001 From: GGGGGGGGGGGGGGG_J <115770858+NaturalSoda4552@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:14:42 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EA=B3=84=ED=9A=8D=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=94=8C=EB=9E=9C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(with-sidebar)/plans/page.tsx | 85 +++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/app/(with-sidebar)/plans/page.tsx b/app/(with-sidebar)/plans/page.tsx index 448fce6..ce57758 100644 --- a/app/(with-sidebar)/plans/page.tsx +++ b/app/(with-sidebar)/plans/page.tsx @@ -19,6 +19,7 @@ import PlanSection from '@/components/plans/PlanSection'; import SearchBar from '@/components/plans/SearchBar'; import InlineAddPlanForm from '@/components/plans/InlineAddPlanForm'; import { Target, Calendar, CheckCircle2 } from 'lucide-react'; +import Pagination from '@/components/common/Pagination'; const Page = () => { const [user, setUser] = useState(null); @@ -26,6 +27,9 @@ const Page = () => { const [isAdding, setIsAdding] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 5; + // 사용자 인증 상태 리스너 및 초기 플랜 목록 로드 useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { @@ -35,9 +39,13 @@ const Page = () => { try { const fetchedPlans = await fetchPlans(currentUser.uid); setPlans(fetchedPlans); + + setCurrentPage(1); } catch (err) { console.error('플랜 목록 로딩 실패:', err); setPlans([]); + + setCurrentPage(1); } } else { setPlans([]); @@ -59,6 +67,7 @@ const Page = () => { // 목록 새로고침 const fetchedPlans = await fetchPlans(user.uid); setPlans(fetchedPlans); + setCurrentPage(1); setIsAdding(false); // 폼 닫기 } catch (err) { @@ -89,6 +98,16 @@ const Page = () => { // 목록 새로고침 const fetchedPlans = await fetchPlans(user.uid); setPlans(fetchedPlans); + + // 페이지 조절 + 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); + } } catch (err) { console.error(err); } @@ -117,6 +136,19 @@ const Page = () => { } }; + const totalPages = Math.ceil(plans.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedPlans = plans.slice(startIndex, endIndex); + + useEffect(() => { + if (currentPage > totalPages && totalPages > 0) { + setCurrentPage(totalPages); + } else if (totalPages === 0 && currentPage !== 1) { + setCurrentPage(1); + } + }, [plans.length, totalPages, currentPage]); + return (
{ {/* 검색 바 */} - {isLoading ? ( -

플랜을 불러오는 중...

- ) : ( -
-
- {plans.length > 0 && user ? ( - plans.map((plan) => ( +
+ {isLoading ? ( +

플랜을 불러오는 중...

+ ) : ( +
+ {paginatedPlans.length > 0 && user ? ( + paginatedPlans.map((plan) => ( { ) : (

아직 생성된 플랜이 없습니다. 첫 플랜을 추가해보세요!

)} -
- - {/* 하단 추가 버튼 or 인라인 폼 */} -
- {isAdding ? ( - - ) : ( -
setIsAdding(true)}> - -
- )}
-
+ )} + + + {/* 페이지네이션 컴포넌트 */} + {totalPages > 1 && ( + )} + + {/* 하단 추가 버튼 or 인라인 폼 */} +
+ {isAdding ? ( + + ) : ( +
setIsAdding(true)}> + +
+ )} +
); }; From 1196321854fb402d5626793b60e6d45383c615d9 Mon Sep 17 00:00:00 2001 From: GGGGGGGGGGGGGGG_J <115770858+NaturalSoda4552@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:48:28 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EA=B3=84=ED=9A=8D=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=83=88=20=ED=94=8C=EB=A0=8C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B2=84=ED=8A=BC=20=EC=9C=84=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(with-sidebar)/plans/page.tsx | 30 +++++++++++++++--------------- components/plans/PlanSection.tsx | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/(with-sidebar)/plans/page.tsx b/app/(with-sidebar)/plans/page.tsx index ce57758..ec638f4 100644 --- a/app/(with-sidebar)/plans/page.tsx +++ b/app/(with-sidebar)/plans/page.tsx @@ -28,7 +28,7 @@ const Page = () => { const [isLoading, setIsLoading] = useState(true); const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 5; + const itemsPerPage = 4; // 사용자 인증 상태 리스너 및 초기 플랜 목록 로드 useEffect(() => { @@ -208,6 +208,20 @@ const Page = () => { {/* 검색 바 */} + {/* 하단 추가 버튼 or 인라인 폼 */} +
+ {isAdding ? ( + + ) : ( +
setIsAdding(true)}> + +
+ )} +
+
{isLoading ? (

플랜을 불러오는 중...

@@ -240,20 +254,6 @@ const Page = () => { onPageChange={setCurrentPage} /> )} - - {/* 하단 추가 버튼 or 인라인 폼 */} -
- {isAdding ? ( - - ) : ( -
setIsAdding(true)}> - -
- )} -
); }; diff --git a/components/plans/PlanSection.tsx b/components/plans/PlanSection.tsx index aeaac94..b049307 100644 --- a/components/plans/PlanSection.tsx +++ b/components/plans/PlanSection.tsx @@ -43,7 +43,7 @@ export default function PlanSection({ onDeletePlan, onUpdatePlan, }: PlanSectionProps) { - const [isOpen, setIsOpen] = useState(true); + const [isOpen, setIsOpen] = useState(false); const [tasks, setTasks] = useState([]); const [isTasksLoading, setIsTasksLoading] = useState(true); const [isAddingTask, setIsAddingTask] = useState(false); From dccc0719d8beb6df6234d7a838984d52bd958f4a Mon Sep 17 00:00:00 2001 From: GGGGGGGGGGGGGGG_J <115770858+NaturalSoda4552@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:54:04 +0900 Subject: [PATCH 5/8] =?UTF-8?q?refactor:=20=EA=B3=84=ED=9A=8D=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20PlanSection=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=EC=9D=98=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=A0=95=EB=A0=AC?= =?UTF-8?q?=EC=9D=84=20=EC=A4=91=EC=95=99=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/plans/PlanSection.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/plans/PlanSection.tsx b/components/plans/PlanSection.tsx index b049307..a7d5f92 100644 --- a/components/plans/PlanSection.tsx +++ b/components/plans/PlanSection.tsx @@ -200,7 +200,7 @@ export default function PlanSection({ {/* 헤더 영역 */}
!isEditingPlan && setIsOpen(!isOpen)} > {isEditingPlan ? ( @@ -247,7 +247,7 @@ export default function PlanSection({
)} -
+
진행률 From b7b04c2de68be1faa0cb64d12bd46e5766ed2e98 Mon Sep 17 00:00:00 2001 From: GGGGGGGGGGGGGGG_J <115770858+NaturalSoda4552@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:29:29 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EA=B3=84=ED=9A=8D=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=81=EB=8B=A8=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=EC=97=90=20=EB=B3=B4=EC=97=AC=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20=EC=A0=84=EC=B2=B4=20=ED=94=8C=EB=9E=9C=20=EC=88=98?= =?UTF-8?q?,=20=EC=A7=84=ED=96=89=20=EC=A4=91=EC=9D=B8=20=ED=94=8C?= =?UTF-8?q?=EB=9E=9C=20=EC=88=98,=20=EC=99=84=EB=A3=8C=EB=90=9C=20?= =?UTF-8?q?=ED=94=8C=EB=9E=9C=20=EC=88=98=EB=A5=BC=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A7=80=EB=8F=84=EB=A1=9D=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20UI=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(with-sidebar)/plans/page.tsx | 75 +++++++++++++++++++-- components/plans/PlanSection.tsx | 9 +++ services/plans/planManageService.service.ts | 14 ++++ 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/app/(with-sidebar)/plans/page.tsx b/app/(with-sidebar)/plans/page.tsx index ec638f4..d9817e6 100644 --- a/app/(with-sidebar)/plans/page.tsx +++ b/app/(with-sidebar)/plans/page.tsx @@ -7,8 +7,10 @@ import { addPlan, deletePlan, deletePlanItem, + fetchAllPlanItems, fetchPlans, Plan, + PlanItem, updatePlan, } from '@/services/plans/planManageService.service'; @@ -30,6 +32,56 @@ const Page = () => { const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 4; + const [stats, setStats] = useState({ + total: 0, + working: 0, // 진행중 + completed: 0, // 완료됨 + }); + + // 카드 업데이트를 위한 stats 가져오기 메서드 + const fetchAndCalculate = async (uid: string) => { + try { + // 1. 데이터 병렬 로드 + const [fetchedPlans, fetchedItems] = await Promise.all([ + fetchPlans(uid), + fetchAllPlanItems(uid), + ]); + + setPlans(fetchedPlans); // 플랜 목록 업데이트 + + // 2. 통계 계산 로직 + let completedCount = 0; + let workingCount = 0; + + fetchedPlans.forEach((plan) => { + const myItems = fetchedItems.filter( + (item: PlanItem) => item.planId === plan.id + ); + // ⚠️ 하위 항목이 하나라도 있는 경우에만 상태를 판별합니다. + if (myItems.length > 0) { + const isAllChecked = myItems.every( + (item: PlanItem) => item.isChecked + ); + + if (isAllChecked) { + completedCount++; // 모두 완료됨 + } else { + workingCount++; // 항목은 있는데 아직 다 완료 안 됨 -> 진행중 + } + } + }); + + // 3. 통계 State 업데이트 + setStats({ + total: fetchedPlans.length, + completed: completedCount, + working: workingCount, + }); + } catch (err) { + console.error(err); + } + }; + // 사용자 인증 상태 리스너 및 초기 플랜 목록 로드 useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { @@ -37,10 +89,15 @@ const Page = () => { if (currentUser) { try { - const fetchedPlans = await fetchPlans(currentUser.uid); - setPlans(fetchedPlans); + const [fetchedPlans, fetchedItems] = await Promise.all([ + fetchPlans(currentUser.uid), + fetchAllPlanItems(currentUser.uid), + ]); + setPlans(fetchedPlans); setCurrentPage(1); + + await fetchAndCalculate(currentUser.uid); } catch (err) { console.error('플랜 목록 로딩 실패:', err); setPlans([]); @@ -69,6 +126,8 @@ const Page = () => { setPlans(fetchedPlans); setCurrentPage(1); + await fetchAndCalculate(user.uid); + setIsAdding(false); // 폼 닫기 } catch (err) { console.error('플랜 추가 실패:', err); @@ -99,6 +158,8 @@ const Page = () => { const fetchedPlans = await fetchPlans(user.uid); setPlans(fetchedPlans); + await fetchAndCalculate(user.uid); + // 페이지 조절 if ( currentPage > Math.ceil((plans.length - 1) / itemsPerPage) && @@ -136,6 +197,7 @@ const Page = () => { } }; + // 페이지네이션 파트 const totalPages = Math.ceil(plans.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; @@ -181,7 +243,9 @@ const Page = () => { 진행중인 플랜 수 - 1 + + {stats.working} +
{/* 오른쪽: 아이콘 영역 */} @@ -195,7 +259,9 @@ const Page = () => { 완료된 플랜 수 - 1 + + {stats.completed} +
{/* 오른쪽: 아이콘 영역 */} @@ -237,6 +303,7 @@ const Page = () => { description={plan.description} onDeletePlan={handleDeletePlan} onUpdatePlan={handleUpdatePlan} + onChangeStats={() => fetchAndCalculate(user!.uid)} /> )) ) : ( diff --git a/components/plans/PlanSection.tsx b/components/plans/PlanSection.tsx index a7d5f92..9cfa093 100644 --- a/components/plans/PlanSection.tsx +++ b/components/plans/PlanSection.tsx @@ -33,6 +33,7 @@ interface PlanSectionProps { newTitle: string, newDescription: string ) => void; + onChangeStats: () => void; } export default function PlanSection({ @@ -42,6 +43,7 @@ export default function PlanSection({ description, onDeletePlan, onUpdatePlan, + onChangeStats, }: PlanSectionProps) { const [isOpen, setIsOpen] = useState(false); const [tasks, setTasks] = useState([]); @@ -111,6 +113,8 @@ export default function PlanSection({ // 데이터 최신화: DB 업데이트 후 목록을 다시 불러옵니다. const updatedTasks = await fetchPlanItems(userId, planId); setTasks(updatedTasks); + + onChangeStats(); } catch (error) { console.error('상태 변경 실패: ', error); // 에러 시 원래대로 돌리거나 다시 불러오기 @@ -142,6 +146,8 @@ export default function PlanSection({ onDeletePlan(planId, title); setShowPlanMenu(false); + + onChangeStats(); }; // 5. 하위 항목 삭제 핸들러 @@ -157,6 +163,8 @@ export default function PlanSection({ console.error(err); } } + + onChangeStats(); }; // 6. 플랜 수정 핸들러 @@ -170,6 +178,7 @@ export default function PlanSection({ setShowPlanMenu(false); // 메뉴 닫기 }; + // 플랜 수정 핸들러 const handleSaveEdit = (e: React.MouseEvent) => { e.stopPropagation(); diff --git a/services/plans/planManageService.service.ts b/services/plans/planManageService.service.ts index ad1ec4d..f3c87d7 100644 --- a/services/plans/planManageService.service.ts +++ b/services/plans/planManageService.service.ts @@ -211,3 +211,17 @@ export const updatePlanItem = async ( await updateDoc(itemRef, updatePayload); } }; + +// 10. 하위 항목 모두 가져오기 +export const fetchAllPlanItems = async (uid: string): Promise => { + const allPlanItemsRef = collection(db, 'users', uid, 'planItems'); + const q = query(allPlanItemsRef); + const snapshot = await getDocs(q); + + return snapshot.docs.map((doc) => ({ + id: doc.id, + planId: doc.data().planId, + isChecked: doc.data().isChecked, + ...doc.data(), + })) as PlanItem[]; +}; From e7ab340fccf3283b546850091d3512c493b0d055 Mon Sep 17 00:00:00 2001 From: GGGGGGGGGGGGGGG_J <115770858+NaturalSoda4552@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:55:10 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=EC=8B=9C=EC=B0=A8=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=B4=20=EC=84=A4=EC=A0=95=ED=95=9C=20=EB=A7=88?= =?UTF-8?q?=EA=B0=90=EC=9D=BC=EA=B3=BC=20=EB=B3=B4=EC=97=AC=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20=EB=A7=88=EA=B0=90=EC=9D=BC=EC=9D=B4=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/plans/TaskItem.tsx | 8 +++++++- services/plans/planManageService.service.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/plans/TaskItem.tsx b/components/plans/TaskItem.tsx index f8613f2..c4a2f23 100644 --- a/components/plans/TaskItem.tsx +++ b/components/plans/TaskItem.tsx @@ -28,7 +28,13 @@ export default function TaskItem({ const formatDate = (d: string | Date | undefined) => { if (!d) return ''; if (typeof d === 'string') return d; - return d.toISOString().split('T')[0]; // YYYY-MM-DD 형식 + + // 로컬 시간 메서드 사용 (시차 방지) + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; }; // 1-2. 마우스 클릭 감지 diff --git a/services/plans/planManageService.service.ts b/services/plans/planManageService.service.ts index f3c87d7..c926b1d 100644 --- a/services/plans/planManageService.service.ts +++ b/services/plans/planManageService.service.ts @@ -28,7 +28,7 @@ export interface PlanItem { planId: string; text: string; isChecked: boolean; - deadline: Date; + deadline?: Date; createdAt: Date; } From 9bd792f845ec648eed17960deae3fa62d7966e86 Mon Sep 17 00:00:00 2001 From: GGGGGGGGGGGGGGG_J <115770858+NaturalSoda4552@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:00:42 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20=EB=A1=9C=EB=94=A9=20=EB=B9=84?= =?UTF-8?q?=EC=9A=A9=20=EC=A4=84=EC=9D=B4=EA=B8=B0=20=EB=B0=8F=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=ED=99=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(with-sidebar)/plans/page.tsx | 19 ++++++++++++------- services/plans/planManageService.service.ts | 15 +++++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/(with-sidebar)/plans/page.tsx b/app/(with-sidebar)/plans/page.tsx index d9817e6..c811ec4 100644 --- a/app/(with-sidebar)/plans/page.tsx +++ b/app/(with-sidebar)/plans/page.tsx @@ -39,13 +39,15 @@ const Page = () => { }); // 카드 업데이트를 위한 stats 가져오기 메서드 - const fetchAndCalculate = async (uid: string) => { + const fetchAndCalculate = async ( + uid: string, + preload?: { plans: Plan[]; items: PlanItem[] } + ) => { try { // 1. 데이터 병렬 로드 - const [fetchedPlans, fetchedItems] = await Promise.all([ - fetchPlans(uid), - fetchAllPlanItems(uid), - ]); + const [fetchedPlans, fetchedItems] = preload + ? [preload.plans, preload.items] + : await Promise.all([fetchPlans(uid), fetchAllPlanItems(uid)]); setPlans(fetchedPlans); // 플랜 목록 업데이트 @@ -57,7 +59,7 @@ const Page = () => { const myItems = fetchedItems.filter( (item: PlanItem) => item.planId === plan.id ); - // ⚠️ 하위 항목이 하나라도 있는 경우에만 상태를 판별합니다. + // 하위 항목이 하나라도 있는 경우에만 if (myItems.length > 0) { const isAllChecked = myItems.every( (item: PlanItem) => item.isChecked @@ -97,7 +99,10 @@ const Page = () => { setPlans(fetchedPlans); setCurrentPage(1); - await fetchAndCalculate(currentUser.uid); + await fetchAndCalculate(currentUser.uid, { + plans: fetchedPlans, + items: fetchedItems, + }); } catch (err) { console.error('플랜 목록 로딩 실패:', err); setPlans([]); diff --git a/services/plans/planManageService.service.ts b/services/plans/planManageService.service.ts index c926b1d..b8328f9 100644 --- a/services/plans/planManageService.service.ts +++ b/services/plans/planManageService.service.ts @@ -218,10 +218,13 @@ export const fetchAllPlanItems = async (uid: string): Promise => { const q = query(allPlanItemsRef); const snapshot = await getDocs(q); - return snapshot.docs.map((doc) => ({ - id: doc.id, - planId: doc.data().planId, - isChecked: doc.data().isChecked, - ...doc.data(), - })) as PlanItem[]; + return snapshot.docs.map((doc) => { + const data = doc.data(); + return { + id: doc.id, + ...data, + createdAt: data.createdAt?.toDate(), + deadline: data.deadline?.toDate() ?? null, + }; + }) as PlanItem[]; };