Skip to content

Commit adb2196

Browse files
committed
feat: 계획 관리 페이지 하위 항목 삭제 기능 추가
1 parent 1fae9ec commit adb2196

3 files changed

Lines changed: 112 additions & 21 deletions

File tree

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
import { useState, useEffect } from 'react';
44
import { User, onAuthStateChanged } from 'firebase/auth';
55
import { auth } from '@/lib/firebase';
6-
import { addPlan, deletePlan, fetchPlans, Plan } from '@/lib/planManageService';
6+
import {
7+
addPlan,
8+
deletePlan,
9+
deletePlanItem,
10+
fetchPlans,
11+
Plan,
12+
} from '@/lib/planManageService';
713

814
import PageHeader from '@/components/common/PageHeader';
915
import Card from '@/components/home/Card';
@@ -160,7 +166,7 @@ const Page = () => {
160166
planId={plan.id}
161167
title={plan.title}
162168
description={plan.description}
163-
onDelete={handleDeletePlan}
169+
onDeletePlan={handleDeletePlan}
164170
/>
165171
))
166172
) : (

components/plans/PlanSection.tsx

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,30 @@ import {
1717
toggleItemStatus,
1818
PlanItem,
1919
addPlanItem,
20-
deletePlan,
20+
deletePlanItem,
2121
} from '@/lib/planManageService';
2222

2323
interface PlanSectionProps {
2424
userId: string;
2525
planId: string;
2626
title: string;
2727
description?: string;
28-
onDelete: (planId: string, title: string) => void;
28+
onDeletePlan: (planId: string, title: string) => void;
2929
}
3030

3131
export default function PlanSection({
3232
userId,
3333
planId,
3434
title,
3535
description,
36-
onDelete,
36+
onDeletePlan,
3737
}: PlanSectionProps) {
3838
const [isOpen, setIsOpen] = useState(true);
3939
const [tasks, setTasks] = useState<PlanItem[]>([]);
4040
const [isTasksLoading, setIsTasksLoading] = useState(true);
4141
const [isAddingTask, setIsAddingTask] = useState(false);
4242

43-
const [showMenu, setShowMenu] = useState(false);
43+
const [showPlanMenu, setShowPlanMenu] = useState(false);
4444

4545
const menuRef = useRef<HTMLDivElement>(null);
4646

@@ -69,11 +69,11 @@ export default function PlanSection({
6969
const handleClickOutside = (event: MouseEvent) => {
7070
// 메뉴가 열려있고, 클릭된 요소가 menuRef 내부가 아니라면 닫기
7171
if (
72-
showMenu &&
72+
showPlanMenu &&
7373
menuRef.current &&
7474
!menuRef.current.contains(event.target as Node)
7575
) {
76-
setShowMenu(false);
76+
setShowPlanMenu(false);
7777
}
7878
};
7979
document.addEventListener('mousedown', handleClickOutside);
@@ -82,7 +82,7 @@ export default function PlanSection({
8282
return () => {
8383
document.removeEventListener('mousedown', handleClickOutside);
8484
};
85-
}, [showMenu]);
85+
}, [showPlanMenu]);
8686

8787
// 2. 하위 항목 상태(완료/미완료) 토글 핸들러
8888
const handleToggleTask = async (itemId: string, currentStatus: boolean) => {
@@ -101,7 +101,7 @@ export default function PlanSection({
101101
const updatedTasks = await fetchPlanItems(userId, planId);
102102
setTasks(updatedTasks);
103103
} catch (error) {
104-
console.error('상태 변경 실패:', error);
104+
console.error('상태 변경 실패: ', error);
105105
// 에러 시 원래대로 돌리거나 다시 불러오기
106106
const rolledBackTasks = await fetchPlanItems(userId, planId);
107107
setTasks(rolledBackTasks);
@@ -120,17 +120,32 @@ export default function PlanSection({
120120

121121
setIsAddingTask(false); // 입력 폼 닫기
122122
} catch (error) {
123-
console.error('하위 항목 추가 실패:', error);
123+
console.error('하위 항목 추가 실패: ', error);
124124
}
125125
};
126126

127-
//
128-
const handleDeleteClick = (e: React.MouseEvent) => {
127+
// 4. 플랜 삭제 핸들러
128+
const handleDeletePlan = (e: React.MouseEvent) => {
129129
e.stopPropagation(); // 카드 열림/닫힘 방지
130130

131-
onDelete(planId, title);
131+
onDeletePlan(planId, title);
132132

133-
setShowMenu(false);
133+
setShowPlanMenu(false);
134+
};
135+
136+
// 5. 하위 항목 삭제 핸들러
137+
const handleDeletePlanItem = async (itemId: string, title: string) => {
138+
if (confirm(`'${title}' 하위 항목을 정말 삭제하시겠습니까?`)) {
139+
try {
140+
await deletePlanItem(userId, itemId);
141+
142+
// 목록 새로고침
143+
const fetchedPlanItems = await fetchPlanItems(userId, planId);
144+
setTasks(fetchedPlanItems);
145+
} catch (err) {
146+
console.error(err);
147+
}
148+
}
134149
};
135150

136151
// 완료된 할 일 개수 계산
@@ -140,7 +155,7 @@ export default function PlanSection({
140155
// isOpen 상태 false 시 포커스 초기화
141156
useEffect(() => {
142157
setIsAddingTask(false); //
143-
setShowMenu(false); // 드롭다운 메뉴
158+
setShowPlanMenu(false); // 드롭다운 메뉴
144159
}, [isOpen]);
145160

146161
return (
@@ -166,28 +181,28 @@ export default function PlanSection({
166181
<button
167182
onClick={(e) => {
168183
e.stopPropagation(); // 부모 클릭(아코디언 토글) 방지
169-
setShowMenu(!showMenu);
184+
setShowPlanMenu(!showPlanMenu);
170185
}}
171186
className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100"
172187
>
173188
<MoreVertical size={20} />
174189
</button>
175190

176191
{/* 드롭다운 메뉴 (절대 위치) */}
177-
{showMenu && (
192+
{showPlanMenu && (
178193
<div className="absolute top-8 right-0 w-32 overflow-hidden rounded-lg border border-gray-100 bg-white py-1 shadow-lg">
179194
<button
180195
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50"
181196
onClick={(e) => {
182197
e.stopPropagation();
183-
alert('수정 기능');
198+
alert('플랜 수정 기능');
184199
}}
185200
>
186201
<Edit2 size={14} /> 수정
187202
</button>
188203
<button
189204
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50"
190-
onClick={handleDeleteClick}
205+
onClick={handleDeletePlan}
191206
>
192207
<Trash2 size={14} /> 삭제
193208
</button>
@@ -222,6 +237,7 @@ export default function PlanSection({
222237
deadline={task.deadline}
223238
isCompleted={task.isChecked}
224239
onToggle={handleToggleTask}
240+
onDelete={handleDeletePlanItem}
225241
/>
226242
))
227243
)}

components/plans/TaskItem.tsx

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { Check, Calendar } from 'lucide-react';
1+
'use client';
2+
3+
import { Check, Calendar, Edit2, Trash2, MoreVertical } from 'lucide-react';
4+
import { useEffect, useRef, useState } from 'react';
25

36
interface TaskItemProps {
47
id: string; // id (토글 식별용)
58
text: string;
69
isCompleted: boolean;
710
deadline?: string | Date; // Date 타입 받을 수 있게 유연하게
811
onToggle: (id: string, currentStatus: boolean) => void; // 토글 핸들러
12+
onDelete: (itemId: string, text: string) => void;
913
}
1014

1115
export default function TaskItem({
@@ -14,14 +18,45 @@ export default function TaskItem({
1418
isCompleted,
1519
deadline,
1620
onToggle,
21+
onDelete,
1722
}: TaskItemProps) {
23+
const [showPlanItemMenu, setShowPlanItemMenu] = useState(false);
24+
25+
const menuRef = useRef<HTMLDivElement>(null);
26+
1827
// 날짜 포맷팅 (Date 객체나 문자열 모두 처리)
1928
const formatDate = (d: string | Date | undefined) => {
2029
if (!d) return '';
2130
if (typeof d === 'string') return d;
2231
return d.toISOString().split('T')[0]; // YYYY-MM-DD 형식
2332
};
2433

34+
// 1-2. 마우스 클릭 감지
35+
useEffect(() => {
36+
// 메뉴가 닫혀있으면 실행하지 않음
37+
if (!showPlanItemMenu) return;
38+
39+
const handleClickOutside = (event: MouseEvent) => {
40+
// menuRef가 존재하고, 클릭한 요소가 menuRef 내부가 아니라면 닫기
41+
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
42+
setShowPlanItemMenu(false);
43+
}
44+
};
45+
46+
document.addEventListener('mousedown', handleClickOutside);
47+
return () => {
48+
document.removeEventListener('mousedown', handleClickOutside);
49+
};
50+
}, [showPlanItemMenu]);
51+
52+
const handleDeletePlanItem = (e: React.MouseEvent) => {
53+
e.stopPropagation(); // 카드 열림/닫힘 방지
54+
55+
onDelete(id, text);
56+
57+
setShowPlanItemMenu(false);
58+
};
59+
2560
return (
2661
<div
2762
className={`mb-2 flex items-center justify-between rounded-xl p-4 transition-colors ${
@@ -59,6 +94,40 @@ export default function TaskItem({
5994
)}
6095
</div>
6196
</div>
97+
<div
98+
ref={menuRef}
99+
className={`relative ${showPlanItemMenu ? 'z-50' : 'z-10'}`}
100+
>
101+
<button
102+
onClick={(e) => {
103+
e.stopPropagation(); // 부모 클릭(아코디언 토글) 방지
104+
setShowPlanItemMenu(!showPlanItemMenu);
105+
}}
106+
className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100"
107+
>
108+
<MoreVertical size={20} />
109+
</button>
110+
111+
{showPlanItemMenu && (
112+
<div className="absolute top-8 right-0 w-32 overflow-hidden rounded-lg border border-gray-100 bg-white py-1 shadow-lg">
113+
<button
114+
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50"
115+
onClick={(e) => {
116+
e.stopPropagation();
117+
alert('수정 기능');
118+
}}
119+
>
120+
<Edit2 size={14} /> 수정
121+
</button>
122+
<button
123+
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50"
124+
onClick={handleDeletePlanItem}
125+
>
126+
<Trash2 size={14} /> 삭제
127+
</button>
128+
</div>
129+
)}
130+
</div>
62131
</div>
63132
);
64133
}

0 commit comments

Comments
 (0)