Skip to content

Commit

Permalink
✨ add login & sign-up feature
Browse files Browse the repository at this point in the history
  • Loading branch information
wook-hyung committed Dec 15, 2023
1 parent 8473476 commit ab50d05
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 125 deletions.
2 changes: 0 additions & 2 deletions .env

This file was deleted.

82 changes: 0 additions & 82 deletions app/(route)/login/_components/choose-profile.tsx

This file was deleted.

23 changes: 3 additions & 20 deletions app/(route)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
'use client'

import Image from 'next/image'

import { useState } from 'react'

import { KAKAO } from '@/app/_constants/kakao'

import ChooseProfile from './_components/choose-profile'

type Step = 'login' | 'profile'

export default function Home() {
const [step, setStep] = useState<Step>('login')

if (step === 'profile') {
return <ChooseProfile />
}

return (
<main className='flex h-full min-h-screen flex-col items-center justify-between bg-login-background bg-cover bg-center bg-no-repeat pb-24 pt-28'>
<div className='flex flex-col items-center gap-5'>
<Image src='/images/logo.png' width={155} height={155} alt='도달 로고' />
<span>1:1 매칭 목표 달성 서비스</span>
</div>
<button
<a
className='flex h-11 w-[260px] items-center justify-center gap-1 rounded-xl bg-dodal-kakao p-4 text-black'
onClick={() => {
setStep('profile')
}}
// href={`https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${KAKAO.REST_API_KEY}&redirect_uri=${KAKAO.REDIRECT_URI}`}
href={`https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${KAKAO.REST_API_KEY}&redirect_uri=${KAKAO.REDIRECT_URI}`}
>
<svg xmlns='http://www.w3.org/2000/svg' width='28' height='26' viewBox='0 0 28 26' fill='none'>
<path
Expand All @@ -38,7 +21,7 @@ export default function Home() {
/>
</svg>
<span>카카오로 시작하기</span>
</button>
</a>
</main>
)
}
22 changes: 7 additions & 15 deletions app/(route)/oauth/callback/kakao/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,30 @@ import { useQuery } from '@tanstack/react-query'

import { ROUTE } from '@/app/_constants/route'
import { getTokenByAuthorizationCode } from '@/app/_service/auth'
import { setKakaoAccessToken } from '@/app/_utils/token'

import { QUERY_KEY } from './queries'

export default function KaKaoCallbackPage() {
const router = useRouter()

const searchParams = useSearchParams()
const code = searchParams.get('code')
const code = searchParams.get('code') as string

const { isSuccess, data } = useQuery({
queryKey: QUERY_KEY.KAKAO_TOKEN(code as string),
queryKey: QUERY_KEY.KAKAO_TOKEN(code),
queryFn: async () =>
await getTokenByAuthorizationCode({
code: code as string,
code,
}),
enabled: code !== null,
})

useEffect(() => {
if (isSuccess) {
router.push(ROUTE.HOME)
setKakaoAccessToken(data.data.access_token)
router.push(ROUTE.SIGN_UP)
}
}, [isSuccess])

if (!isSuccess) {
return null
}

return (
<div>
<span>{code}</span>
<span>{data.data.access_token}</span>
</div>
)
return null
}
163 changes: 163 additions & 0 deletions app/(route)/sign-up/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
'use client'

import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/navigation'

import { useEffect, useState } from 'react'

import { useForm, type SubmitHandler } from 'react-hook-form'

import { ROUTE } from '@/app/_constants/route'
import { type Champion } from '@/app/_service/auth/auth.types'
import { getKakaoAccessToken, removeKakaoAccessToken, setAccessToken } from '@/app/_utils/token'

import { useLoginQuery, useSignUpMutation } from './queries'

const PROFILES = {
DEFAULT: {
RED: '/images/profile-red-1.png',
YELLOW: '/images/profile-yellow-1.png',
GREEN: '/images/profile-green-1.png',
BLUE: '/images/profile-blue-1.png',
BEIGE: '/images/profile-beige-1.png',
PINK: '/images/profile-pink-1.png',
},
SELECTED: {
RED: '/images/profile-red-1-selected.png',
YELLOW: '/images/profile-yellow-1-selected.png',
GREEN: '/images/profile-green-1-selected.png',
BLUE: '/images/profile-blue-1-selected.png',
BEIGE: '/images/profile-beige-1-selected.png',
PINK: '/images/profile-pink-1-selected.png',
},
}

interface Inputs {
nickname: string
}

export default function Register() {
const router = useRouter()

const [selectedChampion, setSelectedChampion] = useState<Champion>('RED')

const { register, handleSubmit, watch } = useForm<Inputs>({
mode: 'onChange',
defaultValues: {
nickname: '',
},
})

const loginQuery = useLoginQuery({ accessToken: getKakaoAccessToken() })
const signUpMutation = useSignUpMutation()

const onSubmit: SubmitHandler<Inputs> = (data) => {
signUpMutation.mutate(
{
accessToken: getKakaoAccessToken(),
nickname: data.nickname,
champion: selectedChampion,
},
{
onSuccess: async () => {
const { data } = await loginQuery.refetch()

removeKakaoAccessToken()
setAccessToken(data?.data.token)

router.push(ROUTE.HOME)
},
}
)
}
const errorMessage = loginQuery.error?.response?.data.message
const invalidKakaoToken = errorMessage === '카카오 access 토큰이 유효하지 않습니다.'
const isNeedToRegister =
errorMessage === 'provider_id가 DB에 없어 회원가입 필요, JWT토큰 생성 불가. 요청에 실패했습니다.'

useEffect(() => {
if (invalidKakaoToken) {
alert('카카오 로그인이 만료되었습니다. 다시 로그인해주세요.')

router.push(ROUTE.LOGIN)
}
}, [loginQuery.isError, invalidKakaoToken])

useEffect(() => {
if (loginQuery.isSuccess) {
setAccessToken(loginQuery.data.data.token)
removeKakaoAccessToken()

router.push(ROUTE.HOME)
}
}, [loginQuery.isSuccess])

if (invalidKakaoToken || !isNeedToRegister) {
return null
}

return (
<main className='flex h-full flex-col items-center px-6 pb-16 pt-11'>
<h1 className='text-2xl font-semibold'>프로필 생성</h1>
<form className='w-full' onSubmit={handleSubmit(onSubmit)}>
<div className='flex w-full flex-col gap-6'>
<div className='mt-10 flex flex-col items-center'>
<Image
className='rounded-full bg-white'
src={PROFILES.DEFAULT[selectedChampion]}
width={187}
height={187}
alt=''
/>

<div className='mt-3 flex flex-col items-center gap-1'>
<input
className='mb-1 w-[84px] appearance-none border-0 border-b border-b-[#B8B8B8] bg-transparent p-0 text-center text-xl focus:ring-0'
maxLength={4}
defaultValue=''
{...register('nickname', {
required: true,
maxLength: 4,
pattern: /^[-a-zA-Z0-9]*$/,
onChange: (e) => {
e.target.value = e.target.value.slice(0, 4)
},
})}
/>
<span className='text-xs'>{watch('nickname').length >= 4 ? 4 : watch('nickname').length ?? 0} / 4</span>
</div>
</div>
<div className='mx-auto grid w-fit grid-cols-3 gap-x-3 gap-y-4'>
{Object.keys(PROFILES.DEFAULT).map((champion) => (
<button
key={champion}
onClick={() => {
setSelectedChampion(champion as Champion)
}}
>
<Image
className='rounded-full bg-white'
src={
selectedChampion === champion
? PROFILES.SELECTED[champion as Champion]
: PROFILES.DEFAULT[champion as Champion]
}
width={80}
height={80}
alt=''
/>
</button>
))}
</div>
<button
className='mt-28 flex h-14 w-full items-center justify-center rounded-[50px] bg-[#482BD9]'
type='submit'
>
달 탐사 시작하기
</button>
</div>
</form>
</main>
)
}
36 changes: 36 additions & 0 deletions app/(route)/sign-up/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useMutation, useQuery } from '@tanstack/react-query'
import { type AxiosError } from 'axios'

