From 32e8f168e6cc711d4d1b94cf9d558232bedccb9b Mon Sep 17 00:00:00 2001 From: Minhyung Cho Date: Fri, 19 Sep 2025 21:05:11 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat=20:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EC=97=90=20GOOGLE=5FCLIENT=5FID=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-dev.yml | 1 + .github/workflows/deploy.yml | 1 + apps/client/Dockerfile | 2 ++ apps/client/env.d.ts | 1 + compose.dev.yaml | 2 ++ compose.yaml | 1 + 6 files changed, 8 insertions(+) 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/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} From 23bfb1cae6fd03b8818c226e5cf03ee598c7ead7 Mon Sep 17 00:00:00 2001 From: Minhyung Cho Date: Fri, 19 Sep 2025 21:05:30 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/src/domains/auth/api/index.ts | 15 ++ apps/client/src/pages/login/callback.tsx | 16 +- .../src/pages/login/google/callback.tsx | 226 ++++++++++++++++++ apps/client/src/pages/login/index.tsx | 39 ++- 4 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 apps/client/src/pages/login/google/callback.tsx 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..1433e4f1 --- /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 KakaoCallbackPageProps = { + code: string; + state: string; +}; + +export default function KakaoCallbackPage({ + code, + state +}: KakaoCallbackPageProps): 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 ( <> -
- +
+ - 카카오로 시작하기 + 카카오로 시작하기 +
+ + +
+ + + + + + + + 구글로 시작하기
From b52092eaab55e0dad03d7e853eaf7afa373dedc3 Mon Sep 17 00:00:00 2001 From: Minhyung Cho Date: Fri, 19 Sep 2025 21:15:36 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=BD=9C=EB=B0=B1=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=9D=98=20=ED=83=80=EC=9E=85=20=EB=B0=8F=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/src/pages/login/google/callback.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/client/src/pages/login/google/callback.tsx b/apps/client/src/pages/login/google/callback.tsx index 1433e4f1..4ecdcc87 100644 --- a/apps/client/src/pages/login/google/callback.tsx +++ b/apps/client/src/pages/login/google/callback.tsx @@ -5,7 +5,7 @@ import { JSX, useEffect } from "react"; import Link from "next/link"; import { postGoogleAuthorizationCode } from "@/domains/auth/api"; -type KakaoCallbackPageProps = { +type GoogleCallbackPageProps = { code: string; state: string; }; @@ -13,7 +13,7 @@ type KakaoCallbackPageProps = { export default function KakaoCallbackPage({ code, state -}: KakaoCallbackPageProps): JSX.Element | null { +}: GoogleCallbackPageProps): JSX.Element | null { const router = useRouter(); const authMutation = useMutation({ @@ -89,7 +89,7 @@ export default function KakaoCallbackPage({

로그인 처리 중...

-

카카오 로그인을 완료하고 있습니다

+

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

@@ -204,7 +204,7 @@ export default function KakaoCallbackPage({ export const getServerSideProps = ( context: GetServerSidePropsContext -): GetServerSidePropsResult => { +): GetServerSidePropsResult => { const { code, state } = context.query; // Authorization code가 없는 경우 From c5fc3794318c4e08bcac8a6c3a807be90ddbb610 Mon Sep 17 00:00:00 2001 From: Minhyung Cho Date: Fri, 19 Sep 2025 21:16:12 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=BD=9C=EB=B0=B1=20URI=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/src/pages/login/google/callback.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/pages/login/google/callback.tsx b/apps/client/src/pages/login/google/callback.tsx index 4ecdcc87..e5067b94 100644 --- a/apps/client/src/pages/login/google/callback.tsx +++ b/apps/client/src/pages/login/google/callback.tsx @@ -172,7 +172,7 @@ export default function KakaoCallbackPage({