Skip to content

Commit 2868abd

Browse files
Merge pull request #44 from DeveloperBlog-Devflow/feature/plan-management-page
feat: 계획 관리 페이지 플랜 생성 & 삭제, 하위항목 추가 & 완료 기능 구현
2 parents 8a9c599 + d517d34 commit 2868abd

7 files changed

Lines changed: 681 additions & 86 deletions

File tree

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

Lines changed: 115 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,112 @@
1-
import FormField from '@/components/auth/FormField';
1+
'use client';
2+
3+
import { useState, useEffect } from 'react';
4+
import { User, onAuthStateChanged } from 'firebase/auth';
5+
import { auth } from '@/lib/firebase';
6+
import { addPlan, deletePlan, fetchPlans, Plan } from '@/lib/planManageService';
7+
28
import PageHeader from '@/components/common/PageHeader';
39
import Card from '@/components/home/Card';
410
import AddPlanButton from '@/components/plans/AddPlanButton';
511
import PlanSection from '@/components/plans/PlanSection';
612
import SearchBar from '@/components/plans/SearchBar';
7-
13+
import InlineAddPlanForm from '@/components/plans/InlineAddPlanForm';
814
import { Target, Calendar, CheckCircle2 } from 'lucide-react';
915

10-
const sampleTasks = [
11-
{
12-
id: 1,
13-
text: 'useState, useEffect 기초',
14-
date: '2025-01-20',
15-
isChecked: true,
16-
},
17-
{
18-
id: 2,
19-
text: 'useContext, useReducer',
20-
date: '2025-01-22',
21-
isChecked: false,
22-
},
23-
{
24-
id: 3,
25-
text: 'Custom Hooks 만들기',
26-
date: '2025-01-22',
27-
isChecked: false,
28-
},
29-
];
30-
3116
const Page = () => {
17+
const [user, setUser] = useState<User | null>(null);
18+
const [plans, setPlans] = useState<Plan[]>([]);
19+
const [isAdding, setIsAdding] = useState(false);
20+
const [isLoading, setIsLoading] = useState(true);
21+
22+
// 사용자 인증 상태 리스너 및 초기 플랜 목록 로드
23+
useEffect(() => {
24+
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
25+
setUser(currentUser);
26+
27+
if (currentUser) {
28+
try {
29+
const fetchedPlans = await fetchPlans(currentUser.uid);
30+
setPlans(fetchedPlans);
31+
} catch (err) {
32+
console.error('플랜 목록 로딩 실패:', err);
33+
setPlans([]);
34+
}
35+
} else {
36+
setPlans([]);
37+
}
38+
setIsLoading(false);
39+
});
40+
return () => unsubscribe(); // 클린업
41+
}, []);
42+
43+
// 플랜 생성(추가) 핸들러
44+
const handleSavePlan = async (title: string, description: string) => {
45+
if (!user) {
46+
alert('로그인이 필요합니다.');
47+
return;
48+
}
49+
try {
50+
await addPlan(user.uid, title, description);
51+
52+
// 목록 새로고침
53+
const fetchedPlans = await fetchPlans(user.uid);
54+
setPlans(fetchedPlans);
55+
56+
setIsAdding(false); // 폼 닫기
57+
} catch (err) {
58+
console.error('플랜 추가 실패:', err);
59+
}
60+
};
61+
62+
// 플랜 추가 취소 핸들러
63+
const handleCancelAdd = () => {
64+
setIsAdding(false);
65+
};
66+
67+
// 플랜 삭제 핸들러
68+
const handleDeletePlan = async (planId: string, title: string) => {
69+
if (!user) {
70+
alert('로그인이 필요합니다.');
71+
return;
72+
}
73+
74+
if (
75+
confirm(
76+
`'${title}' 플랜을 정말 삭제하시겠습니까? 포함된 모든 할 일이 삭제됩니다.`
77+
)
78+
) {
79+
try {
80+
await deletePlan(user.uid, planId);
81+
82+
// 목록 새로고침
83+
const fetchedPlans = await fetchPlans(user.uid);
84+
setPlans(fetchedPlans);
85+
} catch (err) {
86+
console.error(err);
87+
}
88+
}
89+
};
90+
3291
return (
3392
<div className="bg-background min-h-screen p-11">
34-
{/* 1. 페이지 헤더 */}
3593
<PageHeader
3694
title="플랜"
3795
highlight="관리하기"
3896
description="학습 주제를 만들고 세부 과제를 관리하세요"
3997
/>
4098

41-
{/* 2. 상단 통계 카드 (Grid) */}
99+
{/* 상단 통계 카드 (Grid) */}
42100
<div className="mb-4 grid grid-cols-1 gap-3.5 md:grid-cols-3">
43101
<Card className="flex items-center justify-between border-2 border-[#D5DCFB]">
44102
{/* 왼쪽: 텍스트 영역 */}
45103
<div className="flex flex-col gap-1">
46104
<span className="text-text-sub text-sm font-medium">
47105
전체 플랜 수
48106
</span>
49-
<span className="text-4xl font-bold text-[#4757D3]">1</span>
107+
<span className="text-4xl font-bold text-[#4757D3]">
108+
{plans.length}
109+
</span>
50110
</div>
51111

52112
{/* 오른쪽: 아이콘 영역 */}
@@ -84,23 +144,41 @@ const Page = () => {
84144
</Card>
85145
</div>
86146

87-
{/* 3. 검색 바 */}
147+
{/* 검색 바 */}
88148
<SearchBar />
89149

90-
{/* 4. 메인 플랜 목록 */}
150+
{/* 메인 플랜 목록 */}
91151
<section className="space-y-6">
92-
<PlanSection
93-
title="React Hooks 학습"
94-
description="React Hooks의 기본부터 고급 패턴까지 학습"
95-
tasks={sampleTasks}
96-
/>
97-
98-
{/* 추가적인 PlanSection이 있다면 여기에 배치 */}
152+
{isLoading ? (
153+
<p>플랜을 불러오는 중...</p>
154+
) : plans.length > 0 && user ? (
155+
plans.map((plan) => (
156+
<PlanSection
157+
key={plan.id}
158+
userId={user.uid}
159+
planId={plan.id}
160+
title={plan.title}
161+
description={plan.description}
162+
onDelete={handleDeletePlan}
163+
/>
164+
))
165+
) : (
166+
<p>아직 생성된 플랜이 없습니다. 첫 플랜을 추가해보세요!</p>
167+
)}
99168
</section>
100169

101-
{/* 5. 하단 추가 버튼 */}
170+
{/* 하단 추가 버튼 or 인라인 폼 */}
102171
<div className="mt-6">
103-
<AddPlanButton />
172+
{isAdding ? (
173+
<InlineAddPlanForm
174+
onSave={handleSavePlan}
175+
onCancel={handleCancelAdd}
176+
/>
177+
) : (
178+
<div onClick={() => setIsAdding(true)}>
179+
<AddPlanButton />
180+
</div>
181+
)}
104182
</div>
105183
</div>
106184
);

app/globals.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
--color-border-focus: #7a57ff;
2626
/* 호버 시 */
2727
--color-border-hover: #7a57ff;
28+
/* 선택 시 (파란색) */
29+
--color-border-focus-blue: #4757d3;
2830

2931
/* --- 카드 배경 surface --- */
3032
/* 기본 카드 배경 */
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
5+
interface InlineAddPlanFormProps {
6+
onSave: (title: string, description: string) => void;
7+
onCancel: () => void;
8+
}
9+
10+
const InlineAddPlanForm = ({ onSave, onCancel }: InlineAddPlanFormProps) => {
11+
const [title, setTitle] = useState('');
12+
const [description, setDescription] = useState('');
13+
14+
const handleSave = () => {
15+
if (!title.trim()) {
16+
alert('플랜 제목을 입력해주세요.');
17+
return;
18+
}
19+
onSave(title, description);
20+
};
21+
22+
return (
23+
<div className="bg-surface w-full flex-col items-center justify-center gap-4 rounded-2xl border-2 border-gray-300 p-6 transition-all">
24+
<div className="flex w-full flex-col gap-3">
25+
<input
26+
type="text"
27+
value={title}
28+
onChange={(e) => setTitle(e.target.value)}
29+
placeholder="플랜 제목 (예: React Hooks 마스터하기)"
30+
className="border-border focus:border-border-focus-blue w-full rounded-lg border-2 p-3 text-base font-bold placeholder-gray-400 focus:outline-none"
31+
/>
32+
<textarea
33+
value={description}
34+
onChange={(e) => setDescription(e.target.value)}
35+
placeholder="소제목 또는 간단한 설명 (선택 사항)"
36+
className="focus:border-border-focus-blue border-border w-full rounded-lg border-2 p-3 text-sm placeholder-gray-400 focus:outline-none"
37+
rows={2}
38+
/>
39+
</div>
40+
<div className="mt-4 flex justify-end gap-2">
41+
<button
42+
onClick={onCancel}
43+
className="rounded-lg px-4 py-2 font-medium text-gray-600 transition-colors hover:bg-gray-200"
44+
>
45+
취소
46+
</button>
47+
<button
48+
onClick={handleSave}
49+
className="rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white transition-colors hover:bg-blue-700"
50+
>
51+
플랜 저장
52+
</button>
53+
</div>
54+
</div>
55+
);
56+
};
57+
58+
export default InlineAddPlanForm;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { Calendar, Plus, X } from 'lucide-react';
5+
6+
interface InlineAddTaskFormProps {
7+
onSave: (text: string, date?: Date) => Promise<void>; // 저장 핸들러
8+
onCancel: () => void; // 취소 핸들러
9+
}
10+
11+
export default function InlineAddTaskForm({
12+
onSave,
13+
onCancel,
14+
}: InlineAddTaskFormProps) {
15+
const [text, setText] = useState('');
16+
const [isSubmitting, setIsSubmitting] = useState(false);
17+
18+
// 날짜 라이브러리를 붙일 예정이므로 지금은 임시 상태
19+
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
20+
21+
const handleSubmit = async (e?: React.FormEvent) => {
22+
e?.preventDefault();
23+
if (!text.trim() || isSubmitting) return;
24+
25+
setIsSubmitting(true);
26+
27+
try {
28+
await onSave(text, selectedDate);
29+
setText(''); // 저장 후 초기화
30+
onCancel(); // 저장 후 닫기
31+
} catch (error) {
32+
console.error(error);
33+
} finally {
34+
setIsSubmitting(false);
35+
}
36+
};
37+
38+
// 엔터키 처리
39+
const handleKeyDown = (e: React.KeyboardEvent) => {
40+
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
41+
handleSubmit();
42+
} else if (e.key === 'Escape') {
43+
onCancel();
44+
}
45+
};
46+
47+
return (
48+
<div className="border-border-focus-blue mb-2 flex items-center gap-3 rounded-xl border-2 bg-white p-2 pl-4 shadow-sm">
49+
{/* 1. 체크박스 자리 (비활성 모양) */}
50+
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-gray-200 bg-gray-50">
51+
<Plus size={14} className="text-gray-400" />
52+
</div>
53+
54+
{/* 2. 입력 필드 (투명 배경) */}
55+
<div className="flex flex-1 flex-col gap-1">
56+
<input
57+
autoFocus
58+
type="text"
59+
value={text}
60+
onChange={(e) => setText(e.target.value)}
61+
onKeyDown={handleKeyDown}
62+
placeholder="할 일을 입력하고 Enter를 누르세요"
63+
className="w-full bg-transparent text-sm font-medium text-gray-900 placeholder:text-gray-400 focus:outline-none"
64+
disabled={isSubmitting}
65+
/>
66+
67+
{/* 3. 마감일 버튼 (요청하신 부분) */}
68+
<div className="flex items-center">
69+
<button
70+
type="button"
71+
className="flex items-center gap-1.5 rounded-md bg-gray-100 px-2 py-1 text-xs text-gray-600 transition-colors hover:bg-gray-200"
72+
onClick={() => alert('나중에 달력 라이브러리 연결될 곳!')}
73+
>
74+
<Calendar size={12} />
75+
{selectedDate ? selectedDate.toLocaleDateString() : '마감일 설정'}
76+
</button>
77+
</div>
78+
</div>
79+
80+
{/* 4. 우측 저장/취소 버튼 그룹 */}
81+
<div className="flex items-center gap-1 pr-2">
82+
<button
83+
onClick={() => handleSubmit()}
84+
disabled={!text.trim()}
85+
className="text-border-focus-blue rounded-lg p-2 hover:bg-purple-50 disabled:opacity-50 disabled:hover:bg-transparent"
86+
>
87+
<span className="text-xs font-bold">추가</span>
88+
</button>
89+
<button
90+
onClick={onCancel}
91+
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
92+
>
93+
<X size={18} />
94+
</button>
95+
</div>
96+
</div>
97+
);
98+
}

0 commit comments

Comments
 (0)