diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..7b8b73a --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,91 @@ +# Sample workflow for building and deploying a Next.js site to GitHub Pages +# +# To get started with Next.js see: https://nextjs.org/docs/getting-started +# +name: Test Next Build + +on: + pull_request: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +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 + - name: Detect package manager + id: detect-package-manager + run: | + if [ -f "${{ github.workspace }}/yarn.lock" ]; then + echo "manager=yarn" >> $GITHUB_OUTPUT + echo "command=install" >> $GITHUB_OUTPUT + echo "runner=yarn" >> $GITHUB_OUTPUT + exit 0 + elif [ -f "${{ github.workspace }}/package.json" ]; then + echo "manager=npm" >> $GITHUB_OUTPUT + echo "command=ci" >> $GITHUB_OUTPUT + echo "runner=npx --no-install" >> $GITHUB_OUTPUT + exit 0 + else + echo "Unable to determine package manager" + exit 1 + fi + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: ${{ steps.detect-package-manager.outputs.manager }} + - name: Setup Pages + uses: actions/configure-pages@v5 + with: + # Automatically inject basePath in your Next.js configuration file and disable + # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). + # + # You may remove this line if you want to manage the configuration yourself. + static_site_generator: next + - name: Restore cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ 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 + run: ${{ steps.detect-package-manager.outputs.runner }} next export + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./out diff --git a/.github/workflows/nextjs.yml b/.github/workflows/deploy.yml similarity index 92% rename from .github/workflows/nextjs.yml rename to .github/workflows/deploy.yml index 57b7d09..d9e111e 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/deploy.yml @@ -9,7 +9,7 @@ on: push: branches: ["deploy"] pull_request: - branches: ["deploy", "main"] + branches: ["deploy"] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -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 diff --git a/.gitignore b/.gitignore index 581a22a..d76b553 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,8 @@ /.next/ /out/ -# production -/build +# # production +# /build # misc .DS_Store diff --git a/apis/admin.ts b/apis/admin.ts new file mode 100644 index 0000000..73a36b5 --- /dev/null +++ b/apis/admin.ts @@ -0,0 +1,25 @@ +import { CalendarDate } from "./calendar"; +import client, { ResponseBody } from "./client"; + +export interface Program { + name: string; + dueDate: CalendarDate; + openDate: CalendarDate; + location: string; + category: string; + host: string; + schedule: string; + description: string; +} + +async function postAttendanceCode(challengeIdx: number): Promise { + const { data } = await client.post(`/challenges/attendance/${challengeIdx}`); + return data; +} + +async function postProgram(body: Program): Promise { + const { data } = await client.post(`/programs`, body); + return data; +} + +export { postAttendanceCode, postProgram }; diff --git a/apis/calendar.ts b/apis/calendar.ts index 19d1183..23d1d95 100644 --- a/apis/calendar.ts +++ b/apis/calendar.ts @@ -7,6 +7,8 @@ interface GetMonthCalendarResponse extends ResponseBody { export interface MonthCalendarProps { programIdx: number; name: string; + category: string | null; + location: string | null; openDate: { year: number; month: number; @@ -19,7 +21,7 @@ export interface MonthCalendarProps { }; } -type CalendarDate = { +export type CalendarDate = { year: number; month: number; day: number; @@ -38,7 +40,7 @@ interface GetProgramDetailBody { // 챌린지 월별 조회 export const getMonthCalendar = async (): Promise => { - const response = await client.get("/programs"); + const response = await client.get("/programs/list"); // console.log("calenderData", response.data.result); return response.data.result; }; @@ -48,6 +50,5 @@ export const getProgramDetail = async ( ): Promise => { // const response = await client.get(`/programs/${programIdx}`); const response = await client.get(`/programs/2`); - // console.log("calenderDetail", response.data.result); return response.data.result; }; diff --git a/apis/challenge.ts b/apis/challenge.ts index 4eea8ac..a7e55a2 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,76 @@ export interface Challenge { attendanceRate: number; totalAttendanceRate: number; } +interface GetChallengeAdsResponse extends ResponseBody2 { + result: { + mostParticipatedChallenge: Challenge; + mostAttendancedChallenge: Challenge; + mostRecentlyStartedChallenge: Challenge; + }; +} + +interface getChallengeSearchResponse extends ResponseBody2 { + result: Challenge[]; +} + +export interface AttendanceRequestBody { + challengeIdx: number; + attendanceCode: string; +} + async function getMyChallengeList(): Promise { const { data } = await client.get(`/challenges`); return data; } -export { getMyChallengeList }; +type AttendanceDate = { + year: number; + month: number; + day: number; +}; + +export interface GetChallengeDetailBody { + attendanceDate: AttendanceDate; +} + +async function getChallengDetail(): Promise { + const response = await client.get( + `/challenges/attendance/2?year=2024&month=6`, + ); + return response.data.result; +} + +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; +} + +async function postAttendance( + body: AttendanceRequestBody, +): Promise { + const { data } = await client.post(`/challenges/attendance`, body); + return data; +} + +export { + getMyChallengeList, + getChallengeAds, + getChallengeSearch, + postNewChallenge, + postAttendance, + getChallengDetail, +}; diff --git a/apis/client.ts b/apis/client.ts index c3dbd5a..3e200f3 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); @@ -18,6 +22,18 @@ const getTokenFromLocalStorage = () => { return accessToken; }; +export const setIsAdminAtLocalStorage = (is_admin: string) => { + localStorage.setItem("is_admin", is_admin); +}; + +const getIsAdminFromLocalStorage = () => { + const isAdmin = localStorage.getItem("is_admin"); + if (!isAdmin) { + return null; + } + return isAdmin; +}; + const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, withCredentials: true, @@ -49,4 +65,4 @@ client.interceptors.request.use( ); export default client; -export type { ResponseBody }; +export type { ResponseBody, ResponseBody2 }; diff --git a/apis/hooks/admin.ts b/apis/hooks/admin.ts new file mode 100644 index 0000000..6e9d0af --- /dev/null +++ b/apis/hooks/admin.ts @@ -0,0 +1,36 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Program, postAttendanceCode, postProgram } from "../admin"; +import { useRouter } from "next/router"; + +function usePostAttendanceCode(challengeIdx: number) { + const { mutate } = useMutation({ + mutationKey: ["postAttendanceCode", challengeIdx], + mutationFn: () => postAttendanceCode(challengeIdx), + onSuccess: (data) => window.alert(`인증번호: ${data}`), + onError: () => window.alert("에러 발생. 앱 관리자에게 문의해주세요."), + }); + + return { mutate }; +} + +function usePostProgram() { + const router = useRouter(); + const queryclient = useQueryClient(); + + const { mutate } = useMutation({ + mutationKey: ["postProgram"], + mutationFn: (body: Program) => postProgram(body), + onSuccess: () => { + queryclient.invalidateQueries({ + queryKey: ["getMyChallengeList"], + }); + window.alert("프로그램이 성공적으로 등록되었습니다."); + router.push("/"); + }, + onError: () => router.push("/404"), + }); + + return { mutate }; +} + +export { usePostAttendanceCode, usePostProgram }; diff --git a/apis/hooks/challenge.ts b/apis/hooks/challenge.ts index 0ab0369..fa63eb8 100644 --- a/apis/hooks/challenge.ts +++ b/apis/hooks/challenge.ts @@ -1,5 +1,13 @@ -import { getMyChallengeList } from "../challenge"; -import { useQuery } from "@tanstack/react-query"; +import { + AttendanceRequestBody, + getChallengeAds, + getChallengeSearch, + getMyChallengeList, + postAttendance, + postNewChallenge, + getChallengDetail, +} from "../challenge"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; function useGetMyChallengeList() { const { data } = useQuery({ @@ -10,4 +18,68 @@ function useGetMyChallengeList() { return { data }; } -export { useGetMyChallengeList }; +function useGetChallengeDetail() { + const { data } = useQuery({ + queryKey: ["getChallengeDetail"], + queryFn: getChallengDetail, + }); + return { data }; +} + +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 }; +} + +function usePostAttendance() { + const queryClient = useQueryClient(); + const { mutate } = useMutation({ + mutationKey: ["postAttendance"], + mutationFn: (body: AttendanceRequestBody) => postAttendance(body), + }); + + return { mutate }; +} + +export { + useGetMyChallengeList, + useGetChallengeAds, + useGetChallengeSearch, + usePostNewChallenge, + usePostAttendance, + useGetChallengeDetail, +}; diff --git a/components/Divider.tsx b/components/Divider.tsx index 1e77b17..5d8e953 100644 --- a/components/Divider.tsx +++ b/components/Divider.tsx @@ -5,7 +5,7 @@ interface DividerProps { export default function Divider({ height }: DividerProps) { return (
); diff --git a/components/HeadFunction.tsx b/components/HeadFunction.tsx index 30c5a3f..4f91c3e 100644 --- a/components/HeadFunction.tsx +++ b/components/HeadFunction.tsx @@ -1,6 +1,5 @@ import { useRouter } from "next/router"; import FlexBox from "./Flexbox"; -import Image from "next/image"; import Head from "next/head"; import LeftArrowIcon from "@/public/svgs/LeftArrow.svg"; diff --git a/components/Input.tsx b/components/Input.tsx index 1f77be0..3c1a2eb 100644 --- a/components/Input.tsx +++ b/components/Input.tsx @@ -26,11 +26,11 @@ export default function TextInput({
onChangeText(event)} placeholder={placeholder} diff --git a/components/NavBar.tsx b/components/NavBar.tsx index 0b58248..ab19362 100644 --- a/components/NavBar.tsx +++ b/components/NavBar.tsx @@ -9,50 +9,51 @@ import FlexBox from "./Flexbox"; const NavBar = () => { const router = useRouter(); const pathname = router.pathname; - const handleNavigate = (path: string) => { router.push(path); }; - useEffect(() => { window.scrollTo(0, 0); }, []); return ( -
-
- handleNavigate("/")} - iconType="home" - > - - - handleNavigate("/calendar")} - iconType="calendar" - > - - - handleNavigate("/mypage")} - iconType="mypage" - > - - + <> +
+
+ + handleNavigate("/")} + iconType="home" + > + + + handleNavigate("/calendar")} + iconType="calendar" + > + + + handleNavigate("/mypage")} + iconType="mypage" + > + + +
-
+ ); }; diff --git a/components/TextArea.tsx b/components/TextArea.tsx index 9b152a8..77ac9c2 100644 --- a/components/TextArea.tsx +++ b/components/TextArea.tsx @@ -24,10 +24,10 @@ export default function TextArea({