import { login, signUp } from '@/app/_service/auth'
import { type Champion, type LoginResponse } from '@/app/_service/auth/auth.types'
import { type ServerError, type ServerResponse } from '@/app/_service/core/api.types'

const QUERY_KEY = {
LOGIN: (accessToken: string) => ['login', accessToken],
}

export const useLoginQuery = ({ accessToken }: { accessToken: string }) => {
return useQuery<ServerResponse<LoginResponse>, AxiosError<ServerError>>({
queryKey: QUERY_KEY.LOGIN(accessToken),
queryFn: () => {
return login({ accessToken })
},
enabled: Boolean(accessToken),
})
}

export const useSignUpMutation = () => {
return useMutation({
mutationFn: ({
accessToken,
nickname,
champion,
}: {
accessToken: string
nickname: string
champion: Champion
}) => {
return signUp({ accessToken, nickname, champion })
},
})
}
1 change: 1 addition & 0 deletions app/_components/providers/QueryClientProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default function Providers({ children }: PropsWithChildren) {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
retry: false,
},
},
})
Expand Down
2 changes: 1 addition & 1 deletion app/_constants/kakao.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const KAKAO = {
// REST_API_KEY: '8cb6bb4a47d00e99bb5a038f9b6467fe',
REST_API_KEY: 'df0059498e461a8b0c76d9b4d537dbaa',
REDIRECT_URI: 'http://localhost:3000/oauth/callback/kakao',
REDIRECT_URI: 'http://localhost:4000/oauth/callback/kakao',
}
Loading

0 comments on commit ab50d05

Please sign in to comment.