This commit is contained in:
jooho
2021-10-26 09:42:40 +09:00
parent fd50dd78a0
commit 80e2e4e8b3
14 changed files with 256 additions and 40 deletions

View File

@@ -0,0 +1,36 @@
package org.egovframe.cloud.userservice.api.user.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* org.egovframe.cloud.userservice.api.user.dto.SocialUserRequestDto
*
* 소셜 사용자 요청 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/10/22
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/10/22 jooho 최초 생성
* </pre>
*/
@Getter
@NoArgsConstructor
public class SocialUserRequestDto {
/**
* 공급자
*/
private String provider;
/**
* 토큰
*/
private String token;
}

View File

@@ -0,0 +1,58 @@
package org.egovframe.cloud.userservice.api.user.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* org.egovframe.cloud.userservice.api.user.dto.SocialUserResponseDto
* <p>
* 소셜 사용자 응답 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/10/22
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/10/22 jooho 최초 생성
* </pre>
*/
@Getter
@NoArgsConstructor
public class SocialUserResponseDto {
/**
* 아이디
*/
private String id;
/**
* 이메일
*/
private String email;
/**
* 이름
*/
private String name;
/**
* 소셜 사용자 DTO 클래스 생성자
* 빌더 패턴으로 객체 생성
*
* @param id 아이디
* @param email 이메일
* @param name 이름
*/
@Builder
public SocialUserResponseDto(String id, String email, String name) {
this.id = id;
this.email = email;
this.name = name;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -36,6 +36,7 @@ const App = ({ component: Component, ...pageProps }: AppProps) => {
const router = useRouter() const router = useRouter()
const pathname = router.pathname const pathname = router.pathname
const authPage = pathname?.startsWith('/auth/') const authPage = pathname?.startsWith('/auth/')
const naverLoginCallbackPage = pathname?.startsWith('/auth/login/naver')
const errorPage = router.pathname === '/404' || router.pathname === '/_error' const errorPage = router.pathname === '/404' || router.pathname === '/_error'
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
@@ -127,7 +128,7 @@ const App = ({ component: Component, ...pageProps }: AppProps) => {
return null return null
} }
return errorPage ? ( return errorPage || naverLoginCallbackPage ? (
<Wrapper> <Wrapper>
<Component {...pageProps} /> <Component {...pageProps} />
</Wrapper> </Wrapper>

View File

@@ -1,12 +1,17 @@
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import KakaoLogin from 'react-kakao-login' import KakaoLogin from 'react-kakao-login'
import { KAKAO_JAVASCRIPT_KEY } from '@constants/env' import { KAKAO_JAVASCRIPT_KEY } from '@constants/env'
import { ISocialButton } from '@components/Buttons/GoogleLoginButton' import { ISocialButton } from '@components/Buttons/GoogleLoginButton'
import CustomConfirm, { CustomConfirmPrpps } from '@components/CustomConfirm' import CustomConfirm, { CustomConfirmPrpps } from '@components/CustomConfirm'
const KakaoLoginButton = (props: ISocialButton) => { export interface ISocialKakaoButton extends ISocialButton {
const { handleClick, confirmMessage } = props kakaoLoginMode?: string
setKakaoLoginMode?: any
}
const KakaoLoginButton = (props: ISocialKakaoButton) => {
const { handleClick, confirmMessage, kakaoLoginMode, setKakaoLoginMode } = props
const { t } = useTranslation() const { t } = useTranslation()
const [customConfirm, setCustomConfirm] = useState<CustomConfirmPrpps>({ const [customConfirm, setCustomConfirm] = useState<CustomConfirmPrpps>({
@@ -15,6 +20,23 @@ const KakaoLoginButton = (props: ISocialButton) => {
handleCancel: () => {}, handleCancel: () => {},
}) })
useEffect(() => {
// 라이브러리에서 로그인 상태를 유지하고 바꿀 수 없어서 이런 코드를..
if (kakaoLoginMode !== 'logout' || !document || !document.querySelector('#kakaoIdLogin')) {
return
}
const kakaoLoginButton = document.querySelector('#kakaoIdLogin')
// @ts-ignore
kakaoLoginButton.href = 'javascript:void(0);'
// @ts-ignore
kakaoLoginButton.click()
setKakaoLoginMode(null)
}, [kakaoLoginMode])
return ( return (
<> <>
<KakaoLogin <KakaoLogin
@@ -24,6 +46,7 @@ const KakaoLoginButton = (props: ISocialButton) => {
render={(_props: any) => ( render={(_props: any) => (
<a <a
href="#" href="#"
id="kakaoIdLogin"
className="social kakao" className="social kakao"
onClick={event => { onClick={event => {
event.preventDefault() event.preventDefault()

View File

@@ -47,6 +47,7 @@ const NaverLoginButton = (loginButtonProps: ISocialButton) => {
}) })
naverLogin.init() naverLogin.init()
if (!window.opener) { if (!window.opener) {
naver.successCallback = data => { naver.successCallback = data => {
return onSuccess(data) return onSuccess(data)
@@ -91,28 +92,28 @@ const NaverLoginButton = (loginButtonProps: ISocialButton) => {
} }
const loadScript = useCallback(() => { const loadScript = useCallback(() => {
if (mounted) { if (
if ( document &&
document && document.querySelectorAll('#naver-login-sdk').length === 0
document.querySelectorAll('#naver-login-sdk').length === 0 ) {
) { let script = document.createElement('script')
let script = document.createElement('script') script.id = 'naver-login-sdk'
script.id = 'naver-login-sdk' script.src = NAVER_ID_SDK_URL
script.src = NAVER_ID_SDK_URL script.onload = () => {
script.onload = () => { return initLoginButton()
return initLoginButton()
}
document.head.appendChild(script)
} else {
initLoginButton()
} }
document.head.appendChild(script)
} else {
initLoginButton()
} }
}, [mounted]) }, [])
useEffect(() => { useEffect(() => {
appendNaverButton() if (mounted) {
loadScript() appendNaverButton()
}, []) loadScript()
}
}, [mounted])
const handleLogin = () => { const handleLogin = () => {
if (!document || !document.querySelector('#naverIdLogin').firstChild) { if (!document || !document.querySelector('#naverIdLogin').firstChild) {

View File

@@ -70,7 +70,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json(payload) res.status(200).json(payload)
} else { } else {
res.status(401).json({ message: 'Invalid Credentials 🥺' }) res.status(result.status).json({ message: 'Invalid Credentials 🥺' })
} }
} else { } else {
res.status(401).json({ message: 'Invalid Credentials 🥺' }) res.status(401).json({ message: 'Invalid Credentials 🥺' })

View File

@@ -2,12 +2,13 @@ import CustomAlert, { CustomAlertPrpps } from '@components/CustomAlert'
import { DLWrapper } from '@components/WriteDLFields' import { DLWrapper } from '@components/WriteDLFields'
import { makeStyles, Theme } from '@material-ui/core/styles' import { makeStyles, Theme } from '@material-ui/core/styles'
import Alert from '@material-ui/lab/Alert' import Alert from '@material-ui/lab/Alert'
import { userService } from '@service' import { ISocialUser, userService } from '@service'
import { format, isValidPassword } from '@utils' import { format, isValidPassword } from '@utils'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import React, { createRef, useState } from 'react' import React, { createRef, useEffect, useState } from 'react'
import { Controller, useForm } from 'react-hook-form' import { Controller, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { GetServerSideProps } from 'next'
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
alert: { alert: {
@@ -20,9 +21,17 @@ interface IUserForm {
password: string password: string
passwordConfirm: string passwordConfirm: string
userName: string userName: string
provider?: string
token?: string
} }
const Form = () => { interface FormProps {
socialUser: ISocialUser
}
const Form = (props: FormProps) => {
const { socialUser } = props
const router = useRouter() const router = useRouter()
const classes = useStyles() const classes = useStyles()
const { t } = useTranslation() const { t } = useTranslation()
@@ -44,10 +53,12 @@ const Form = () => {
// form hook // form hook
const methods = useForm<IUserForm>({ const methods = useForm<IUserForm>({
defaultValues: { defaultValues: {
email: '', email: socialUser.email || '',
password: '', password: '',
passwordConfirm: '', passwordConfirm: '',
userName: '', userName: socialUser.name || '',
provider: router.query.provider as string,
token: router.query.token as string,
}, },
}) })
const { const {
@@ -67,6 +78,14 @@ const Form = () => {
}) })
} }
useEffect(() => {
if (socialUser) {
if (socialUser.name) {
}
}
}, [socialUser])
// 이메일중복확인 // 이메일중복확인
const handleCheckEmail = event => { const handleCheckEmail = event => {
event.preventDefault() event.preventDefault()
@@ -156,6 +175,7 @@ const Form = () => {
<input <input
ref={emailRef} ref={emailRef}
type="text" type="text"
readOnly={/*typeof socialUser.email !== 'undefined' && socialUser.email !== null*/false}
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
placeholder={t('user.email')} placeholder={t('user.email')}
@@ -264,6 +284,7 @@ const Form = () => {
> >
<input <input
type="text" type="text"
readOnly={/*typeof socialUser.name !== 'undefined' && socialUser.name !== null*/false}
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
placeholder={t('label.title.name')} placeholder={t('label.title.name')}
@@ -310,4 +331,33 @@ const Form = () => {
) )
} }
export const getServerSideProps: GetServerSideProps = async context => {
const provider = context.query.provider as string
const token = context.query.token as string
let socialUser = {}
try {
if (provider && token) {
const result = await userService.social(provider, token)
if (result) {
socialUser = (await result.data) as ISocialUser
}
}
} catch (error) {
console.error(`social item query error ${error.message}`)
if (error.response?.data?.code === 'E003') {
return {
notFound: true,
}
}
}
return {
props: {
socialUser
},
}
}
export default Form export default Form

View File

@@ -61,7 +61,7 @@ const Join = ({ policyPP, policyTOS }: IJoinProps) => {
return return
} }
router.push('/auth/join/form') router.push(`/auth/join/form?provider=${router.query.provider}&token=${router.query.token}`)
} }
return ( return (

View File

@@ -9,17 +9,26 @@ import Loader from '@components/Loader'
import useUser from '@hooks/useUser' import useUser from '@hooks/useUser'
import { ILogin, loginSerivce } from '@service' import { ILogin, loginSerivce } from '@service'
import { userAtom } from '@stores' import { userAtom } from '@stores'
import Router from 'next/router' import Router, { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useRecoilValue } from 'recoil' import { useRecoilValue } from 'recoil'
import CustomConfirm, { CustomConfirmPrpps } from '@components/CustomConfirm'
interface AlertProps extends CustomConfirmPrpps {
message: string
}
const Login = () => { const Login = () => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter()
const { isLogin, mutate } = useUser() const { isLogin, mutate } = useUser()
const user = useRecoilValue(userAtom) const user = useRecoilValue(userAtom)
const [customConfirm, setCustomConfirm] = useState<AlertProps | null>(null)
const [errorState, setErrorState] = useState<string | null>(null) const [errorState, setErrorState] = useState<string | null>(null)
const [kakaoLoginMode, setKakaoLoginMode] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
if (isLogin && user) { if (isLogin && user) {
@@ -42,7 +51,29 @@ const Login = () => {
setErrorState(result) setErrorState(result)
} }
} catch (error) { } catch (error) {
setErrorState(t('err.user.login')) if (error === 'join') {
setCustomConfirm({
open: true,
message: t('msg.confirm.join.social'),
handleConfirm: () => {
setCustomConfirm({
...customConfirm,
open: false,
})
// recoil 쓰려했는데 회원가입에 스탭이 있어서 진행중에 새로고침하면 상태가 삭제되면서 일반회원으로 가입될 수 있어서 소셜 정보를 파라미터로 넘김
router.push(`/auth/join?provider=${data.provider}&token=${data.token}`)
},
handleCancel: () => {
if (data.provider === 'kakao') {
setKakaoLoginMode('logout')
}
setCustomConfirm({ ...customConfirm, open: false })
},
})
} else {
setErrorState(t('err.user.login'))
}
} }
} }
@@ -105,11 +136,19 @@ const Login = () => {
<span>{t('label.title.login.oauth')}</span> <span>{t('label.title.login.oauth')}</span>
</h3> </h3>
<div> <div>
<KakaoLoginButton handleClick={handleKakaoLogin} /> <KakaoLoginButton handleClick={handleKakaoLogin} kakaoLoginMode={kakaoLoginMode} setKakaoLoginMode={setKakaoLoginMode} />
<NaverLoginButton handleClick={handleNaverLogin} /> <NaverLoginButton handleClick={handleNaverLogin} />
<GoogleLoginButton handleClick={handleGoogleLogin} /> <GoogleLoginButton handleClick={handleGoogleLogin} />
</div> </div>
</article> </article>
{customConfirm && (
<CustomConfirm
handleConfirm={customConfirm.handleConfirm}
handleCancel={customConfirm.handleCancel}
contentText={customConfirm.message}
open={customConfirm.open}
/>
)}
</section> </section>
) )
} }

View File

@@ -14,7 +14,7 @@ const LoginNaver = () => {
return <></> */ return <></> */
return <NaverLoginButton handleClick={() => {}} /> return <div style={{ display: 'none' }}><NaverLoginButton handleClick={() => {}} /></div>
} }
export default LoginNaver export default LoginNaver

View File

@@ -150,9 +150,8 @@ const UserInfo = () => {
token: response.response.access_token, token: response.response.access_token,
}, },
}) })
setErrorState(null)
} else { } else {
setErrorState(t('err.user.login.social')) setErrorState({ message: t('err.user.login.social') })
} }
} }
@@ -173,9 +172,8 @@ const UserInfo = () => {
token: response.accessToken, token: response.accessToken,
}, },
}) })
setErrorState(null)
} else { } else {
setErrorState(t('err.user.login.social')) setErrorState({ message: t('err.user.login.social') })
} }
} }
@@ -193,9 +191,8 @@ const UserInfo = () => {
token: response.tokenId, token: response.tokenId,
}, },
}) })
setErrorState(null)
} else { } else {
setErrorState(t('err.user.login.social')) setErrorState({ message: t('err.user.login.social') })
} }
} }

View File

@@ -34,6 +34,8 @@ export const loginSerivce = {
if (result.status === 200) { if (result.status === 200) {
onSuccessLogin(await result.json()) onSuccessLogin(await result.json())
resolve('success') resolve('success')
} if (result.status === 412) {
reject('join')
} else { } else {
reject('noAuth') reject('noAuth')
} }

View File

@@ -38,10 +38,19 @@ interface IUserUpdate extends IVerification {
userName: string userName: string
} }
// 소셜 정보
export interface ISocialUser {
id: string
email: string
name: string
}
/** /**
* 사용자 관리 서비스 * 사용자 관리 서비스
*/ */
export const userService = { export const userService = {
social: (provider: string, token: string) =>
axios.post(`${USER_URL}/social`, { provider, token }),
existsEmail: (email: string, userId?: string) => existsEmail: (email: string, userId?: string) =>
new Promise<boolean>((resolve, rejects) => { new Promise<boolean>((resolve, rejects) => {
axios axios