diff --git a/app/(route)/(index)/not-found.tsx b/app/(route)/(index)/not-found.tsx deleted file mode 100644 index 4657016..0000000 --- a/app/(route)/(index)/not-found.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function NotFound() { - return
100dvh
-} diff --git a/app/(route)/challenge/_components/ChallengeFormDialog.tsx b/app/(route)/challenge/_components/ChallengeFormDialog.tsx index 34332bb..f0a64ca 100644 --- a/app/(route)/challenge/_components/ChallengeFormDialog.tsx +++ b/app/(route)/challenge/_components/ChallengeFormDialog.tsx @@ -3,23 +3,79 @@ import { useState } from 'react' import * as DialogPrimitive from '@radix-ui/react-dialog' +import { type SubmitHandler, useForm, Controller } from 'react-hook-form' import AddIcon from '@/app/_components/icons/AddIcon' import AlertIcon from '@/app/_components/icons/AlertIcon' import BlackCloseIcon from '@/app/_components/icons/BlackCloseIcon' +import CheckBoxIcon from '@/app/_components/icons/CheckBoxIcon' +import { useUserContext } from '@/app/_components/providers/UserProvider' import { Dialog, DialogClose, DialogContent, DialogTrigger } from '@/app/_components/shared/dialog' import { cn } from '@/app/_styles/utils' import { share } from '../_utils' +import { useCreateChallengeMutation } from '../queries' import ChallengeForm from './challenge-form-input' -type Category = '자기계발' | '생활습관' | '공부' | '운동' | '기타' | '선택안함' +type Category = '자기계발' | '생활습관' | '공부' | '운동' | '기타' const CATEGORIES: Category[] = ['자기계발', '생활습관', '공부', '운동', '기타'] +const categoryObject = { + 자기계발: 'SELF_DEVELOPMENT', + 생활습관: 'LIFE_STYLES', + 공부: 'STUDY', + 운동: 'WORKOUT', + 기타: 'ETC', +} as const + +interface Inputs { + challengeName: string + authenticationMethod: string + reward: string +} + export default function ChallengeFormDialog() { - const [selectedCategory, setSelectedCategory] = useState('선택안함') + const [selectedCategory, setSelectedCategory] = useState('선택안함') + const [isChecked, setIsChecked] = useState({ + check1: false, + check2: false, + }) + + const createChallengeMutation = useCreateChallengeMutation() + const { user } = useUserContext() + + const { handleSubmit, watch, control } = useForm({ + mode: 'onChange', + defaultValues: { + challengeName: '', + authenticationMethod: '', + reward: '', + }, + }) + + const onSubmit: SubmitHandler = ({ challengeName, authenticationMethod, reward }) => { + createChallengeMutation.mutate( + { + name: challengeName, + authenticationMethod, + reward, + category: categoryObject[selectedCategory as Category], + }, + { + onSuccess: ({ data: { challengeId } }) => { + console.log('challengeId', challengeId) + share.kakao({ + title: `${user.nickname}님이 초대장을 보냈어요!`, + description: '1:1 목표 매칭 서비스', + imageUrl: 'https://dodals3.s3.ap-northeast-2.amazonaws.com/asset/dodaldodal_square.png', + link: `https://dodaldodal-frontend-vercel.app/challenge/${challengeId}`, + }) + }, + } + ) + } return ( @@ -29,7 +85,10 @@ export default function ChallengeFormDialog() { -
+
@@ -42,7 +101,22 @@ export default function ChallengeFormDialog() {
- + ( + { + onChange(e.target.value.slice(0, 12)) + }} + maxLength={12} + /> + )} + />
@@ -50,6 +124,7 @@ export default function ChallengeFormDialog() { return (
-
+ {isChecked.check1 ? ( + + ) : ( +
-
+ {isChecked.check2 ? ( + + ) : ( +
@@ -99,18 +239,19 @@ export default function ChallengeFormDialog() { 'mx-auto min-h-[60px] w-[240px] rounded-lg bg-[#482BD9] text-center', 'disabled:bg-[#A6A6A6]' )} - onClick={() => { - share.kakao({ - title: '불주먹123님이 초대장을 보냈어요!', - description: '1:1 목표 매칭 서비스', - imageUrl: 'https://dodals3.s3.ap-northeast-2.amazonaws.com/asset/dodaldodal_square.png', - link: 'https://dodaldodal-frontend-vercel.app', - }) - }} + type='submit' + disabled={ + watch('challengeName')?.length < 1 || + watch('authenticationMethod')?.length < 1 || + watch('reward')?.length < 1 || + selectedCategory === '선택안함' || + !isChecked.check1 || + !isChecked.check2 + } > 함께할 친구 초대하기 -
+
) diff --git a/app/(route)/challenge/_components/challenge-form-input.tsx b/app/(route)/challenge/_components/challenge-form-input.tsx index 4f41543..c6da9e6 100644 --- a/app/(route)/challenge/_components/challenge-form-input.tsx +++ b/app/(route)/challenge/_components/challenge-form-input.tsx @@ -12,10 +12,10 @@ function Title({ title }: ChallengeFormTitleProps) { interface ChallengeFormInputProps extends React.HTMLProps { currentLength: number - maxLength: number + limitLength: number } -function Input({ currentLength, maxLength, ...props }: ChallengeFormInputProps) { +function Input({ currentLength, limitLength, ...props }: ChallengeFormInputProps) { return (
- {currentLength}/{maxLength} + {currentLength}/{limitLength}
) diff --git a/app/(route)/challenge/_utils/index.ts b/app/(route)/challenge/_utils/index.ts index 9b94ba1..8895c34 100644 --- a/app/(route)/challenge/_utils/index.ts +++ b/app/(route)/challenge/_utils/index.ts @@ -12,9 +12,9 @@ export const share = { }) => { const { Kakao } = window if (!Kakao.isInitialized()) { - console.log('init') Kakao.init('f0146584008c5fad855abb3cfad073d7') } + Kakao.Share.sendDefault({ objectType: 'feed', content: { diff --git a/app/(route)/challenge/layout.tsx b/app/(route)/challenge/layout.tsx index 5fa691f..db511d7 100644 --- a/app/(route)/challenge/layout.tsx +++ b/app/(route)/challenge/layout.tsx @@ -1,6 +1,7 @@ 'use client' import { useRouter } from 'next/navigation' +import Script from 'next/script' import BottomNavigation from '@/app/_components/shared/bottom-navigation' import Header from '@/app/_components/shared/header' @@ -21,12 +22,11 @@ export default function Layout({ children }: { children: React.ReactNode }) { {children} - + > ) } diff --git a/app/(route)/challenge/queries.ts b/app/(route)/challenge/queries.ts new file mode 100644 index 0000000..a7338c8 --- /dev/null +++ b/app/(route)/challenge/queries.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query' + +import { createChallenge } from '@/app/_service/challenge' + +export const useCreateChallengeMutation = () => { + return useMutation({ + mutationFn: createChallenge, + }) +} diff --git a/app/_components/icons/CheckBoxIcon.tsx b/app/_components/icons/CheckBoxIcon.tsx new file mode 100644 index 0000000..0739c7e --- /dev/null +++ b/app/_components/icons/CheckBoxIcon.tsx @@ -0,0 +1,11 @@ +export default function CheckBoxIcon() { + return ( + + + + + ) +} diff --git a/app/_components/providers/CoreProvider.tsx b/app/_components/providers/CoreProvider.tsx index d18e1a8..fcbdcc8 100644 --- a/app/_components/providers/CoreProvider.tsx +++ b/app/_components/providers/CoreProvider.tsx @@ -4,13 +4,16 @@ import { useAxiosInterceptor } from '@/app/_hooks/useAxiosInterceptor' import AxiosProvider from './AxiosInterceptorProvider' import QueryClientProvider from './QueryClientProvider' +import { UserContextProvider } from './UserProvider' export default function CoreProvider({ children }: { children: React.ReactNode }) { useAxiosInterceptor() return ( - {children} + + {children} + ) } diff --git a/app/_components/providers/UserProvider.tsx b/app/_components/providers/UserProvider.tsx new file mode 100644 index 0000000..13926ea --- /dev/null +++ b/app/_components/providers/UserProvider.tsx @@ -0,0 +1,57 @@ +'use client' + +import { usePathname, useRouter } from 'next/navigation' + +import { createContext, useContext, useEffect } from 'react' + +import { useQuery } from '@tanstack/react-query' + +import { ROUTE } from '@/app/_constants/route' +import { getUser } from '@/app/_service/auth' +import { type User } from '@/app/_types/user' + +interface UserContextType { + user: User +} + +export const UserContext = createContext(null) + +export const useUserContext = () => { + const userContext = useContext(UserContext) + + if (userContext === null || userContext === undefined) { + throw new Error('UserContext: No value provided.') + } + + return userContext +} + +export const UserContextProvider = ({ children }: { children: React.ReactNode }) => { + const pathname = usePathname() + const router = useRouter() + + const userQuery = useQuery({ + queryKey: ['user'], + queryFn: () => { + return getUser() + }, + }) + + useEffect(() => { + const isPublicPath = + pathname === ROUTE.LOGIN || + pathname === ROUTE.SIGN_UP || + pathname === ROUTE.OAUTH.KAKAO.CALLBACK || + pathname.includes('/invitation') + + if (userQuery.isError && !isPublicPath) { + router.push(ROUTE.LOGIN) + } + }, [userQuery.isError, pathname]) + + if (!userQuery.isSuccess) { + return null + } + + return {children} +} diff --git a/app/_service/auth/index.ts b/app/_service/auth/index.ts index 14803e0..157d946 100644 --- a/app/_service/auth/index.ts +++ b/app/_service/auth/index.ts @@ -1,5 +1,7 @@ import axios from 'axios' +import { type User } from '@/app/_types/user' + import api from '../core/api' import { @@ -13,8 +15,8 @@ export const getTokenByAuthorizationCode = async ({ code }: GetTokenFromKakaoPar return await axios.get(`/oauth/callback/kakao/api?code=${code}`) } -export const getUsers = () => { - return api.get('/users') +export const getUser = () => { + return api.get('/users') } export const login = ({ accessToken }: { accessToken: string }) => { diff --git a/app/_service/challenge/challenge.types.ts b/app/_service/challenge/challenge.types.ts index 0ecee31..792d2c9 100644 --- a/app/_service/challenge/challenge.types.ts +++ b/app/_service/challenge/challenge.types.ts @@ -5,3 +5,16 @@ export interface GetTodayStatus { explorationCount: number certificatedCount: number } + +export type Category = 'LIFE_STYLES' | 'SELF_DEVELOPMENT' | 'STUDY' | 'WORKOUT' | 'ETC' + +export interface CreateChallengeParams { + name: string + category: Category + authenticationMethod: string + reward: string +} + +export interface CreateChallengeResponse { + challengeId: number +} diff --git a/app/_service/challenge/index.ts b/app/_service/challenge/index.ts index 5cddbfb..dc1bfe5 100644 --- a/app/_service/challenge/index.ts +++ b/app/_service/challenge/index.ts @@ -1,7 +1,16 @@ import api from '../core/api' -import { type GetTodayStatus } from './challenge.types' +import { type CreateChallengeResponse, type CreateChallengeParams, type GetTodayStatus } from './challenge.types' export const getTodayStatus = () => { return api.get('/challenges/today') } + +export const createChallenge = ({ name, category, authenticationMethod, reward }: CreateChallengeParams) => { + return api.post('/challenges', { + name, + category, + authenticationMethod, + reward, + }) +} diff --git a/app/_styles/globals.css b/app/_styles/globals.css index a108f25..78bea4a 100644 --- a/app/_styles/globals.css +++ b/app/_styles/globals.css @@ -25,7 +25,7 @@ input { } .DialogContent { - z-index: 51; + z-index: 50; display: flex; justify-content: center; align-items: center; diff --git a/app/_types/user.ts b/app/_types/user.ts new file mode 100644 index 0000000..2a4667d --- /dev/null +++ b/app/_types/user.ts @@ -0,0 +1,9 @@ +import { type Champion } from '../_service/auth/auth.types' + +export interface User { + id: number + nickname: string + provider: 'KAKAO' | 'GOOGLE' + provider_id: string + champion: Champion +} diff --git a/app/(route)/layout.tsx b/app/layout.tsx similarity index 95% rename from app/(route)/layout.tsx rename to app/layout.tsx index b0ab3b5..0f808ae 100644 --- a/app/(route)/layout.tsx +++ b/app/layout.tsx @@ -8,7 +8,7 @@ import CoreProvider from '@/app/_components/providers/CoreProvider' import { cn } from '@/app/_styles/utils' const myFont = localFont({ - src: '../_assets/fonts/PretendardVariable.woff2', + src: '../app/_assets/fonts/PretendardVariable.woff2', display: 'swap', }) diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..cc3f977 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,26 @@ +import Image from 'next/image' +import Link from 'next/link' + +import Header from './_components/shared/header' +import { ROUTE } from './_constants/route' + +export default function NotFound() { + return ( +
+
+ + + +
+ + Sorry···! + 서비스 준비중이에요! +
+ ) +} diff --git a/tailwind.config.ts b/tailwind.config.ts index fa6ebae..10dca2f 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,7 +1,7 @@ import type { Config } from 'tailwindcss' const config: Config = { - content: ['./app/**/*.{js,ts,jsx,tsx,mdx}'], + content: ['./app/**/*.{js,ts,jsx,tsx,mdx}', `./app/*.{js,ts,jsx,tsx,mdx}`], future: { hoverOnlyWhenSupported: true, },