diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index bacb1fa2..312aad1c 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -29,6 +29,7 @@ jobs: export NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} export SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} export DEV_CDN_BASE_URL=${{ secrets.DEV_CDN_BASE_URL }} + export GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID_DEV }} cd kokomen docker system prune -f diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e653934c..0ebea8cc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,6 +24,7 @@ jobs: NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} PROD_CDN_BASE_URL: ${{ secrets.PROD_CDN_BASE_URL }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID_PROD }} run: | yarn types:build diff --git a/apps/client/Dockerfile b/apps/client/Dockerfile index 483c312c..dac3cd3e 100644 --- a/apps/client/Dockerfile +++ b/apps/client/Dockerfile @@ -13,6 +13,7 @@ ARG NEXT_PUBLIC_API_BASE_URL ARG NEXT_PUBLIC_POSTHOG_KEY ARG NEXT_PUBLIC_POSTHOG_HOST ARG SENTRY_AUTH_TOKEN +ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID ARG NEXT_PUBLIC_CDN_BASE_URL ARG NEXT_PUBLIC_V2_API_BASE_URL ARG NEXT_PUBLIC_NOTIFICATION_API_BASE_URL @@ -24,6 +25,7 @@ ENV NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST ENV NEXT_PUBLIC_NOTIFICATION_API_BASE_URL=$NEXT_PUBLIC_NOTIFICATION_API_BASE_URL ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN ENV NEXT_PUBLIC_CDN_BASE_URL=$NEXT_PUBLIC_CDN_BASE_URL +ENV NEXT_PUBLIC_GOOGLE_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_CLIENT_ID RUN yarn install diff --git a/apps/client/env.d.ts b/apps/client/env.d.ts index 6f5378b9..15598be7 100644 --- a/apps/client/env.d.ts +++ b/apps/client/env.d.ts @@ -8,5 +8,6 @@ declare const process: { NEXT_PUBLIC_API_BASE_URL: string; NEXT_PUBLIC_V2_API_BASE_URL: string; NEXT_PUBLIC_CDN_BASE_URL: string; + NEXT_PUBLIC_GOOGLE_CLIENT_ID: string; }; }; diff --git a/apps/client/src/domains/auth/api/index.ts b/apps/client/src/domains/auth/api/index.ts index a0f1272c..f9c80f62 100644 --- a/apps/client/src/domains/auth/api/index.ts +++ b/apps/client/src/domains/auth/api/index.ts @@ -28,6 +28,20 @@ const postAuthorizationCode = async ( ); }; +const postGoogleAuthorizationCode = async ( + code: string, + redirectUri: string +): AxiosPromise => { + return serverInstance.post( + `/auth/google-login`, + { + code, + redirect_uri: redirectUri + }, + { withCredentials: true } + ); +}; + const getUserInfo = async ( context: GetServerSidePropsContext ): AxiosPromise => { @@ -60,6 +74,7 @@ const deleteUser = async (): AxiosPromise => { export { postAuthorizationCode, + postGoogleAuthorizationCode, getUserInfo, logout, updateUserProfile, diff --git a/apps/client/src/pages/login/callback.tsx b/apps/client/src/pages/login/callback.tsx index e9c272d6..48a0b8d2 100644 --- a/apps/client/src/pages/login/callback.tsx +++ b/apps/client/src/pages/login/callback.tsx @@ -12,14 +12,14 @@ type KakaoCallbackPageProps = { export default function KakaoCallbackPage({ code, - state, + state }: KakaoCallbackPageProps): JSX.Element | null { const router = useRouter(); const authMutation = useMutation({ mutationFn: ({ code, - redirectUri, + redirectUri }: { code: string; redirectUri: string; @@ -38,7 +38,7 @@ export default function KakaoCallbackPage({ onError: (error) => { console.error("로그인 실패:", error); }, - retry: 1, + retry: 1 }); useEffect(() => { @@ -54,7 +54,7 @@ export default function KakaoCallbackPage({ authMutation.mutate({ code: code as string, - redirectUri, + redirectUri }); }, [router.isReady, code, state, authMutation]); @@ -212,15 +212,15 @@ export const getServerSideProps = ( return { redirect: { destination: "/500", - permanent: false, - }, + permanent: false + } }; } return { props: { code: code as string, - state: (state as string) || "/", - }, + state: (state as string) || "/" + } }; }; diff --git a/apps/client/src/pages/login/google/callback.tsx b/apps/client/src/pages/login/google/callback.tsx new file mode 100644 index 00000000..e5067b94 --- /dev/null +++ b/apps/client/src/pages/login/google/callback.tsx @@ -0,0 +1,226 @@ +import { useMutation } from "@tanstack/react-query"; +import { GetServerSidePropsContext, GetServerSidePropsResult } from "next"; +import { useRouter } from "next/router"; +import { JSX, useEffect } from "react"; +import Link from "next/link"; +import { postGoogleAuthorizationCode } from "@/domains/auth/api"; + +type GoogleCallbackPageProps = { + code: string; + state: string; +}; + +export default function KakaoCallbackPage({ + code, + state +}: GoogleCallbackPageProps): JSX.Element | null { + const router = useRouter(); + + const authMutation = useMutation({ + mutationFn: ({ + code, + redirectUri + }: { + code: string; + redirectUri: string; + }) => postGoogleAuthorizationCode(code, redirectUri), + + onSuccess: ({ data }) => { + if (!data.profile_completed) { + router.replace(`/login/profile?state=${state || "/"}`); + return; + } + + const redirectTo = state || "/"; + router.replace(redirectTo); + }, + + onError: (error) => { + console.error("로그인 실패:", error); + }, + retry: 1 + }); + + useEffect(() => { + if ( + !router.isReady || + authMutation.isPending || + authMutation.error || + authMutation.isSuccess + ) + return; + + const redirectUri = `${process.env.NEXT_PUBLIC_BASE_URL}/login/google/callback`; + + authMutation.mutate({ + code: code as string, + redirectUri + }); + }, [router.isReady, code, state, authMutation]); + + // 로딩 상태 + if (authMutation.isPending) { + return ( +
+
+
+
+ + + + +
+

+ 로그인 처리 중... +

+

구글 로그인을 완료하고 있습니다

+
+
+
+ ); + } + + // 성공 상태 + if (authMutation.isSuccess) { + return ( +
+
+
+
+ + + +
+

+ 로그인 완료! +

+

+ 로그인이 성공적으로 완료되었습니다 +

+
+
+ 페이지가 곧 이동됩니다... +
+ + 지금 이동하기 + +
+
+
+
+ ); + } + + // 에러 상태 + if (authMutation.isError) { + return ( +
+
+
+
+ + + +
+

+ 로그인 실패 +

+

+ 로그인 처리 중 문제가 발생했습니다. 다시 시도해주세요. +

+
+ + + 로그인 페이지로 돌아가기 + + + 홈으로 이동 + +
+
+
+
+ ); + } + + return null; +} + +export const getServerSideProps = ( + context: GetServerSidePropsContext +): GetServerSidePropsResult => { + const { code, state } = context.query; + + // Authorization code가 없는 경우 + if (!code) { + return { + redirect: { + destination: "/500", + permanent: false + } + }; + } + + return { + props: { + code: code as string, + state: (state as string) || "/" + } + }; +}; diff --git a/apps/client/src/pages/login/index.tsx b/apps/client/src/pages/login/index.tsx index c2f3eb70..de11513e 100644 --- a/apps/client/src/pages/login/index.tsx +++ b/apps/client/src/pages/login/index.tsx @@ -10,6 +10,7 @@ export default function LoginPage(): JSX.Element { const { query } = useRouter(); const redirectTo = `&state=${query.redirectTo ? query.redirectTo : "/"}`; const redirectUri = `${process.env.NEXT_PUBLIC_BASE_URL}/login/callback${redirectTo}`; + const googleRedirectUri = `${process.env.NEXT_PUBLIC_BASE_URL}/login/google/callback${redirectTo}`; return ( <> -
- +
+ - 카카오로 시작하기 + 카카오로 시작하기 +
+ + +
+ + + + + + + + 구글로 시작하기
diff --git a/compose.dev.yaml b/compose.dev.yaml index 7b7d8a81..659e8385 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -14,12 +14,14 @@ services: NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST} NEXT_PUBLIC_CDN_BASE_URL: ${DEV_CDN_BASE_URL} SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN} + NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} environment: - NODE_ENV=production - NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY} - NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST} - SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} - NEXT_PUBLIC_CDN_BASE_URL=${DEV_CDN_BASE_URL} + - NEXT_PUBLIC_GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} container_name: kokomen-client expose: - "3000" diff --git a/compose.yaml b/compose.yaml index 6eedc5ac..c08344ad 100644 --- a/compose.yaml +++ b/compose.yaml @@ -14,6 +14,7 @@ services: NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST} NEXT_PUBLIC_CDN_BASE_URL: ${PROD_CDN_BASE_URL} SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN} + NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} environment: - NODE_ENV=production - NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}