diff --git a/package.json b/package.json
index 71381bc..cb4e4e8 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"react-dom": "^19.0.0",
"react-mobile-picker": "^1.1.2",
"react-qr-code": "^2.0.15",
+ "swiper": "^11.2.8",
"yup": "^1.6.1",
"zustand": "^5.0.3"
},
@@ -75,5 +76,6 @@
"extends": [
"plugin:storybook/recommended"
]
- }
+ },
+ "packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
}
diff --git a/src/app/api/goal/index.ts b/src/app/api/goal/index.ts
index acbf1d2..5682915 100644
--- a/src/app/api/goal/index.ts
+++ b/src/app/api/goal/index.ts
@@ -1,5 +1,17 @@
import { authAPI } from "@/app/api/config"
-export const getGoalInfo = (id: number) => {
- return authAPI.get(`/goal/2025-01-25`)
+export const getGoalMainInfo = (date: string) => {
+ return authAPI.get(`/api/goal?date=${date}`)
+}
+
+export const createGoal = (request) => {
+ return authAPI.post(`/api/goal`, request)
+}
+
+export const getGoalDetailInfo = (date: string) => {
+ return authAPI.get(`/api/goal?date=${date}`)
+}
+
+export const postCertifyGoal = (goalId: number, request) => {
+ return authAPI.post(`/api/goal/${goalId}/certify`, request)
}
diff --git a/src/app/api/letter/index.ts b/src/app/api/letter/index.ts
new file mode 100644
index 0000000..a3cb6bb
--- /dev/null
+++ b/src/app/api/letter/index.ts
@@ -0,0 +1,9 @@
+import { authAPI } from "@/app/api/config"
+
+export const getGoalLetter = (weeklyGoalId: number) => {
+ return authAPI.get(`/api/letter/${weeklyGoalId}`)
+}
+
+export const createGoalLetter = (weeklyGoalId: number, request: string) => {
+ return authAPI.post(`/api/letter/${weeklyGoalId}`, request)
+}
diff --git a/src/app/goal/_components/buttonwrapper.module.scss b/src/app/goal/_components/buttonwrapper.module.scss
index af40d66..f94e5e6 100644
--- a/src/app/goal/_components/buttonwrapper.module.scss
+++ b/src/app/goal/_components/buttonwrapper.module.scss
@@ -1,9 +1,9 @@
.goal__btn {
- padding: 0 30px;
- background: $white;
+ background: #fff;
position: fixed;
- bottom: 0;
- left: 0;
width: 100%;
- z-index: 100;
+ left: 0;
+ bottom: 0;
+ padding: 16px 32px;
+ border-top: 1px solid #D8DADB;
}
diff --git a/src/app/goal/_components/buttonwrapper.tsx b/src/app/goal/_components/buttonwrapper.tsx
index d1ceeee..6757667 100644
--- a/src/app/goal/_components/buttonwrapper.tsx
+++ b/src/app/goal/_components/buttonwrapper.tsx
@@ -1,20 +1,39 @@
"use client"
-import styles from "./buttonwrapper.module.scss";
-import Button from "@/components/common/button/button";
+import styles from "./buttonwrapper.module.scss"
+import Button from "@/components/common/button/button"
+import { useRouter } from "next/navigation"
+import dayjs from "dayjs"
-const ButtonWrapper = () => {
+type Props = {
+ step: 1 | 2 | 3
+ onNext: () => void
+ onMutate: () => void
+}
+
+const ButtonWrapper = ({ step, onNext, onMutate }: Props) => {
+ const router = useRouter()
+ const today = dayjs().format("YYYY-MM-DD")
+ const handleClick = () => {
+ if (step < 2) {
+ onNext()
+ } else if (step === 2) {
+ onMutate()
+ onNext()
+ } else if (step === 3) {
+ router.push(`/goal/detail?date=${today}`)
+ }
+ }
return (
)
}
-export default ButtonWrapper;
\ No newline at end of file
+export default ButtonWrapper
diff --git a/src/app/goal/_components/complete.tsx b/src/app/goal/_components/complete.tsx
index 53c6995..de78f73 100644
--- a/src/app/goal/_components/complete.tsx
+++ b/src/app/goal/_components/complete.tsx
@@ -3,7 +3,7 @@ import Link from "next/link"
import styles from "./complete.module.scss"
import GoalResult from "@/app/goal/_components/goalresult"
-const Complete = () => {
+const Complete = ({ data }) => {
return (
@@ -12,11 +12,9 @@ const Complete = () => {
목표가
설정되었어요!
-
2020년 5월 1일
-
목표 보드로 이동하기
-
-
-
+
+ {data.startDate} ~ {data.endDate}
+
)
diff --git a/src/app/goal/_components/goalresult.module.scss b/src/app/goal/_components/goalresult.module.scss
index 133adf3..d4f12cb 100644
--- a/src/app/goal/_components/goalresult.module.scss
+++ b/src/app/goal/_components/goalresult.module.scss
@@ -1,22 +1,24 @@
.goal__result {
- width: 160px;
- height: 163px;
- border: 5px solid #F2F2F5;
- border-radius: 50%;
+ padding-right: 16px;
+
p {
- margin-top: 20px;
+ padding-top: 20px;
text-align: center;
font-weight: 700;
+
span {
color: #8D8D8D;
font-weight: 400;
}
}
}
+
.goal__icon {
width: 100%;
- height: 100%;
+ aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
+ border: 5px solid #F2F2F5;
+ border-radius: 50%;
}
\ No newline at end of file
diff --git a/src/app/goal/_components/goalresult.tsx b/src/app/goal/_components/goalresult.tsx
index 4439a7f..a65f733 100644
--- a/src/app/goal/_components/goalresult.tsx
+++ b/src/app/goal/_components/goalresult.tsx
@@ -1,13 +1,16 @@
import styles from "./goalresult.module.scss"
-import DefaultIcon from "@/assets/icons/goals/icon-default.svg";
-const GoalResult = () => {
+
+const GoalResult = ({ data }) => {
+ const Icon = data?.icon
return (
-
+
-
01 하루 물 6컵
+
+ {String(data?.number || 1).padStart(2, "0")} {data?.name || "목표 추가"}
+
)
}
-export default GoalResult
\ No newline at end of file
+export default GoalResult
diff --git a/src/app/goal/_components/step1.module.scss b/src/app/goal/_components/step1.module.scss
index f208b00..0f56179 100644
--- a/src/app/goal/_components/step1.module.scss
+++ b/src/app/goal/_components/step1.module.scss
@@ -10,6 +10,18 @@
padding-left: 31px;
}
}
+
+ &__icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ border: 3px solid #F2F2F5;
+ border-radius: 50%;
+ width: 64px;
+ height: 64px;
+ margin: 0 auto;
+ }
}
.goal__result {
@@ -46,17 +58,25 @@
&-control {
width: 100%;
- overflow-x: auto;
- display: flex;
margin-top: 24px;
- gap: 16px;
- padding: 14px;
+
+ > div {
+ text-align: center;
+ }
}
&-txt {
+ margin-top: 16px;
display: flex;
align-items: center;
+ justify-content: center;
gap: 5px;
font-size: 14px;
+
+ p, input {
+ white-space: pre-wrap;
+ text-align: left;
+ max-width: 80px;
+ }
}
}
diff --git a/src/app/goal/_components/step1.tsx b/src/app/goal/_components/step1.tsx
index f77c842..7276870 100644
--- a/src/app/goal/_components/step1.tsx
+++ b/src/app/goal/_components/step1.tsx
@@ -1,90 +1,150 @@
-"use client";
-import classNames from "classnames/bind";
-import styles from "./step1.module.scss";
-const cx = classNames.bind(styles);
+"use client"
+import classNames from "classnames/bind"
+import styles from "./step1.module.scss"
-import StepBox from "@/app/goal/_components/stepBox";
-import Goal1 from "@/assets/icons/sticker/compliment-sticker-1.svg"
-import AddButton from "@/components/common/addbutton/addbutton";
-import GoalResult from "@/app/goal/_components/goalresult";
+const cx = classNames.bind(styles)
+import { Swiper, SwiperSlide } from "swiper/react"
+import "swiper/css"
+import "swiper/css/navigation"
+import "swiper/css/pagination"
+import StepBox from "@/app/goal/_components/stepBox"
+import Goal1 from "@/assets/icons/goals/goal1.svg"
+import Goal2 from "@/assets/icons/goals/goal2.svg"
+import Goal3 from "@/assets/icons/goals/goal3.svg"
+import Goal4 from "@/assets/icons/goals/goal4.svg"
+import Goal5 from "@/assets/icons/goals/goal5.svg"
+import Goal6 from "@/assets/icons/goals/goal6.svg"
+import Goal7 from "@/assets/icons/goals/goal7.svg"
+import Goal8 from "@/assets/icons/goals/goal8.svg"
+import DefaultIcon from "@/assets/icons/goals/icon-default.svg"
+import AddButton from "@/components/common/addbutton/addbutton"
+import GoalResult from "@/app/goal/_components/goalresult"
+import { useEffect, useRef, useState } from "react"
+
+const Step1 = ({ data, setData }) => {
+ const swiperRef = useRef()
+ const initialGoalList = [
+ { id: 1, icon: Goal1, name: "하루 물 6컵" },
+ { id: 2, icon: Goal2, name: "야채 먹기" },
+ { id: 3, icon: Goal3, name: "삼시세끼 채소" },
+ { id: 4, icon: Goal4, name: "가족과 운동" },
+ { id: 5, icon: Goal5, name: "저녁 30분 운동" },
+ { id: 6, icon: Goal6, name: "패스트 푸드 끊기" },
+ { id: 7, icon: Goal7, name: "근력 운동" },
+ { id: 8, icon: Goal8, name: "간식 안 먹기" },
+ ]
+ const [goalList, setGoalList] = useState(initialGoalList)
+ const [selectedGoals, setSelectedGoals] = useState<
+ { icon: any; name: string; editable: boolean }[]
+ >([])
+ const handleClickGoal = (id: number, icon: any, label: string) => {
+ if (selectedGoals.length >= 10) return alert("최대 10개의 목표까지 추가할 수 있어요")
+
+ // 중복 방지
+ const alreadyExists = selectedGoals.some((g) => g.name === label)
+ if (alreadyExists) return alert("이미 추가된 목표예요")
+
+ setSelectedGoals((prev) => {
+ const updated = [...prev, { icon, name: label, editable: false }]
+ setTimeout(() => {
+ swiperRef.current?.slideTo(updated.length - 1)
+ }, 100)
+ return updated
+ })
+ }
+
+ const handleAddGoal = () => {
+ const newGoal = {
+ id: goalList.length + 1,
+ icon: DefaultIcon,
+ name: "목표입력",
+ editable: true,
+ }
+
+ setGoalList((prev) => [...prev, newGoal])
+ }
+
+ useEffect(() => {
+ setData(selectedGoals)
+ }, [selectedGoals, setData])
-const Step1 = () => {
return (
<>
-
+
최대 10개까지의 목표를 추가할 수 있어요
-
+ (swiperRef.current = swiper)}>
+ {selectedGoals.map((goal, idx) => (
+
+
+
+ ))}
+
+
+
+
그로우핏 추천 목표
-
직접 설정하기
+
직접 설정하기
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
-
+
+ {goalList.map((goal, index) => {
+ const Icon = goal.icon
+ const isSelected = selectedGoals.some((g) => g.name === goal.name)
+
+ return (
+
+
+
+
+
+
+ )
+ })}
+
>
- );
-};
+ )
+}
-export default Step1;
\ No newline at end of file
+export default Step1
diff --git a/src/app/goal/_components/step2.module.scss b/src/app/goal/_components/step2.module.scss
index ec50856..3e89fef 100644
--- a/src/app/goal/_components/step2.module.scss
+++ b/src/app/goal/_components/step2.module.scss
@@ -2,6 +2,7 @@
&__content {
padding: 0 16px;
}
+
&__title {
> p {
color: #8D8D8D;
@@ -13,6 +14,7 @@
.goal__period {
margin-top: 50px;
+
h2 {
font-size: 18px;
text-align: center;
@@ -22,21 +24,26 @@
.goal__times {
margin-top: 89px;
+
> h3 {
font-size: 16px;
font-weight: 600;
}
+
> div {
margin-top: 16px;
+
span {
font-size: 15px;
}
+
input {
border: 1px solid #D8DADB;
border-radius: 6px;
width: 107px;
height: 42px;
margin: 0 8px;
+ text-align: right;
}
}
}
\ No newline at end of file
diff --git a/src/app/goal/_components/step2.tsx b/src/app/goal/_components/step2.tsx
index b1a922e..28bd864 100644
--- a/src/app/goal/_components/step2.tsx
+++ b/src/app/goal/_components/step2.tsx
@@ -1,18 +1,33 @@
-"use client";
-import { useState } from "react";
-import classNames from "classnames/bind";
-import styles from "./step2.module.scss";
-import DefaultIcon from "@/assets/icons/goals/icon-default.svg"
-const cx = classNames.bind(styles);
-import BackHeader from "@/components/layout/header/BackHeader";
-import StepBox from "@/app/goal/_components/stepBox";
-import Goal1 from "@/assets/icons/sticker/compliment-sticker-1.svg"
-import AddButton from "@/components/common/addbutton/addbutton";
-import CustomCalendar from "@/components/common/calendar/customcalendar";
-import { DateRange } from "react-day-picker";
+"use client"
+import { useEffect, useState } from "react"
+import classNames from "classnames/bind"
+import styles from "./step2.module.scss"
-const Step2 = () => {
- const [selectedWeek, setSelectedWeek] = useState();
+const cx = classNames.bind(styles)
+import StepBox from "@/app/goal/_components/stepBox"
+import CustomCalendar from "@/components/common/calendar/customcalendar"
+import { DateRange } from "react-day-picker"
+import dayjs from "dayjs"
+
+const Step2 = ({ setData }) => {
+ const [selectedWeek, setSelectedWeek] = useState()
+ useEffect(() => {
+ if (selectedWeek?.from && selectedWeek?.to) {
+ setData((prev) => ({
+ ...prev,
+ startDate: dayjs(selectedWeek.from).format("YYYY-MM-DD"),
+ endDate: dayjs(selectedWeek.to).format("YYYY-MM-DD"),
+ }))
+ }
+ }, [selectedWeek, setData])
+
+ const handleCertCountChange = (e) => {
+ const value = Math.min(Number(e.target.value), 5)
+ setData((prev) => ({
+ ...prev,
+ certificationCount: value,
+ }))
+ }
return (
<>
@@ -26,24 +41,28 @@ const Step2 = () => {
>
- );
-};
+ )
+}
-export default Step2;
\ No newline at end of file
+export default Step2
diff --git a/src/app/goal/create/page.tsx b/src/app/goal/create/page.tsx
index 31c26fa..12a1a6c 100644
--- a/src/app/goal/create/page.tsx
+++ b/src/app/goal/create/page.tsx
@@ -1,19 +1,56 @@
-import styles from "./page.module.scss"
+"use client"
import Step1 from "@/app/goal/_components/step1"
import Step2 from "@/app/goal/_components/step2"
import ButtonWrapper from "@/app/goal/_components/buttonwrapper"
import Complete from "@/app/goal/_components/complete"
import BackHeader from "@/components/layout/header/BackHeader"
+import { useState } from "react"
+import { useGoalCreate } from "@/queries/goal/userGoalQuery"
const Page = () => {
+ const [step, setStep] = useState<1 | 2 | 3>(1) // 단계: 1 → 2 → 3
+ const [data, setData] = useState({
+ startDate: "",
+ endDate: "",
+ certificationCount: 0,
+ name: "",
+ iconId: 0,
+ icon: "",
+ })
+
+ const transformData = (rawData) => {
+ const goals = Object.entries(rawData)
+ .filter(([key]) => !["startDate", "endDate", "certificationCount"].includes(key))
+ .map(([_, value], index) => ({
+ name: value.name,
+ iconId: index + 1,
+ }))
+
+ return {
+ startDate: rawData.startDate,
+ endDate: rawData.endDate,
+ certificationCount: rawData.certificationCount,
+ goals,
+ }
+ }
+
+ const requestData = transformData(data)
+
+ const handleNext = () => {
+ setStep((prev) => (prev < 3 ? ((prev + 1) as 1 | 2 | 3) : prev))
+ }
+ const { mutate } = useGoalCreate(requestData)
+
return (
- {/**/}
- {/**/}
-
-
+
+ {step === 1 && }
+ {step === 2 && }
+ {step === 3 && }
+
+
)
}
diff --git a/src/app/goal/detail/@modal/goal-modal/goalmodal.module.scss b/src/app/goal/detail/@modal/goal-modal/goalmodal.module.scss
index acf0cd7..c9d3f27 100644
--- a/src/app/goal/detail/@modal/goal-modal/goalmodal.module.scss
+++ b/src/app/goal/detail/@modal/goal-modal/goalmodal.module.scss
@@ -1,24 +1,42 @@
+.calendar {
+ > div {
+ padding: 0 10px;
+
+ > div:first-of-type {
+ display: none;
+ }
+ }
+}
+
.upload {
padding-top: 20px;
border-top: 1px solid #D9D9D9;
}
+
.uploadtitle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30px;
+
h3 {
font-size: 20px;
font-weight: 700;
}
+
p {
color: $gray-80;
font-size: 18px;
}
}
+.smallcalendar_calendar-header {
+ //display: none;
+}
+
.uploadimage {
padding: 20px 30px;
+
span {
display: flex;
align-items: center;
@@ -29,6 +47,7 @@
color: #727793;
border-radius: 6px;
}
+
label {
display: inline-block;
width: 240px;
@@ -36,6 +55,7 @@
background-size: cover;
border-radius: 6px;
}
+
input {
display: none;
}
diff --git a/src/app/goal/detail/@modal/goal-modal/page.tsx b/src/app/goal/detail/@modal/goal-modal/page.tsx
index fd00333..9d0a17f 100644
--- a/src/app/goal/detail/@modal/goal-modal/page.tsx
+++ b/src/app/goal/detail/@modal/goal-modal/page.tsx
@@ -1,63 +1,78 @@
"use client"
-import BottomSheet from "@/components/common/buttomsheet/bottomsheet";
-import { useRouter } from "next/navigation";
-import SmallCalendar from "@/components/common/smallcalendar/smallcalendar";
+import BottomSheet from "@/components/common/buttomsheet/bottomsheet"
+import { useRouter, useSearchParams } from "next/navigation"
+import SmallCalendar from "@/components/common/smallcalendar/smallcalendar"
import styles from "./goalmodal.module.scss"
-import React, {useState} from "react";
-import Button from "@/components/common/button/button";
+import React, { useState } from "react"
+import Button from "@/components/common/button/button"
+import dayjs from "dayjs"
+import { useGoalCertifyQuery } from "@/queries/goal/userGoalQuery"
const GoalModal = () => {
- const router = useRouter();
- const [imagePreview, setImagePreview] = useState(null);
+ const router = useRouter()
+ const today = dayjs().format("YYYY-MM-DD")
+ const searchParams = useSearchParams()
+ const goalId = searchParams.get("goalId")
+ const [clickedDate, setClickedDate] = useState(today)
+ const [imagePreview, setImagePreview] = useState(null)
+ const { mutate } = useGoalCertifyQuery(goalId)
const handleImageChange = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file) return;
+ const file = e.target.files?.[0]
+ if (!file) return
- const reader = new FileReader();
+ const reader = new FileReader()
reader.onloadend = () => {
- setImagePreview(reader.result as string);
- };
- reader.readAsDataURL(file);
- };
+ setImagePreview(reader.result as string)
+ }
+ reader.readAsDataURL(file)
+ }
const closeModal = () => {
- router.back();
- };
+ router.back()
+ }
+
+ const onSubmit = () => {
+ if (!imagePreview) {
+ alert("이미지를 업로드해주세요.")
+ return
+ }
+ const formData = new FormData()
+ formData.append("image", imagePreview)
+ mutate(formData)
+ }
return (
-
+
+
+
-
)
}
-export default GoalModal;
\ No newline at end of file
+export default GoalModal
diff --git a/src/app/goal/detail/@modal/letter-modal/lettermodal.module.scss b/src/app/goal/detail/@modal/letter-modal/lettermodal.module.scss
index 226f680..4653379 100644
--- a/src/app/goal/detail/@modal/letter-modal/lettermodal.module.scss
+++ b/src/app/goal/detail/@modal/letter-modal/lettermodal.module.scss
@@ -45,16 +45,6 @@
}
}
-.bottom {
- margin-top: 50px;
- padding: 0 0 0 30px;
-
- h2 {
- font-size: 20px;
- font-weight: 700;
- }
-}
-
.goal-box {
width: 100%;
display: flex;
diff --git a/src/app/goal/detail/@modal/letter-modal/page.tsx b/src/app/goal/detail/@modal/letter-modal/page.tsx
index bcf4d75..144307f 100644
--- a/src/app/goal/detail/@modal/letter-modal/page.tsx
+++ b/src/app/goal/detail/@modal/letter-modal/page.tsx
@@ -1,52 +1,45 @@
"use client"
-import React from "react";
-import { useRouter } from "next/navigation";
+import React, { useState } from "react"
+import { useRouter, useSearchParams } from "next/navigation"
import styles from "./lettermodal.module.scss"
-import BottomSheet from "@/components/common/buttomsheet/bottomsheet";
+import BottomSheet from "@/components/common/buttomsheet/bottomsheet"
import ProfileIcon from "@/assets/character/profile-default.svg"
-import Button from "@/components/common/button/button";
+import Button from "@/components/common/button/button"
+import { useGetGoalLetter } from "@/queries/letter/useLetterQuery"
+
const LetterModal = () => {
- const router = useRouter();
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const weeklyGoalId = searchParams.get("weeklyGoalId")
+ const { data } = useGetGoalLetter(Number(weeklyGoalId))
const closeModal = () => {
- router.back();
- };
+ router.back()
+ }
return (
-
+
To. 미니준
-
블라블라블라
+
블라블라블라블라블라
From. 엄마가
-
-
1월 24일 ~ 1월 31일 목표
-
- -
-
20분동안걷기
-
-
- -
-
20분동안걷기
-
-
- -
-
20분동안걷기
-
-
-
-
- {
- }}/>
+ {}}
+ />
)
}
-export default LetterModal;
\ No newline at end of file
+export default LetterModal
diff --git a/src/app/goal/detail/page.tsx b/src/app/goal/detail/page.tsx
index de4900b..9607eca 100644
--- a/src/app/goal/detail/page.tsx
+++ b/src/app/goal/detail/page.tsx
@@ -1,26 +1,49 @@
"use client"
+import { useEffect, useState } from "react"
+import { useRouter, useSearchParams } from "next/navigation"
+import dayjs from "dayjs"
import styles from "./detail.module.scss"
-
import Character1 from "@/assets/character/step1.svg"
import SmallCalendar from "@/components/common/smallcalendar/smallcalendar"
import StepBox from "@/components/common/stepbox/stepbox"
-import { useRouter } from "next/navigation"
import TopSheet from "@/components/common/topsheet/topsheet"
import DefaultHeader from "@/components/layout/header/DefaultHeader"
import Navigation from "@/components/layout/Navigation"
+import { useGoalMainQuery } from "@/queries/goal/userGoalQuery"
const Page = () => {
const router = useRouter()
- const openModal = () => {
- router.push("/goal/detail/goal-modal")
+ const searchParams = useSearchParams()
+ const date = searchParams.get("date") // "2025-07-05"
+ const [clickedDate, setClickedDate] = useState(date)
+ const { data } = useGoalMainQuery(clickedDate)
+ const [weeklyGoalId, setWeeklyGoalId] = useState()
+
+ console.log(weeklyGoalId)
+
+ const formatStartDate = dayjs(data?.data?.startDate).format("M월 D일")
+ const formatEndDate = dayjs(data?.data?.endDate).format("M월 D일")
+ const openModal = (goalId: number) => {
+ router.push(`/goal/detail/goal-modal?goalId=${goalId}`)
}
+ const openLetterModal = () => {
+ const goalId = data?.data?.weeklyGoalId
+ if (goalId) {
+ // router.push(`/goal/letter?weeklyGoalId=${weeklyGoalId}`)
+ router.push(`/goal/letter/arrived?weeklyGoalId=${weeklyGoalId}`)
+ }
+ }
+
+ useEffect(() => {
+ setWeeklyGoalId(data?.data.weeklyGoalId)
+ }, [data])
return (
-
+
@@ -28,14 +51,17 @@ const Page = () => {
-
-
1월 24일 ~ 1월 31일 목표
+
+
+ {formatStartDate} ~ {formatEndDate} 목표
+
한 주 동안 레벨 업! 더 가볍고 건강하게!
0/5개 달성
+ {!data?.data?.isLetterSent &&
편지 작성
}
-
+
diff --git a/src/app/goal/letter/arrived/letter.module.scss b/src/app/goal/letter/arrived/letter.module.scss
new file mode 100644
index 0000000..3658bfe
--- /dev/null
+++ b/src/app/goal/letter/arrived/letter.module.scss
@@ -0,0 +1,102 @@
+.letter {
+ width: 100%;
+ height: 100%;
+ background: #fff;
+}
+
+.letter__mission {
+ margin-top: 60px;
+ padding-top: 30px;
+ text-align: center;
+
+ p {
+ color: $gray-70;
+ font-size: 18px;
+ line-height: 26px;
+ }
+
+ h2 {
+ margin-top: 8px;
+ font-size: 20px;
+ line-height: 28px;
+ font-weight: 700;
+ }
+
+ > div {
+ margin-top: 80px;
+ }
+}
+
+.letter__from {
+ padding: 80px 34px 0;
+
+ &-content {
+ margin-top: 45px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background: #F1F3FF;
+ border-radius: 4px;
+ height: 315px;
+ padding: 16px 25px;
+
+ h3 {
+ font-weight: 700;
+ font-size: 18px;
+ line-height: 1.5;
+ }
+
+ div {
+ text-align: center;
+
+ h3 {
+ text-align: left;
+ }
+ }
+
+ > h3 {
+ text-align: right;
+ }
+
+ p {
+ white-space: pre-line;
+ overflow-wrap: break-word;
+ font-size: 18px;
+ margin-top: 8px;
+ text-align: left;
+ }
+ }
+}
+
+.goal-box {
+ width: 100%;
+ display: flex;
+ gap: 20px;
+ padding-top: 32px;
+ overflow: scroll;
+ padding-bottom: 50px;
+
+ li {
+ p {
+ color: $gray-70
+ }
+
+ div {
+ width: 220px;
+ height: 235px;
+ border-radius: 6px;
+ background: #F2F2F5;
+ margin-top: 20px;
+ }
+ }
+}
+
+.btn {
+ background: #fff;
+ position: fixed;
+ width: 100%;
+ left: 0;
+ bottom: 0;
+ padding: 16px 32px;
+ border-top: 1px solid #D8DADB;
+}
\ No newline at end of file
diff --git a/src/app/goal/letter/arrived/page.tsx b/src/app/goal/letter/arrived/page.tsx
new file mode 100644
index 0000000..8254c29
--- /dev/null
+++ b/src/app/goal/letter/arrived/page.tsx
@@ -0,0 +1,49 @@
+"use client"
+import React, { useState } from "react"
+import { useRouter, useSearchParams } from "next/navigation"
+import styles from "./letter.module.scss"
+import ProfileIcon from "@/assets/character/profile-default.svg"
+import Button from "@/components/common/button/button"
+import { useGetGoalLetter } from "@/queries/letter/useLetterQuery"
+import BackHeader from "@/components/layout/header/BackHeader"
+
+const LetterModal = () => {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const weeklyGoalId = searchParams.get("weeklyGoalId")
+ const { data } = useGetGoalLetter(Number(weeklyGoalId))
+ const closeModal = () => {
+ router.back()
+ }
+ return (
+
+
+
+
1월 24일 ~ 1월 31일 목표
+
20분동안 걷기외 5개의 미션을 모두 마쳤어요!
+
+
+
+
+
+
+
To. 미니준
+
블라블라블라블라블라
+
+
From. 엄마가
+
+
+
+ {}}
+ />
+
+
+ )
+}
+
+export default LetterModal
diff --git a/src/app/goal/letter/letter.module.scss b/src/app/goal/letter/letter.module.scss
index 8809c8b..d70f02e 100644
--- a/src/app/goal/letter/letter.module.scss
+++ b/src/app/goal/letter/letter.module.scss
@@ -1,24 +1,40 @@
.letter {
padding: 0 16px;
+
&__title {
margin-top: 14px;
+
p {
font-size: 18px;
color: #8D8D8D;
margin-bottom: 7px;
}
+
strong {
font-size: 20px;
font-weight: 600;
}
}
+
&__field {
margin-top: 27px;
+
h3 {
font-size: 16px;
}
+
> div {
margin-top: 27px;
}
}
+
+ &__btn {
+ background: #fff;
+ position: fixed;
+ width: 100%;
+ left: 0;
+ bottom: 0;
+ padding: 16px 32px;
+ border-top: 1px solid #D8DADB;
+ }
}
\ No newline at end of file
diff --git a/src/app/goal/letter/page.tsx b/src/app/goal/letter/page.tsx
index 7972b8a..68b078b 100644
--- a/src/app/goal/letter/page.tsx
+++ b/src/app/goal/letter/page.tsx
@@ -1,23 +1,56 @@
+"use client"
import styles from "./letter.module.scss"
-import BackHeader from "@/components/layout/header/BackHeader";
+import BackHeader from "@/components/layout/header/BackHeader"
import ProfileIcon from "@/assets/character/profile-default.svg"
-import TextArea from "@/components/common/textarea/textarea";
+import TextArea from "@/components/common/textarea/textarea"
+import { useState } from "react"
+import { useGoalLetterQuery } from "@/queries/letter/useLetterQuery"
+import Button from "@/components/common/button/button"
+import { useSearchParams } from "next/navigation"
+
const LetterCreate = () => {
+ const searchParams = useSearchParams()
+ const [content, setContent] = useState("")
+ const weeklyGoalId = searchParams.get("weeklyGoalId")
+ const { mutate } = useGoalLetterQuery(Number(weeklyGoalId))
+ const handleChangeValue = (e) => {
+ setContent(e.target.value)
+ }
+ const onSubmit = () => {
+ mutate({
+ content,
+ })
+ }
+
return (
-
+
아이가 목표를 멋지게 해냈어요!
칭찬 편지를 남겨볼까요?
To.미니준
-
+
+
+
+
)
}
-export default LetterCreate
\ No newline at end of file
+export default LetterCreate
diff --git a/src/app/goal/page.tsx b/src/app/goal/page.tsx
index eb25e27..142d4e9 100644
--- a/src/app/goal/page.tsx
+++ b/src/app/goal/page.tsx
@@ -14,15 +14,22 @@ import AddButton from "@/components/common/addbutton/addbutton"
import TopSheet from "@/components/common/topsheet/topsheet"
import DefaultHeader from "@/components/layout/header/DefaultHeader"
import Navigation from "@/components/layout/Navigation"
+import { useGoalMainQuery } from "@/queries/goal/userGoalQuery"
+import { useState } from "react"
+import dayjs from "dayjs"
const Page = () => {
+ const today = dayjs().format("YYYY-MM-DD")
+ const [clickedDate, setClickedDate] = useState(today)
+
+ const { data } = useGoalMainQuery(clickedDate)
return (
-
+
@@ -42,16 +49,19 @@ const Page = () => {
- {/* /goals/detail > 상세, /goals/create > 수정 */}
-
+
0 ? `/goal/detail?date=${clickedDate}` : "/goal/create"}`}>
진행중인 목표
-
-
-
-
{}} />
- 목표 만들기
+ {data?.data?.goals.length > 0 ? (
+
+ ) : (
+
-
+ )}
diff --git a/src/assets/icons/common/icon-add_black.svg b/src/assets/icons/common/icon-add_black.svg
new file mode 100644
index 0000000..6083ca2
--- /dev/null
+++ b/src/assets/icons/common/icon-add_black.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/icons/common/icon-added_black.svg b/src/assets/icons/common/icon-added_black.svg
new file mode 100644
index 0000000..b855f58
--- /dev/null
+++ b/src/assets/icons/common/icon-added_black.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/icons/goals/goal1.svg b/src/assets/icons/goals/goal1.svg
new file mode 100644
index 0000000..975a790
--- /dev/null
+++ b/src/assets/icons/goals/goal1.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/goals/goal2.svg b/src/assets/icons/goals/goal2.svg
new file mode 100644
index 0000000..9fc1bd8
--- /dev/null
+++ b/src/assets/icons/goals/goal2.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/goals/goal3.svg b/src/assets/icons/goals/goal3.svg
new file mode 100644
index 0000000..3117456
--- /dev/null
+++ b/src/assets/icons/goals/goal3.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/goals/goal4.svg b/src/assets/icons/goals/goal4.svg
new file mode 100644
index 0000000..ff4e5e2
--- /dev/null
+++ b/src/assets/icons/goals/goal4.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/goals/goal5.svg b/src/assets/icons/goals/goal5.svg
new file mode 100644
index 0000000..33abc7e
--- /dev/null
+++ b/src/assets/icons/goals/goal5.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/goals/goal6.svg b/src/assets/icons/goals/goal6.svg
new file mode 100644
index 0000000..73afd55
--- /dev/null
+++ b/src/assets/icons/goals/goal6.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/goals/goal7.svg b/src/assets/icons/goals/goal7.svg
new file mode 100644
index 0000000..4b6373e
--- /dev/null
+++ b/src/assets/icons/goals/goal7.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/goals/goal8.svg b/src/assets/icons/goals/goal8.svg
new file mode 100644
index 0000000..51e652e
--- /dev/null
+++ b/src/assets/icons/goals/goal8.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/goals/goal9.svg b/src/assets/icons/goals/goal9.svg
new file mode 100644
index 0000000..46b427b
--- /dev/null
+++ b/src/assets/icons/goals/goal9.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/components/common/addbutton/addbutton.tsx b/src/components/common/addbutton/addbutton.tsx
index 1094929..ad4e3fe 100644
--- a/src/components/common/addbutton/addbutton.tsx
+++ b/src/components/common/addbutton/addbutton.tsx
@@ -1,7 +1,7 @@
import classNames from "classnames/bind"
import styles from "./addbutton.module.scss"
-import Addable from "@/assets/icons/common/icon-add.svg"
-import Added from "@/assets/icons/common/icon-added.svg"
+import Addable from "@/assets/icons/common/icon-add_black.svg"
+import Added from "@/assets/icons/common/icon-added_black.svg"
const cx = classNames.bind(styles)
diff --git a/src/components/common/buttomsheet/buttomsheet.module.scss b/src/components/common/buttomsheet/buttomsheet.module.scss
index e0b7ab6..b762ec3 100644
--- a/src/components/common/buttomsheet/buttomsheet.module.scss
+++ b/src/components/common/buttomsheet/buttomsheet.module.scss
@@ -37,6 +37,7 @@
display: flex;
align-items: center;
justify-content: space-between;
+ z-index: 10;
h2 {
font-size: 20px;
diff --git a/src/components/common/gaugedonut/gaugedonut.tsx b/src/components/common/gaugedonut/gaugedonut.tsx
index f72f7dd..27662a9 100644
--- a/src/components/common/gaugedonut/gaugedonut.tsx
+++ b/src/components/common/gaugedonut/gaugedonut.tsx
@@ -1,12 +1,15 @@
-import classNames from "classnames/bind";
-import styles from "./gaugedonut.module.scss";
-const cx = classNames.bind(styles);
+import classNames from "classnames/bind"
+import styles from "./gaugedonut.module.scss"
-const GaugeDonut = () => {
+const cx = classNames.bind(styles)
+
+const GaugeDonut = ({ goalLength }) => {
return (
-
0/5
+
+ {goalLength}/5
+
)
}
-export default GaugeDonut
\ No newline at end of file
+export default GaugeDonut
diff --git a/src/components/common/smallcalendar/smallcalendar.module.scss b/src/components/common/smallcalendar/smallcalendar.module.scss
index a45f2a3..776c2d0 100644
--- a/src/components/common/smallcalendar/smallcalendar.module.scss
+++ b/src/components/common/smallcalendar/smallcalendar.module.scss
@@ -2,21 +2,44 @@
background-color: #fff;
border-radius: 0 0 32px 32px;
text-align: center;
+ padding: 24px 10px;
+
+ &-header {
+ h2 {
+ padding-top: 26px;
+ text-align: center;
+ font-size: 24px;
+ font-weight: 700;
+ margin-bottom: 27px;
+ }
+ }
&-item {
display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 24px 30px;
+ flex-direction: column;
font-weight: 700;
+ padding-bottom: 10px;
- li {
+ p {
+ position: relative;
+ margin-top: 5px;
+ padding: 10px;
font-size: 20px;
- text-align: center;
- p {
- margin-top: 5px;
- padding: 10px;
+ &:nth-of-type(2)::after {
+ content: "";
+ position: absolute;
+ top: -2px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: -1;
+ border-radius: 50%;
+ width: 46px;
+ height: 46px;
+ }
+
+ &.clickedDate::after {
+ background: #F1F3FF;
}
}
}
diff --git a/src/components/common/smallcalendar/smallcalendar.tsx b/src/components/common/smallcalendar/smallcalendar.tsx
index 6070c50..bda8387 100644
--- a/src/components/common/smallcalendar/smallcalendar.tsx
+++ b/src/components/common/smallcalendar/smallcalendar.tsx
@@ -1,41 +1,100 @@
-import classNames from "classnames/bind";
-import styles from "./smallcalendar.module.scss";
-const cx = classNames.bind(styles);
-const SmallCalendar = () => {
+"use client"
+
+import { useEffect, useRef, useState } from "react"
+import classNames from "classnames/bind"
+import styles from "./smallcalendar.module.scss"
+import { Swiper, SwiperSlide } from "swiper/react"
+import "swiper/css"
+import generateDates from "@/lib/utils/generatedates"
+import dayjs from "dayjs"
+
+import "dayjs/locale/ko"
+
+dayjs.locale("ko")
+
+const cx = classNames.bind(styles)
+
+const SmallCalendar = ({ clickedDate, setClickedDate }) => {
+ const swiperRef = useRef(null)
+ const centerDate = clickedDate ? dayjs(clickedDate) : dayjs()
+ const [dates, setDates] = useState(() => generateDates(centerDate, 30))
+ const [currentCenterDate, setCurrentCenterDate] = useState(null)
+
+ const todayIndex = dates.findIndex((d) => d.isToday)
+
+ const handleSlideChange = (swiper: any) => {
+ const center = dates[swiper.activeIndex]
+ if (center) {
+ setCurrentCenterDate(center.fullDate)
+ }
+
+ // 날짜 확장 로직
+ const buffer = 5
+ if (swiper.activeIndex < buffer) {
+ const first = dates[0].fullDate
+ const more = generateDates(first.subtract(30, "day"), 30)
+ setDates((prev) => [...more.slice(0, 30), ...prev])
+ swiper.slideTo(swiper.activeIndex + 30, 0)
+ }
+
+ if (swiper.activeIndex > dates.length - buffer) {
+ const last = dates[dates.length - 1].fullDate
+ const more = generateDates(last.add(1, "day"), 30)
+ setDates((prev) => [...prev, ...more.slice(1)])
+ }
+ }
+
+ useEffect(() => {
+ if (!clickedDate || dates.length === 0) return
+
+ const clickedIndex = dates.findIndex((d) => d.fullDate.format("YYYY-MM-DD") === clickedDate)
+
+ if (clickedIndex !== -1 && swiperRef.current) {
+ swiperRef.current.slideTo(clickedIndex, 0)
+ setCurrentCenterDate(dates[clickedIndex].fullDate)
+ }
+ }, [clickedDate, dates])
+
return (
-
- -
-
월
- 24
-
- -
-
월
- 24
-
- -
-
월
- 24
-
- -
-
월
- 24
-
- -
-
월
- 24
-
- -
-
월
- 24
-
- -
-
월
- 24
-
-
+ {currentCenterDate && (
+
+
{currentCenterDate.format("YYYY년 M월")}
+
+ )}
+
{
+ swiperRef.current = swiper
+ }}
+ onSlideChange={handleSlideChange}
+ slidesPerView={7}
+ centeredSlides
+ spaceBetween={10}
+ sx={{
+ padding: "0 10px",
+ }}>
+ {dates.map((day) => (
+
+ {
+ const dayString = String(day.date).padStart(2, "0")
+ setClickedDate(`${currentCenterDate?.format("YYYY-MM")}-${dayString}`)
+ }}>
+
{day.label}
+
+ {day.date}
+
+
+
+ ))}
+
)
}
-export default SmallCalendar;
\ No newline at end of file
+
+export default SmallCalendar
diff --git a/src/components/common/stepbox/stepbox.tsx b/src/components/common/stepbox/stepbox.tsx
index c1b75e7..550081e 100644
--- a/src/components/common/stepbox/stepbox.tsx
+++ b/src/components/common/stepbox/stepbox.tsx
@@ -1,32 +1,23 @@
-import classNames from "classnames/bind";
-import styles from "./stepbox.module.scss";
-const cx = classNames.bind(styles);
-const StepBox = () => {
+import classNames from "classnames/bind"
+import styles from "./stepbox.module.scss"
+
+const cx = classNames.bind(styles)
+const StepBox = ({ goalList, openModal }) => {
return (
-
-
-
-
-
-
20분동안 걷기
-
시작
+ {goalList.map((item) => (
+
+
+
+
{item.name}
+ openModal(item.goalId)}>
+ 시작
+
+
-
+ ))}
)
}
-export default StepBox
\ No newline at end of file
+export default StepBox
diff --git a/src/components/common/textarea/textarea.tsx b/src/components/common/textarea/textarea.tsx
index 7059a31..f4b1a30 100644
--- a/src/components/common/textarea/textarea.tsx
+++ b/src/components/common/textarea/textarea.tsx
@@ -1,16 +1,30 @@
"use client"
-import React from "react";
+import React from "react"
import styles from "./textarea.module.scss"
-import {CloseIcon} from "@/components/common/icon";
-const TextArea = ({placeholder} : { placeholder: string }) => {
+import { CloseIcon } from "@/components/common/icon"
+
+const TextArea = ({
+ name,
+ placeholder,
+ onChange,
+ value,
+}: {
+ name: string
+ placeholder: string
+ value: string
+ onChange: (e: React.ChangeEvent
) => void
+}) => {
return (
)
}
-export default TextArea
\ No newline at end of file
+export default TextArea
diff --git a/src/components/common/topsheet/topsheet.module.scss b/src/components/common/topsheet/topsheet.module.scss
index 335d0da..ee37d01 100644
--- a/src/components/common/topsheet/topsheet.module.scss
+++ b/src/components/common/topsheet/topsheet.module.scss
@@ -4,12 +4,7 @@
box-shadow: 0 18px 35px 0 rgba(0, 0, 0, 0.1);
text-align: center;
padding-bottom: 10px;
- h2 {
- padding-top: 26px;
- text-align: center;
- font-size: 24px;
- font-weight: 700;
- }
+
&-bar {
display: inline-block;
width: 78px;
diff --git a/src/components/common/topsheet/topsheet.tsx b/src/components/common/topsheet/topsheet.tsx
index 6ffbe93..8122ff2 100644
--- a/src/components/common/topsheet/topsheet.tsx
+++ b/src/components/common/topsheet/topsheet.tsx
@@ -1,15 +1,15 @@
-import classNames from "classnames/bind";
-import styles from "./topsheet.module.scss";
-const cx = classNames.bind(styles);
+import classNames from "classnames/bind"
+import styles from "./topsheet.module.scss"
+
+const cx = classNames.bind(styles)
const TopSheet = ({ children }) => {
return (
)
}
-export default TopSheet;
\ No newline at end of file
+export default TopSheet
diff --git a/src/lib/utils/generatedates.ts b/src/lib/utils/generatedates.ts
new file mode 100644
index 0000000..608c6a2
--- /dev/null
+++ b/src/lib/utils/generatedates.ts
@@ -0,0 +1,18 @@
+import dayjs from "dayjs"
+
+const generateDates = (centerDate: dayjs.Dayjs, range = 30) => {
+ const dates = []
+ for (let i = -range; i <= range; i++) {
+ const d = centerDate.add(i, "day")
+ dates.push({
+ key: d.format("YYYY-MM-DD"),
+ label: d.format("dd"), // 요일
+ date: d.date(),
+ fullDate: d,
+ isToday: d.isSame(dayjs(), "day"),
+ })
+ }
+ return dates
+}
+
+export default generateDates
diff --git a/src/queries/goal/userGoalQuery.ts b/src/queries/goal/userGoalQuery.ts
new file mode 100644
index 0000000..7a72715
--- /dev/null
+++ b/src/queries/goal/userGoalQuery.ts
@@ -0,0 +1,30 @@
+import { useMutation, useQuery } from "@tanstack/react-query"
+import { createGoal, getGoalMainInfo, postCertifyGoal } from "@/app/api/goal"
+
+export const useGoalMainQuery = (clickedDate) => {
+ return useQuery({
+ queryKey: ["goalmain", clickedDate],
+ queryFn: () => getGoalMainInfo(clickedDate),
+ })
+}
+
+export const useGoalCreate = (data) => {
+ return useMutation({
+ mutationKey: ["goaldata"],
+ mutationFn: () => createGoal(data),
+ })
+}
+
+export const useGoalDetailQuery = (clickedDate) => {
+ return useQuery({
+ queryKey: ["goaldetail", clickedDate],
+ queryFn: () => getGoalMainInfo(clickedDate),
+ })
+}
+
+export const useGoalCertifyQuery = (goalId: number) => {
+ return useMutation({
+ mutationKey: ["goalcertify", goalId],
+ mutationFn: (request) => postCertifyGoal(goalId, request),
+ })
+}
diff --git a/src/queries/letter/useLetterQuery.ts b/src/queries/letter/useLetterQuery.ts
new file mode 100644
index 0000000..284d663
--- /dev/null
+++ b/src/queries/letter/useLetterQuery.ts
@@ -0,0 +1,17 @@
+import { useMutation } from "@tanstack/react-query"
+import { createGoalLetter, getGoalLetter } from "@/app/api/letter"
+import { postLetter } from "@/app/api/goal"
+
+export const useGoalLetterQuery = (weeklyGoalId: number) => {
+ return useMutation({
+ mutationKey: ["letter_create", weeklyGoalId],
+ mutationFn: (request) => createGoalLetter(weeklyGoalId, request),
+ })
+}
+
+export const useGetGoalLetter = (weeklyGoalId: number) => {
+ return useMutation({
+ mutationKey: ["letter_gey", weeklyGoalId],
+ mutationFn: () => getGoalLetter(weeklyGoalId),
+ })
+}
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
index 3b7bea4..8eca643 100644
--- a/src/styles/_variables.scss
+++ b/src/styles/_variables.scss
@@ -1,5 +1,5 @@
$nav-height: 57px;
-$header-height: 60px;
+$header-height: 52px;
$layout-top: 70px;
$layout-bottom: 60px;
\ No newline at end of file