diff --git a/apis/challenge.ts b/apis/challenge.ts index 4eea8ac..3ea3c3b 100644 --- a/apis/challenge.ts +++ b/apis/challenge.ts @@ -1,4 +1,4 @@ -import client, { ResponseBody } from "./client"; +import client, { ResponseBody, ResponseBody2 } from "./client"; interface GetMyChallengeListResponse extends ResponseBody { result: Challenge[]; @@ -13,9 +13,45 @@ export interface Challenge { attendanceRate: number; totalAttendanceRate: number; } +interface GetChallengeAdsResponse extends ResponseBody2 { + result: { + mostParticipatedChallenge: Challenge; + mostAttendancedChallenge: Challenge; + mostRecentlyStartedChallenge: Challenge; + }; +} + +interface getChallengeSearchResponse extends ResponseBody2 { + result: Challenge[]; +} + async function getMyChallengeList(): Promise<GetMyChallengeListResponse> { const { data } = await client.get(`/challenges`); return data; } -export { getMyChallengeList }; +async function getChallengeAds(): Promise<GetChallengeAdsResponse> { + const { data } = await client.get(`/challenges/ads`); + return data; +} + +async function getChallengeSearch( + keyword: string, +): Promise<getChallengeSearchResponse> { + const { data } = await client.get(`/challenges/search?searchWord=${keyword}`); + return data; +} + +async function postNewChallenge(challengeIdx: number): Promise<ResponseBody> { + const { data } = await client.post(`/challenges/participation`, { + challengeIdx, + }); + return data; +} + +export { + getMyChallengeList, + getChallengeAds, + getChallengeSearch, + postNewChallenge, +}; diff --git a/apis/client.ts b/apis/client.ts index c3dbd5a..04a1d94 100644 --- a/apis/client.ts +++ b/apis/client.ts @@ -5,6 +5,10 @@ interface ResponseBody { code: number; message: string; } +interface ResponseBody2 { + isSuccess: boolean; + message: string; +} export const setTokenFromLocalStorage = (access_token: string) => { localStorage.setItem("access_token", access_token); @@ -49,4 +53,4 @@ client.interceptors.request.use( ); export default client; -export type { ResponseBody }; +export type { ResponseBody, ResponseBody2 }; diff --git a/apis/hooks/challenge.ts b/apis/hooks/challenge.ts index 0ab0369..3055a43 100644 --- a/apis/hooks/challenge.ts +++ b/apis/hooks/challenge.ts @@ -1,5 +1,10 @@ -import { getMyChallengeList } from "../challenge"; -import { useQuery } from "@tanstack/react-query"; +import { + getChallengeAds, + getChallengeSearch, + getMyChallengeList, + postNewChallenge, +} from "../challenge"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; function useGetMyChallengeList() { const { data } = useQuery({ @@ -10,4 +15,48 @@ function useGetMyChallengeList() { return { data }; } -export { useGetMyChallengeList }; +function useGetChallengeAds() { + const { data } = useQuery({ + queryKey: ["getChallengeAds"], + queryFn: getChallengeAds, + }); + + return { data }; +} + +function useGetChallengeSearch(keyword: string) { + const { data } = useQuery({ + queryKey: ["getChallengeSearch", keyword], + queryFn: () => getChallengeSearch(keyword), + enabled: keyword.trim().length !== 0, + }); + + return { data }; +} + +function usePostNewChallenge( + challengeIdx: number, + challengeName: string, + notify: (title: string) => void, +) { + const queryClient = useQueryClient(); + const { mutate } = useMutation({ + mutationKey: ["postNewChallenge", challengeIdx], + mutationFn: () => postNewChallenge(challengeIdx), + onSuccess: () => { + notify(challengeName); + queryClient.invalidateQueries({ + queryKey: ["getChallengeSearch"], + }); + }, + }); + + return { mutate }; +} + +export { + useGetMyChallengeList, + useGetChallengeAds, + useGetChallengeSearch, + usePostNewChallenge, +}; diff --git a/components/home/carousel.tsx b/components/home/carousel.tsx index 53e2f8e..dcd6c07 100644 --- a/components/home/carousel.tsx +++ b/components/home/carousel.tsx @@ -1,19 +1,32 @@ import { NextPage } from "next"; import FlexBox from "../Flexbox"; -import { PopularChallenge } from "./popular"; +import PopularChallenge from "./popular"; import useEmblaCarousel from "embla-carousel-react"; import Autoplay from "embla-carousel-autoplay"; +import { useGetChallengeAds } from "@/apis/hooks/challenge"; -export const HomeCarousel: NextPage = () => { +const HomeCarousel: NextPage = () => { const [emblaRef] = useEmblaCarousel({ loop: true }, [Autoplay()]); + const { data } = useGetChallengeAds(); return ( - <div className="w-full embla overflow-hidden rounded-lg " ref={emblaRef}> + <div className="w-full embla overflow-hidden rounded-lg" ref={emblaRef}> <FlexBox className="embla__container"> - <PopularChallenge /> - <PopularChallenge /> - <PopularChallenge /> + <PopularChallenge + challengeInfo={data?.result.mostAttendancedChallenge} + type="mostAttendancedChallenge" + /> + <PopularChallenge + challengeInfo={data?.result.mostParticipatedChallenge} + type="mostParticipatedChallenge" + /> + <PopularChallenge + challengeInfo={data?.result.mostRecentlyStartedChallenge} + type="mostRecentlyStartedChallenge" + /> </FlexBox> </div> ); }; + +export default HomeCarousel; diff --git a/components/home/certifyModal.tsx b/components/home/certifyModal.tsx index 0e8bbba..22c3532 100644 --- a/components/home/certifyModal.tsx +++ b/components/home/certifyModal.tsx @@ -27,7 +27,7 @@ export default function CertifyModal({ return ( <> <FlexBox className="justify-between items-start"> - <div className="h3 mb-4">풍물패 두드림 챌린지 인증하기</div> + <div className="h3 mb-4">챌린지 인증하기</div> <div onClick={() => setIsModalVisible(false)}> <Image src={"/svgs/Close.svg"} width={20} height={20} /> </div> diff --git a/components/home/challenge.tsx b/components/home/challenge.tsx index 3015706..29f068d 100644 --- a/components/home/challenge.tsx +++ b/components/home/challenge.tsx @@ -3,12 +3,17 @@ import FlexBox from "../Flexbox"; import { useRouter } from "next/router"; import { useAtom } from "jotai"; import { isAdminAtom } from "@/utils/atom"; +import { Challenge as ChallengeType } from "@/apis/challenge"; interface ChallengeProps { setIsModalVisible: React.Dispatch<React.SetStateAction<boolean>>; + challengeInfo: ChallengeType; } -const Challenge: NextPage<ChallengeProps> = ({ setIsModalVisible }) => { +const Challenge: NextPage<ChallengeProps> = ({ + setIsModalVisible, + challengeInfo, +}) => { const router = useRouter(); const [isAdmin] = useAtom(isAdminAtom); @@ -20,10 +25,14 @@ const Challenge: NextPage<ChallengeProps> = ({ setIsModalVisible }) => { onClick={isAdmin ? null : () => router.push("/challenge")} > <FlexBox className="w-full justify-between items-start"> - <div className="h2">풍물패 두드림</div> - <div className="h4 text-gray-500">12명 참여 중</div> + <div className="h2">{challengeInfo.name}</div> + <div className="h4 text-gray-500"> + {challengeInfo.participantsNum}명 참여 중 + </div> </FlexBox> - <div className="h4 self-start">서울시립도서관 4층 | 월 16시</div> + <div className="h4 self-start"> + {challengeInfo.location} | {challengeInfo.schedule} + </div> </FlexBox> <FlexBox direction="col" className="w-full gap-1"> <FlexBox @@ -33,12 +42,12 @@ const Challenge: NextPage<ChallengeProps> = ({ setIsModalVisible }) => { {!isAdmin && ( <FlexBox className="gap-1"> <div className="h5 text-gray-500">개인달성률</div> - <div className="h3">100%</div> + <div className="h3">{challengeInfo.attendanceRate}%</div> </FlexBox> )} <FlexBox className="gap-1"> <div className="h5 text-gray-500">전체달성률</div> - <div className="h3">100%</div> + <div className="h3">{challengeInfo.totalAttendanceRate}%</div> </FlexBox> </FlexBox> <div diff --git a/components/home/challengeBox.tsx b/components/home/challengeBox.tsx index a114f41..aae9041 100644 --- a/components/home/challengeBox.tsx +++ b/components/home/challengeBox.tsx @@ -1,7 +1,7 @@ import { NextPage } from "next"; import FlexBox from "../Flexbox"; import Challenge from "./challenge"; -import { HomeCarousel } from "./carousel"; +import HomeCarousel from "./carousel"; import { useRouter } from "next/router"; import { isAdminAtom } from "@/utils/atom"; import { useAtom } from "jotai"; @@ -9,6 +9,7 @@ import CertifyModal from "./certifyModal"; import { useState } from "react"; import ReactModal from "react-modal"; import Image from "next/image"; +import { useGetMyChallengeList } from "@/apis/hooks/challenge"; interface HomeChallengeProps { onNotify: (msg: string) => void; @@ -19,6 +20,8 @@ const HomeChallenge: NextPage<HomeChallengeProps> = ({ onNotify }) => { const [isAdmin] = useAtom(isAdminAtom); const [isOpen, setIsOpen] = useState<boolean>(false); + const { data: challengeInfo } = useGetMyChallengeList(); + return ( <FlexBox direction="col" className="w-full items-start gap-2 p-4"> <div className="h1"> @@ -31,8 +34,13 @@ const HomeChallenge: NextPage<HomeChallengeProps> = ({ onNotify }) => { > {isAdmin ? "새 프로그램 등록" : "참여 프로그램 추가"} </div> - <Challenge setIsModalVisible={setIsOpen} /> - <Challenge setIsModalVisible={setIsOpen} /> + {challengeInfo?.result.map((info) => ( + <Challenge + setIsModalVisible={setIsOpen} + challengeInfo={info} + key={info.challengeIdx} + /> + ))} <ReactModal isOpen={isOpen} style={modalStyle} diff --git a/components/home/popular.tsx b/components/home/popular.tsx index 280a64d..354c03b 100644 --- a/components/home/popular.tsx +++ b/components/home/popular.tsx @@ -2,15 +2,41 @@ import { NextPage } from "next"; import FlexBox from "../Flexbox"; import React from "react"; import useEmblaCarousel from "embla-carousel-react"; +import { Challenge } from "@/apis/challenge"; + +interface PopularChallengeProps { + challengeInfo: Challenge; + type: + | "mostParticipatedChallenge" + | "mostAttendancedChallenge" + | "mostRecentlyStartedChallenge"; +} + +export default function PopularChallenge({ + challengeInfo, + type, +}: PopularChallengeProps) { + const returnTitle = () => { + switch (type) { + case "mostAttendancedChallenge": + return "출석률이 가장 높은 챌린지 🔥"; + break; + case "mostParticipatedChallenge": + return "참여자가 가장 많은 챌린지 🔥"; + break; + case "mostRecentlyStartedChallenge": + return "가장 최근에 개설된 챌린지 🔥"; + break; + } + }; -export const PopularChallenge: NextPage = () => { return ( <FlexBox direction="col" className="w-full bg-gray-100 items-start p-4 min-w-0 flex-[0_0_100%]" > - <div className="h5 text-gray-700">참여자가 가장 많은 챌린지 🔥</div> - <div className="h2">레전드 영화보기 챌린지</div> + <div className="h5 text-gray-700">{returnTitle()}</div> + <div className="h2">{challengeInfo?.name}</div> </FlexBox> ); -}; +} diff --git a/components/home/searchResult.tsx b/components/home/searchResult.tsx index e17273d..eafaf19 100644 --- a/components/home/searchResult.tsx +++ b/components/home/searchResult.tsx @@ -1,15 +1,31 @@ +import { Challenge } from "@/apis/challenge"; import FlexBox from "../Flexbox"; +import { usePostNewChallenge } from "@/apis/hooks/challenge"; interface SearchResultProps { - onClick?: () => void; + notify: (title: string) => void; + challengeInfo: Challenge; } -export default function SearchResult({ onClick }: SearchResultProps) { +export default function SearchResult({ + notify, + challengeInfo, +}: SearchResultProps) { + const { mutate } = usePostNewChallenge( + challengeInfo.challengeIdx, + challengeInfo.name, + notify, + ); + + const onClick = () => mutate(); + return ( <FlexBox className="w-full border-b p-2 justify-between"> <FlexBox direction="col" className="gap-1 items-start"> - <div className="h3">풍물패 두드림</div> - <div className="h4 text-gray-700">서울시립도서관 4층 | 월 16시</div> + <div className="h3">{challengeInfo.name}</div> + <div className="h4 text-gray-700"> + {challengeInfo.location} | {challengeInfo.schedule} + </div> </FlexBox> <div className="h2 text-main-color p-2" onClick={onClick}> 참여 diff --git a/pages/challenge/join.tsx b/pages/challenge/join.tsx index 25f56db..8df79f1 100644 --- a/pages/challenge/join.tsx +++ b/pages/challenge/join.tsx @@ -1,3 +1,4 @@ +import { useGetChallengeSearch } from "@/apis/hooks/challenge"; import Divider from "@/components/Divider"; import HeadFunction from "@/components/HeadFunction"; import NavBar from "@/components/NavBar"; @@ -8,16 +9,17 @@ import Image from "next/image"; import { useState } from "react"; import { toast, ToastContainer, Zoom } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; +import CheckIcon from "@/public/svgs/Check.svg"; const JoinChallenge: NextPage = () => { const [keyword, setKeyword] = useState<string>(""); - const notify = () => { - toast.success("(챌린지이름)에 성공적으로 참여하셨습니다.", { + const { data } = useGetChallengeSearch(keyword); + + const notify = (title: string) => { + toast.success(`${title}에 성공적으로 참여하셨습니다.`, { position: "bottom-center", - icon: ({ theme, type }) => ( - <Image src="/svgs/Check.svg" width={24} height={24} /> - ), + icon: ({ theme, type }) => <CheckIcon width={24} height={24} />, }); }; @@ -27,8 +29,9 @@ const JoinChallenge: NextPage = () => { <SearchBar value={keyword} setValue={setKeyword} /> <Divider height={8} /> <div className="w-full px-4"> - <SearchResult onClick={notify} /> - <SearchResult onClick={notify} /> + {data?.result.map((info) => ( + <SearchResult notify={notify} challengeInfo={info} /> + ))} </div> <NavBar /> <ToastContainer diff --git a/pages/index.tsx b/pages/index.tsx index 881a1fe..d5b3001 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -2,21 +2,20 @@ import HeadFunction from "@/components/HeadFunction"; import { NextPage } from "next"; import NavBar from "@/components/NavBar"; import Challenge from "@/components/home/challenge"; -import { HomeCarousel } from "@/components/home/carousel"; +import HomeCarousel from "@/components/home/carousel"; import { HomeHeader } from "@/components/home/header"; import HomeChallenge from "@/components/home/challengeBox"; import FlexBox from "@/components/Flexbox"; import { ToastContainer, Zoom, toast } from "react-toastify"; import Image from "next/image"; import "react-toastify/dist/ReactToastify.css"; +import CheckIcon from "@/public/svgs/Check.svg"; const Home: NextPage = () => { const notify = (msg: string) => { toast.success(msg, { position: "bottom-center", - icon: ({ theme, type }) => ( - <Image src="/svgs/Check.svg" width={24} height={24} /> - ), + icon: ({ theme, type }) => <CheckIcon width={24} height={24} />, }); };