Skip to content

Commit bb2733f

Browse files
committed
feat: 하위 항목 수정 기능 구현
1 parent 9bd792f commit bb2733f

2 files changed

Lines changed: 171 additions & 66 deletions

File tree

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/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)