diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index c5dc28c..f372ef7 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -9,7 +9,6 @@ import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { ArrowRight, - BarChart3, Calendar, Plus, Target, @@ -27,6 +26,7 @@ import DashboardReflectionWidget from "@/components/reflections/DashboardReflect import DashboardBudgetWidget from "@/components/budget/DashboardBudgetWidget"; import Modal from "@/components/common/Modal"; import QuestionForm from "@/components/questions/QuestionForm"; +import { getDaysLeft, getGoalProgress } from "@/utils/goals/goalsHelpers"; const DashboardPage = () => { const { user, profile, isLoading: isAuthLoading } = useAuth(); @@ -76,14 +76,15 @@ const DashboardPage = () => { // 곧 마감되는 목표들 (7일 이내) const soonDueGoals = activeGoals.filter((goal) => { if (!goal.target_date) return false; - const daysLeft = goal.progress.daysLeft; - return daysLeft <= 7 && daysLeft > 0; + const daysLeft = getDaysLeft(goal.target_date); + return daysLeft !== null && daysLeft <= 7 && daysLeft > 0; }); // 달성률이 80% 이상인 목표들 - const nearCompletionGoals = activeGoals.filter( - (goal) => goal.progress.overallProgress >= 80, - ); + const nearCompletionGoals = activeGoals.filter((goal) => { + const progress = getGoalProgress(goal); + return progress.overallProgress >= 80; + }); return { total: totalGoals, @@ -156,6 +157,17 @@ const DashboardPage = () => { // 상위 목표들 (진행률 기준) const topProgressGoals = useMemo(() => { return activeGoals + .map((goal) => ({ + ...goal, + progress: { + ...getGoalProgress(goal), + daysLeft: getDaysLeft(goal.target_date), + progressText: `진행률 ${getGoalProgress(goal).overallProgress}%`, + isComplete: + goal.status === "completed" || + getGoalProgress(goal).overallProgress >= 100, + }, + })) .sort((a, b) => b.progress.overallProgress - a.progress.overallProgress) .slice(0, 3); }, [activeGoals]); @@ -302,7 +314,7 @@ const DashboardPage = () => { {/* 이번 주 활동 요약 */} -
+ {/*

@@ -327,7 +339,7 @@ const DashboardPage = () => {

-
+
*/} {/* 위젯 그리드 */} diff --git a/src/app/goals/[id]/edit/page.tsx b/src/app/goals/[id]/edit/page.tsx new file mode 100644 index 0000000..0670b1a --- /dev/null +++ b/src/app/goals/[id]/edit/page.tsx @@ -0,0 +1,495 @@ +"use client"; + +import { useAuth } from "@/hooks/auth"; +import { useGoals } from "@/hooks/goals/useGoals"; +import { GoalFormData } from "@/types/goals"; +import { + getChallengeMode, + getStatusText, + getTypeText, +} from "@/utils/goals/goalsHelpers"; +import { format } from "date-fns"; +import { + BarChart3, + Calendar, + Edit3, + HandCoins, + Hash, + Save, + Target, + X, +} from "lucide-react"; +import { useParams, useRouter } from "next/navigation"; +import { FormEvent, useEffect, useState } from "react"; + +interface GoalEditFormData { + title: string; + description: string; + reason: string; + target_amount: string; + target_count: string; + target_date: string; + status: "active" | "completed" | "paused" | "cancelled"; + challenge_mode: "amount" | "count" | "both"; +} + +const GoalEditPage = () => { + const { user } = useAuth(); + const { + goals, + isLoading: isGoalsLoading, + updateGoal, + isUpdatingGoal, + } = useGoals({ userId: user?.id }); + const params = useParams(); + const router = useRouter(); + + const goalId = params.id as string; + const goal = goals.find((g) => g.id === goalId); + + const [formData, setFormData] = useState({ + title: "", + description: "", + reason: "", + target_amount: "", + target_count: "", + target_date: "", + status: "active", + challenge_mode: "amount", + }); + + const [errors, setErrors] = useState>({}); + + // 목표 데이터로 폼 초기화 + useEffect(() => { + if (goal) { + setFormData({ + title: goal.title || "", + description: goal.description || "", + reason: goal.reason || "", + target_amount: goal.target_amount ? goal.target_amount.toString() : "", + target_count: goal.target_count ? goal.target_count.toString() : "", + target_date: goal.target_date + ? format(new Date(goal.target_date), "yyyy-MM-dd") + : "", + status: goal.status, + challenge_mode: goal.challenge_mode, + }); + } + }, [goal]); + + const handleInputChange = (field: keyof GoalEditFormData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + // 에러 클리어 + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: undefined })); + } + }; + + const validateForm = (): boolean => { + const newErrors: Partial = {}; + + if (!formData.title.trim()) { + newErrors.title = "목표 제목은 필수 입니다."; + } + + if ( + formData.challenge_mode === "amount" || + formData.challenge_mode === "both" + ) { + if (!formData.target_amount || Number(formData.target_amount) <= 0) { + newErrors.target_amount = "목표 금액을 올바르게 입력해주세요."; + } + } + + if ( + formData.challenge_mode === "count" || + formData.challenge_mode === "both" + ) { + if (!formData.target_count || Number(formData.target_count) <= 0) { + newErrors.target_count = "목표 횟수를 올바르게 입력해주세요."; + } + } + + if (!formData.target_date) { + newErrors.target_date = "목표 날짜는 필수입니다."; + } else { + const targetDate = new Date(formData.target_date); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (targetDate < today) { + newErrors.target_date = "목표 날짜는 오늘 이후여야 합니다."; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + const updateData: Partial = { + title: formData.title.trim(), + description: formData.description.trim() || null, + reason: formData.reason.trim() || null, + target_date: formData.target_date || null, + status: formData.status, + challenge_mode: formData.challenge_mode, + }; + + // 챌린지 모드에 따라 목표값 설정 + if (formData.challenge_mode === "amount") { + updateData.target_amount = Number(formData.target_amount); + updateData.target_count = null; + } else if (formData.challenge_mode === "count") { + updateData.target_count = Number(formData.target_count); + updateData.target_amount = null; + } else { + updateData.target_amount = Number(formData.target_amount); + updateData.target_count = Number(formData.target_count); + } + + try { + updateGoal({ id: goalId, updates: updateData }); + alert("목표가 성공적으로 수정되었습니다."); + router.push(`/goals/${goalId}`); + } catch (error) { + console.error("목표 수정 실패:", error); + alert("목표 수정에 실패했습니다."); + } + }; + + // 로딩 상태 + if (isGoalsLoading) { + return ( +
+
+
+
+
+
+

+ 목표를 불러오는 중 +

+

잠시만 기다려주세요...

+
+
+ ); + } + + // 사용자 인증 확인 + if (!user?.id) { + return ( +
+
+
+ +
+

목표 수정

+

로그인이 필요한 서비스입니다.

+
+
+ ); + } + + // 목표를 찾을 수 없는 경우 + if (!goal) { + return ( +
+
+
+ +
+

+ 목표를 찾을 수 없습니다 +

+

+ 수정하려는 목표가 존재하지 않거나 삭제되었습니다. +

+ +
+
+ ); + } + + return ( +
+
+ {/* 메인 폼 */} +
+ {/* 헤더 섹션 */} +
+
+ +

목표 수정

+
+
+ {goal.category && ( + <> + {goal.category.name} + + + )} + {getTypeText(goal.type)} + + {getStatusText(goal.status)} +
+
+ +
+ {/* 기본 정보 섹션 */} +
+

+ + 기본 정보 +

+ +
+ {/* 목표 제목 */} +
+ + handleInputChange("title", e.target.value)} + className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-500 transition-colors hover:border-accent-300 ${ + errors.title ? "border-red-500" : "border-gray-400" + }`} + placeholder="목표 제목을 작성하세요." + /> + {errors.title && ( +

{errors.title}

+ )} +
+ + {/* 목표 설명 */} +
+ +