Skip to content

Commit 92176c0

Browse files
authored
Merge pull request #59 from DeveloperBlog-Devflow/feature/plan-management-page
feat: 계획 관리 페이지 하위 항목 수정 기능, 플랜 검색 기능 구현
2 parents ff7f1b3 + c0edc05 commit 92176c0

4 files changed

Lines changed: 204 additions & 76 deletions

File tree

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const Page = () => {
3838
completed: 0, // 완료됨
3939
});
4040

41+
const [searchQuery, setSearchQuery] = useState('');
42+
4143
// 카드 업데이트를 위한 stats 가져오기 메서드
4244
const fetchAndCalculate = async (
4345
uid: string,
@@ -166,12 +168,16 @@ const Page = () => {
166168
await fetchAndCalculate(user.uid);
167169

168170
// 페이지 조절
171+
const newFilteredLength = fetchedPlans.filter((plan) =>
172+
plan.title.toLowerCase().includes(searchQuery.toLowerCase())
173+
).length;
174+
169175
if (
170-
currentPage > Math.ceil((plans.length - 1) / itemsPerPage) &&
171-
Math.ceil((plans.length - 1) / itemsPerPage) > 0
176+
currentPage > Math.ceil(newFilteredLength / itemsPerPage) &&
177+
Math.ceil(newFilteredLength / itemsPerPage) > 0
172178
) {
173-
setCurrentPage(Math.ceil((plans.length - 1) / itemsPerPage));
174-
} else if (Math.ceil((plans.length - 1) / itemsPerPage) === 0) {
179+
setCurrentPage(Math.ceil(newFilteredLength / itemsPerPage));
180+
} else if (Math.ceil(newFilteredLength / itemsPerPage) === 0) {
175181
setCurrentPage(1);
176182
}
177183
} catch (err) {
@@ -202,19 +208,29 @@ const Page = () => {
202208
}
203209
};
204210

211+
// 검색 로직
212+
const filteredPlans = plans.filter((plan) =>
213+
plan.title.toLowerCase().includes(searchQuery.toLowerCase())
214+
);
215+
205216
// 페이지네이션 파트
206-
const totalPages = Math.ceil(plans.length / itemsPerPage);
217+
const totalPages = Math.ceil(filteredPlans.length / itemsPerPage);
207218
const startIndex = (currentPage - 1) * itemsPerPage;
208219
const endIndex = startIndex + itemsPerPage;
209-
const paginatedPlans = plans.slice(startIndex, endIndex);
220+
const paginatedPlans = filteredPlans.slice(startIndex, endIndex);
221+
222+
const handleSearch = (query: string) => {
223+
setSearchQuery(query);
224+
setCurrentPage(1); // 검색 결과가 바뀌면 첫 페이지로 리셋
225+
};
210226

211227
useEffect(() => {
212228
if (currentPage > totalPages && totalPages > 0) {
213229
setCurrentPage(totalPages);
214230
} else if (totalPages === 0 && currentPage !== 1) {
215231
setCurrentPage(1);
216232
}
217-
}, [plans.length, totalPages, currentPage]);
233+
}, [totalPages, currentPage]);
218234

