diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml index 57b7d09..bc11cdd 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/nextjs.yml @@ -30,6 +30,10 @@ jobs: # Build job build: runs-on: ubuntu-latest + + env: + NEXT_PUBLIC_API_URL: ${{secrets.NEXT_PUBLIC_API_URL}} + steps: - name: Checkout uses: actions/checkout@v4 @@ -75,6 +79,11 @@ jobs: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- - name: Install dependencies run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} + - name: Check environment variable + run: echo NEXT_PUBLIC_API_URL + - name: Create .env file + run: | + echo NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }} >> .env - name: Build with Next.js run: ${{ steps.detect-package-manager.outputs.runner }} next build - name: Export with Next.js @@ -84,6 +93,7 @@ jobs: with: path: ./out + # Deployment job deploy: environment: diff --git a/apis/challenge.ts b/apis/challenge.ts index ff79fbf..8dea8d5 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,6 +13,18 @@ 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 { const { data } = await client.get(`/challenges`); return data; @@ -32,8 +44,32 @@ async function getChallengDetail(): Promise { const response = await client.get( `/challenges/attendance/2?year=2024&month=6`, ); - // console.log("challengeData", response.data.result); return response.data.result; } -export { getMyChallengeList, getChallengDetail }; +async function getChallengeAds(): Promise { + const { data } = await client.get(`/challenges/ads`); + return data; +} + +async function getChallengeSearch( + keyword: string, +): Promise { + const { data } = await client.get(`/challenges/search?searchWord=${keyword}`); + return data; +} + +async function postNewChallenge(challengeIdx: number): Promise { + const { data } = await client.post(`/challenges/participation`, { + challengeIdx, + }); + return data; +} + +export { + getMyChallengeList, + getChallengeAds, + getChallengeSearch, + postNewChallenge, + getChallengDetail +}; 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 80128a4..2242fa1 100644 --- a/apis/hooks/challenge.ts +++ b/apis/hooks/challenge.ts @@ -1,5 +1,12 @@ -import { getMyChallengeList, getChallengDetail } from "../challenge"; -import { useQuery } from "@tanstack/react-query"; +import { + getChallengeAds, + getChallengeSearch, + getMyChallengeList, + postNewChallenge, + getChallengDetail, +} from "../challenge"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + function useGetMyChallengeList() { const { data } = useQuery({ @@ -18,4 +25,50 @@ function useGetChallengeDetail() { return { data }; } -export { useGetMyChallengeList, useGetChallengeDetail }; +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, + useGetChallengeDetail, +}; + 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 ( -
+
- - - + + +
); }; + +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 ( <> -
풍물패 두드림 챌린지 인증하기
+
챌린지 인증하기
setIsModalVisible(false)}>
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>; + challengeInfo: ChallengeType; } -const Challenge: NextPage = ({ setIsModalVisible }) => { +const Challenge: NextPage = ({ + setIsModalVisible, + challengeInfo, +}) => { const router = useRouter(); const [isAdmin] = useAtom(isAdminAtom); @@ -20,10 +25,14 @@ const Challenge: NextPage = ({ setIsModalVisible }) => { onClick={isAdmin ? null : () => router.push("/challenge")} > -
풍물패 두드림
-
12명 참여 중
+
{challengeInfo.name}
+
+ {challengeInfo.participantsNum}명 참여 중 +
-
서울시립도서관 4층 | 월 16시
+
+ {challengeInfo.location} | {challengeInfo.schedule} +
= ({ setIsModalVisible }) => { {!isAdmin && (
개인달성률
-
100%
+
{challengeInfo.attendanceRate}%
)}
전체달성률
-
100%
+
{challengeInfo.totalAttendanceRate}%
void; @@ -19,6 +20,8 @@ const HomeChallenge: NextPage = ({ onNotify }) => { const [isAdmin] = useAtom(isAdminAtom); const [isOpen, setIsOpen] = useState(false); + const { data: challengeInfo } = useGetMyChallengeList(); + return (
@@ -31,8 +34,13 @@ const HomeChallenge: NextPage = ({ onNotify }) => { > {isAdmin ? "새 프로그램 등록" : "참여 프로그램 추가"}
- - + {challengeInfo?.result.map((info) => ( + + ))} { + switch (type) { + case "mostAttendancedChallenge": + return "출석률이 가장 높은 챌린지 🔥"; + break; + case "mostParticipatedChallenge": + return "참여자가 가장 많은 챌린지 🔥"; + break; + case "mostRecentlyStartedChallenge": + return "가장 최근에 개설된 챌린지 🔥"; + break; + } + }; -export const PopularChallenge: NextPage = () => { return ( -
참여자가 가장 많은 챌린지 🔥
-
레전드 영화보기 챌린지
+
{returnTitle()}
+
{challengeInfo?.name}
); -}; +} 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 ( -
풍물패 두드림
-
서울시립도서관 4층 | 월 16시
+
{challengeInfo.name}
+
+ {challengeInfo.location} | {challengeInfo.schedule} +
참여 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(""); - const notify = () => { - toast.success("(챌린지이름)에 성공적으로 참여하셨습니다.", { + const { data } = useGetChallengeSearch(keyword); + + const notify = (title: string) => { + toast.success(`${title}에 성공적으로 참여하셨습니다.`, { position: "bottom-center", - icon: ({ theme, type }) => ( - - ), + icon: ({ theme, type }) => , }); }; @@ -27,8 +29,9 @@ const JoinChallenge: NextPage = () => {
- - + {data?.result.map((info) => ( + + ))}
{ const notify = (msg: string) => { toast.success(msg, { position: "bottom-center", - icon: ({ theme, type }) => ( - - ), + icon: ({ theme, type }) => , }); };