219235
return (
220236
<div className="bg-background min-h-screen p-11">
@@ -277,7 +293,7 @@ const Page = () => {
277293
</div>
278294

279295
{/* 검색 바 */}
280-
<SearchBar />
296+
<SearchBar value={searchQuery} onChange={handleSearch} />
281297

282298
{/* 하단 추가 버튼 or 인라인 폼 */}
283299
<div className="mt-6 mb-4">

components/plans/PlanSection.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
PlanItem,
2121
addPlanItem,
2222
deletePlanItem,
23+
updatePlanItem,
2324
} from '@/services/plans/planManageService.service';
2425

2526
interface PlanSectionProps {
@@ -167,7 +168,7 @@ export default function PlanSection({
167168
onChangeStats();
168169
};
169170

170-
// 6. 플랜 수정 핸들러
171+
// 6. 플랜 수정 시작 핸들러
171172
const handleStartEdit = (e: React.MouseEvent) => {
172173
e.stopPropagation(); // 아코디언 토글 방지
173174

@@ -195,6 +196,27 @@ export default function PlanSection({
195196
setIsEditingPlan(false); // 수정 모드 종료
196197
};
197198

199+
// 8. 하위 항목 수정 핸들러
200+
const handleUpdatePlanItem = async (
201+
itemId: string,
202+
newText: string,
203+
newDeadline?: Date | null
204+
) => {
205+
try {
206+
// 1. 서비스 함수 호출 (작성해두신 메서드 사용)
207+
await updatePlanItem(userId, itemId, {
208+
text: newText,
209+
deadline: newDeadline, // Date 객체 또는 null(날짜 삭제 시)
210+
});
211+
212+
// 2. 목록 새로고침 & 통계 갱신
213+
const updatedTasks = await fetchPlanItems(userId, planId);
214+
setTasks(updatedTasks);
215+
} catch (error) {
216+
console.error(error);
217+
}
218+
};
219+
198220
// 완료된 할 일 개수 계산
199221
const completedCount = tasks.filter((t) => t.isChecked).length;
200222
const totalCount = tasks.length;
@@ -321,6 +343,7 @@ export default function PlanSection({
321343
isCompleted={task.isChecked}
322344
onToggle={handleToggleTask}
323345
onDelete={handleDeletePlanItem}
346+
onUpdate={handleUpdatePlanItem}
324347
/>
325348
))
326349
)}

components/plans/SearchBar.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { Search } from 'lucide-react';
22

3-
export default function SearchBar() {
3+
interface SearchBarProps {
4+
value: string;
5+
onChange: (value: string) => void;
6+
}
7+
8+
export default function SearchBar({ value, onChange }: SearchBarProps) {
49
return (
510
<div className="mb-4 flex justify-end">
611
<div className="relative">
712
<input
813
type="text"
9-
placeholder="검색어를 입력하세요"
14+
value={value}
15+
onChange={(e) => onChange(e.target.value)}
16+
placeholder="플랜 제목으로 검색..."
1017
className="w-64 rounded-full border border-gray-200 bg-white py-2 pr-10 pl-4 text-sm shadow-sm focus:border-purple-400 focus:outline-none"
1118
/>
1219
<Search className="absolute top-2.5 right-3 text-gray-400" size={18} />

components/plans/TaskItem.tsx

Lines changed: 147 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
'use client';
22

3-
import { Check, Calendar, Edit2, Trash2, MoreVertical } from 'lucide-react';
3+
import { Check, Calendar, Edit2, Trash2, MoreVertical, X } from 'lucide-react';
44
import { useEffect, useRef, useState } from 'react';
5+
import DatePicker from './DatePicker';
56

67
interface TaskItemProps {
78
id: string; // id (토글 식별용)
89
text: string;
910
isCompleted: boolean;
10-
deadline?: string | Date; // Date 타입 받을 수 있게 유연하게
11+
deadline?: Date; // Date 타입 받을 수 있게 유연하게
1112
onToggle: (id: string, currentStatus: boolean) => void; // 토글 핸들러
1213
onDelete: (itemId: string, text: string) => void;
14+
onUpdate: (
15+
itemId: string,
16+
newText: string,
17+
newDeadline?: Date | null
18+
) => void;
1319
}
1420

1521
export default function TaskItem({
@@ -19,9 +25,15 @@ export default function TaskItem({
1925
deadline,
2026
onToggle,
2127
onDelete,
28+
onUpdate,
2229
}: TaskItemProps) {
2330
const [showPlanItemMenu, setShowPlanItemMenu] = useState(false);
2431

32+
const [isEditing, setIsEditing] = useState(false);
33+
const [editText, setEditText] = useState(text);
34+
// deadline이 string으로 올 수도 있으니 안전하게 변환 (보통은 Date로 옴)
35+
const [editDeadline, setEditDeadline] = useState<Date | undefined>(undefined);
36+
2537
const menuRef = useRef<HTMLDivElement>(null);
2638

2739
// 날짜 포맷팅 (Date 객체나 문자열 모두 처리)
@@ -63,77 +75,147 @@ export default function TaskItem({
6375
setShowPlanItemMenu(false);
6476
};
6577

66-
return (
67-
<div
68-
className={`mb-2 flex items-center justify-between rounded-xl p-4 transition-colors ${
69-
isCompleted ? 'bg-gray-50' : 'bg-gray-100'
70-
}`}
71-
>
72-
<div className="flex w-full items-center gap-3">
73-
{/* 체크박스 (클릭 가능) */}
74-
<div
75-
onClick={() => onToggle(id, isCompleted)}
76-
className={`flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-md border transition-colors ${
77-
isCompleted
78-
? 'border-green-500 bg-green-500 text-white'
79-
: 'border-gray-300 bg-white hover:border-purple-400'
80-
}`}
81-
>
82-
{isCompleted && <Check size={16} strokeWidth={3} />}
78+
// 하위항목 수정 모드 시작
79+
const handleStartEdit = (e: React.MouseEvent) => {
80+
e.stopPropagation();
81+
82+
setEditText(text);
83+
setEditDeadline(deadline);
84+
85+
setIsEditing(true);
86+
setShowPlanItemMenu(false);
87+
};
88+
89+
// 하위 항목 수정 저장
90+
const handleSaveEdit = () => {
91+
if (!editText.trim()) return;
92+
// 변경된 내용 부모에게 전달 (날짜가 없으면 null)
93+
onUpdate(id, editText, editDeadline || null);
94+
setIsEditing(false);
95+
};
96+
97+
// 하위 항목 수정 취소
98+
const handleCancelEdit = () => {
99+
setIsEditing(false);
100+
};
101+
102+
if (isEditing) {
103+
return (
104+
<div className="animate-fadeIn mb-2 flex items-center gap-3 rounded-xl border-2 border-[#556BD6]/30 bg-white p-2 pl-4 shadow-sm">
105+
{/* 아이콘 (수정 모드임을 표시) */}
106+
<div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-md border border-gray-200 bg-gray-50">
107+
<Edit2 size={14} className="text-[#556BD6]" />
83108
</div>
84109

85-
{/* 텍스트 내용 */}
86-
<div className="flex flex-col">
87-
<span
88-
className={`text-sm font-medium ${
89-
isCompleted ? 'text-gray-400 line-through' : 'text-gray-700'
90-
}`}
110+
<div className="flex flex-1 flex-col gap-2">
111+
{/* 텍스트 입력 */}
112+
<input
113+
autoFocus
114+
type="text"
115+
value={editText}
116+
onChange={(e) => setEditText(e.target.value)}
117+
onKeyDown={(e) => {
118+
if (e.key === 'Enter' && !e.nativeEvent.isComposing)
119+
handleSaveEdit();
120+
if (e.key === 'Escape') handleCancelEdit();
121+
}}
122+
className="w-full bg-transparent text-sm font-medium text-gray-900 placeholder:text-gray-400 focus:outline-none"
123+
/>
124+
125+
{/* 날짜 선택 (DatePicker 재사용) */}
126+
<div className="flex items-center">
127+
<DatePicker date={editDeadline} setDate={setEditDeadline} />
128+
</div>
129+
</div>
130+
131+
{/* 저장/취소 버튼 */}
132+
<div className="flex items-center gap-1 pr-2">
133+
<button
134+
onClick={handleSaveEdit}
135+
className="rounded-lg p-2 text-[#556BD6] transition-colors hover:bg-[#556BD6]/10"
91136
>
92-
{text}
93-
</span>
94-
{deadline && (
95-
<span className="mt-0.5 flex items-center gap-1 text-xs text-gray-400">
96-
{/* 작은 달력 아이콘 추가 */}
97-
<Calendar size={10} />
98-
마감: {formatDate(deadline)}
99-
</span>
100-
)}
137+
<span className="text-xs font-bold">저장</span>
138+
</button>
139+
<button
140+
onClick={handleCancelEdit}
141+
className="rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
142+
>
143+
<X size={18} />
144+
</button>
101145
</div>
102146
</div>
147+
);
148+
} else {
149+
return (
103150
<div
104-
ref={menuRef}
105-
className={`relative ${showPlanItemMenu ? 'z-50' : 'z-10'}`}
151+
className={`mb-2 flex items-center justify-between rounded-xl p-4 transition-colors ${
152+
isCompleted ? 'bg-gray-50' : 'bg-gray-100'
153+
}`}
106154
>
107-
<button
108-
onClick={(e) => {
109-
e.stopPropagation(); // 부모 클릭(아코디언 토글) 방지
110-
setShowPlanItemMenu(!showPlanItemMenu);
111-
}}
112-
className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100"
113-
>
114-
<MoreVertical size={20} />
115-
</button>
116-
117-
{showPlanItemMenu && (
118-
<div className="absolute top-8 right-0 w-32 overflow-hidden rounded-lg border border-gray-100 bg-white py-1 shadow-lg">
119-
<button
120-
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50"
121-
onClick={(e) => {
122-
e.stopPropagation();
123-
alert('수정 기능');
124-
}}
125-
>
126-
<Edit2 size={14} /> 수정
127-
</button>
128-
<button
129-
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50"
130-
onClick={handleDeletePlanItem}
155+
<div className="flex w-full items-center gap-3">
156+
{/* 체크박스 */}
157+
<div
158+
onClick={() => onToggle(id, isCompleted)}
159+
className={`flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-md border transition-colors ${
160+
isCompleted
161+
? 'border-green-500 bg-green-500 text-white'
162+
: 'border-gray-300 bg-white hover:border-purple-400'
163+
}`}
164+
>
165+
{isCompleted && <Check size={16} strokeWidth={3} />}
166+
</div>
167+
168+
{/* 텍스트 및 날짜 표시 */}
169+
<div className="flex flex-col">
170+
<span
171+
className={`text-sm font-medium ${
172+
isCompleted ? 'text-gray-400 line-through' : 'text-gray-700'
173+
}`}
131174
>
132-
<Trash2 size={14} /> 삭제
133-
</button>
175+
{text}
176+
</span>
177+
{deadline && (
178+
<span className="mt-0.5 flex items-center gap-1 text-xs text-gray-400">
179+
<Calendar size={10} />
180+
마감: {formatDate(deadline)}
181+
</span>
182+
)}
134183
</div>
135-
)}
184+
</div>
185+
186+
{/* 메뉴 버튼 */}
187+
<div
188+
ref={menuRef}
189+
className={`relative ${showPlanItemMenu ? 'z-50' : 'z-10'}`}
190+
>
191+
<button
192+
onClick={(e) => {
193+
e.stopPropagation();
194+
setShowPlanItemMenu(!showPlanItemMenu);
195+
}}
196+
className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100"
197+
>
198+
<MoreVertical size={20} />
199+
</button>
200+
201+
{showPlanItemMenu && (
202+
<div className="absolute top-8 right-0 w-32 overflow-hidden rounded-lg border border-gray-100 bg-white py-1 shadow-lg">
203+
<button
204+
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50"
205+
onClick={handleStartEdit} // ✅ 수정 시작
206+
>
207+
<Edit2 size={14} /> 수정
208+
</button>
209+
<button
210+
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50"
211+
onClick={handleDeletePlanItem}
212+
>
213+
<Trash2 size={14} /> 삭제
214+
</button>
215+
</div>
216+
)}
217+
</div>
136218
</div>
137-
</div>
138-
);
219+
);
220+
}
139221
}

0 commit comments

Comments
 (0)