From 7fe3169628cebacb1eef3f30e62099878c5eb314 Mon Sep 17 00:00:00 2001 From: Minhyung Cho Date: Sun, 21 Sep 2025 01:12:30 +0900 Subject: [PATCH 1/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=EC=BD=9C=EB=B0=B1=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/kokomen-webview/src/routeTree.gen.ts | 21 ++ .../src/routes/login/google.callback.tsx | 207 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 apps/kokomen-webview/src/routes/login/google.callback.tsx diff --git a/apps/kokomen-webview/src/routeTree.gen.ts b/apps/kokomen-webview/src/routeTree.gen.ts index 1a84e019..580f5171 100644 --- a/apps/kokomen-webview/src/routeTree.gen.ts +++ b/apps/kokomen-webview/src/routeTree.gen.ts @@ -20,6 +20,7 @@ import { Route as LoginProfileRouteImport } from './routes/login/profile' import { Route as LoginCallbackRouteImport } from './routes/login/callback' import { Route as InterviewsInterviewIdIndexRouteImport } from './routes/interviews/$interviewId/index' import { Route as MembersInterviewsInterviewIdRouteImport } from './routes/members/interviews.$interviewId' +import { Route as LoginGoogleCallbackRouteImport } from './routes/login/google.callback' import { Route as InterviewsInterviewIdResultRouteImport } from './routes/interviews/$interviewId/result' const IndexRoute = IndexRouteImport.update({ @@ -79,6 +80,11 @@ const MembersInterviewsInterviewIdRoute = path: '/members/interviews/$interviewId', getParentRoute: () => rootRouteImport, } as any) +const LoginGoogleCallbackRoute = LoginGoogleCallbackRouteImport.update({ + id: '/login/google/callback', + path: '/login/google/callback', + getParentRoute: () => rootRouteImport, +} as any) const InterviewsInterviewIdResultRoute = InterviewsInterviewIdResultRouteImport.update({ id: '/interviews/$interviewId/result', @@ -97,6 +103,7 @@ export interface FileRoutesByFullPath { '/interviews': typeof InterviewsIndexRoute '/login': typeof LoginIndexRoute '/interviews/$interviewId/result': typeof InterviewsInterviewIdResultRoute + '/login/google/callback': typeof LoginGoogleCallbackRoute '/members/interviews/$interviewId': typeof MembersInterviewsInterviewIdRoute '/interviews/$interviewId': typeof InterviewsInterviewIdIndexRoute } @@ -111,6 +118,7 @@ export interface FileRoutesByTo { '/interviews': typeof InterviewsIndexRoute '/login': typeof LoginIndexRoute '/interviews/$interviewId/result': typeof InterviewsInterviewIdResultRoute + '/login/google/callback': typeof LoginGoogleCallbackRoute '/members/interviews/$interviewId': typeof MembersInterviewsInterviewIdRoute '/interviews/$interviewId': typeof InterviewsInterviewIdIndexRoute } @@ -126,6 +134,7 @@ export interface FileRoutesById { '/interviews/': typeof InterviewsIndexRoute '/login/': typeof LoginIndexRoute '/interviews/$interviewId/result': typeof InterviewsInterviewIdResultRoute + '/login/google/callback': typeof LoginGoogleCallbackRoute '/members/interviews/$interviewId': typeof MembersInterviewsInterviewIdRoute '/interviews/$interviewId/': typeof InterviewsInterviewIdIndexRoute } @@ -142,6 +151,7 @@ export interface FileRouteTypes { | '/interviews' | '/login' | '/interviews/$interviewId/result' + | '/login/google/callback' | '/members/interviews/$interviewId' | '/interviews/$interviewId' fileRoutesByTo: FileRoutesByTo @@ -156,6 +166,7 @@ export interface FileRouteTypes { | '/interviews' | '/login' | '/interviews/$interviewId/result' + | '/login/google/callback' | '/members/interviews/$interviewId' | '/interviews/$interviewId' id: @@ -170,6 +181,7 @@ export interface FileRouteTypes { | '/interviews/' | '/login/' | '/interviews/$interviewId/result' + | '/login/google/callback' | '/members/interviews/$interviewId' | '/interviews/$interviewId/' fileRoutesById: FileRoutesById @@ -185,6 +197,7 @@ export interface RootRouteChildren { InterviewsIndexRoute: typeof InterviewsIndexRoute LoginIndexRoute: typeof LoginIndexRoute InterviewsInterviewIdResultRoute: typeof InterviewsInterviewIdResultRoute + LoginGoogleCallbackRoute: typeof LoginGoogleCallbackRoute MembersInterviewsInterviewIdRoute: typeof MembersInterviewsInterviewIdRoute InterviewsInterviewIdIndexRoute: typeof InterviewsInterviewIdIndexRoute } @@ -268,6 +281,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof MembersInterviewsInterviewIdRouteImport parentRoute: typeof rootRouteImport } + '/login/google/callback': { + id: '/login/google/callback' + path: '/login/google/callback' + fullPath: '/login/google/callback' + preLoaderRoute: typeof LoginGoogleCallbackRouteImport + parentRoute: typeof rootRouteImport + } '/interviews/$interviewId/result': { id: '/interviews/$interviewId/result' path: '/interviews/$interviewId/result' @@ -289,6 +309,7 @@ const rootRouteChildren: RootRouteChildren = { InterviewsIndexRoute: InterviewsIndexRoute, LoginIndexRoute: LoginIndexRoute, InterviewsInterviewIdResultRoute: InterviewsInterviewIdResultRoute, + LoginGoogleCallbackRoute: LoginGoogleCallbackRoute, MembersInterviewsInterviewIdRoute: MembersInterviewsInterviewIdRoute, InterviewsInterviewIdIndexRoute: InterviewsInterviewIdIndexRoute, } diff --git a/apps/kokomen-webview/src/routes/login/google.callback.tsx b/apps/kokomen-webview/src/routes/login/google.callback.tsx new file mode 100644 index 00000000..f2c26401 --- /dev/null +++ b/apps/kokomen-webview/src/routes/login/google.callback.tsx @@ -0,0 +1,207 @@ +import { postGoogleAuthorizationCode } from "@/domains/auth/api"; +import { useAuthStore } from "@/store"; +import { useMutation } from "@tanstack/react-query"; +import { + createFileRoute, + Link, + useRouter, + useSearch +} from "@tanstack/react-router"; +import React, { useEffect } from "react"; + +// eslint-disable-next-line @rushstack/typedef-var +export const Route = createFileRoute("/login/google/callback")({ + component: RouteComponent +}); + +const ROOT_URI: string = "/interviews"; +function RouteComponent(): React.ReactNode { + const router = useRouter(); + const { code, state } = useSearch({ + from: "/login/google/callback", + select: (search) => search as { code?: string; state?: string } + }); + + const authMutation = useMutation({ + mutationFn: ({ + code, + redirectUri + }: { + code: string; + redirectUri: string; + }) => postGoogleAuthorizationCode(code, redirectUri), + + onSuccess: ({ data }) => { + useAuthStore.getState().setAuth(data); + const redirectTo = state === "/" ? ROOT_URI : state || ROOT_URI; + if (!data.profile_completed) { + router.navigate({ to: `/login/profile?state=${redirectTo}` }); + return; + } + router.navigate({ to: redirectTo, replace: true }); + }, + + onError: (error) => { + console.error("로그인 실패:", error); + }, + retry: 1 + }); + + useEffect(() => { + if (authMutation.isPending || authMutation.error || authMutation.isSuccess) + return; + + const redirectUri = `${import.meta.env.VITE_BASE_URL}/login/google/callback`; + + authMutation.mutate({ + code: code as string, + redirectUri + }); + }, [code, state, authMutation]); + + // 로딩 상태 + if (authMutation.isPending) { + return ( +
+
+
+
+ + + + +
+

+ 로그인 처리 중... +

+

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

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

+ 로그인 완료! +

+

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

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

+ 로그인 실패 +

+

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

+
+ + + 로그인 페이지로 돌아가기 + + + 홈으로 이동 + +
+
+
+
+ ); + } + + return null; +} From 0b85d130beb46ba890deb110ca2708308fae6d84 Mon Sep 17 00:00:00 2001 From: Minhyung Cho Date: Sun, 21 Sep 2025 01:12:41 +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=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/kokomen-webview/src/domains/auth/api/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/kokomen-webview/src/domains/auth/api/index.ts b/apps/kokomen-webview/src/domains/auth/api/index.ts index 89b5e090..b4ec24fd 100644 --- a/apps/kokomen-webview/src/domains/auth/api/index.ts +++ b/apps/kokomen-webview/src/domains/auth/api/index.ts @@ -16,6 +16,16 @@ const postAuthorizationCode = async ( }); }; +const postGoogleAuthorizationCode = async ( + code: string, + redirectUri: string +): AxiosPromise => { + return authServerInstance.post(`/auth/google-login`, { + code, + redirect_uri: redirectUri + }); +}; + const getUserInfo = async (): AxiosPromise => { return authServerInstance.get(`/members/me/profile`); }; @@ -42,6 +52,7 @@ const deleteUser = async (): AxiosPromise => { export { postAuthorizationCode, + postGoogleAuthorizationCode, getUserInfo, logout, updateUserProfile, From fd12c303f6511bce540d2a4541e3e8fa0778ef3c Mon Sep 17 00:00:00 2001 From: Minhyung Cho Date: Sun, 21 Sep 2025 01:12:51 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/kokomen-webview/src/main.tsx | 4 +- .../src/routes/login/index.tsx | 78 ++++++++++++------- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/apps/kokomen-webview/src/main.tsx b/apps/kokomen-webview/src/main.tsx index 73f7a8c3..68addc78 100644 --- a/apps/kokomen-webview/src/main.tsx +++ b/apps/kokomen-webview/src/main.tsx @@ -56,8 +56,8 @@ function AuthRouter() { return ; } -posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, { - api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, +posthog.init(import.meta.env.VITE_POSTHOG_KEY, { + api_host: import.meta.env.VITE_POSTHOG_HOST, defaults: "2025-05-24" }); diff --git a/apps/kokomen-webview/src/routes/login/index.tsx b/apps/kokomen-webview/src/routes/login/index.tsx index 9e972c0d..224b3e9b 100644 --- a/apps/kokomen-webview/src/routes/login/index.tsx +++ b/apps/kokomen-webview/src/routes/login/index.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, useSearch } from "@tanstack/react-router"; +import { createFileRoute, Link, useSearch } from "@tanstack/react-router"; import React from "react"; // eslint-disable-next-line @rushstack/typedef-var @@ -13,15 +13,19 @@ function RouteComponent(): React.ReactNode { }); const redirectTo = `&state=${query.redirectTo ?? "/"}`; const redirectUri = `${import.meta.env.VITE_BASE_URL}/login/callback${redirectTo}`; + const googleRedirectUri = `${import.meta.env.VITE_BASE_URL}/login/google/callback${redirectTo}`; return ( <> -
+
{/* 로고 및 헤더 */} -
-

로그인

+
+ 꼬꼬면 로고 +

+ 꼬꼬면에 로그인 +

- 로그인하고 AI 모의 면접 서비스를 체험해보세요! + 면접 연습을 시작하려면 로그인이 필요합니다

@@ -30,49 +34,71 @@ function RouteComponent(): React.ReactNode { href={`${import.meta.env.VITE_API_BASE_URL}/auth/kakao-login?redirectUri=${redirectUri}`} className="w-full flex items-center justify-center px-4 py-3 border border-transparent rounded-xl text-base font-medium text-black bg-[#FEE500] hover:bg-[#FFEB3B] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-400 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-md hover:shadow-lg" > -
- +
+ - 카카오로 시작하기 + 카카오로 시작하기 +
+ + +
+ + + + + + + + 구글로 시작하기

- 로그인 시 꼬꼬면의{" "} - 서비스 이용약관 - - 과{" "} - + 과 + 개인정보 처리방침 - + 에 동의하게 됩니다.

- - {/* 하단 정보 */} -
-

- 처음 방문하시나요?{" "} - - 카카오로 간편하게 가입하세요 - -

-
From cf1809d9c9b3c3e2f2c66b1dc55914c49834010a Mon Sep 17 00:00:00 2001 From: Minhyung Cho Date: Sun, 21 Sep 2025 01:20:22 +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=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=94=94=EB=A0=89=EC=85=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/routes/login/google.callback.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/kokomen-webview/src/routes/login/google.callback.tsx b/apps/kokomen-webview/src/routes/login/google.callback.tsx index f2c26401..c54f9056 100644 --- a/apps/kokomen-webview/src/routes/login/google.callback.tsx +++ b/apps/kokomen-webview/src/routes/login/google.callback.tsx @@ -33,7 +33,7 @@ function RouteComponent(): React.ReactNode { onSuccess: ({ data }) => { useAuthStore.getState().setAuth(data); - const redirectTo = state === "/" ? ROOT_URI : state || ROOT_URI; + const redirectTo = state && state !== "/" ? state : ROOT_URI; if (!data.profile_completed) { router.navigate({ to: `/login/profile?state=${redirectTo}` }); return; @@ -48,7 +48,12 @@ function RouteComponent(): React.ReactNode { }); useEffect(() => { - if (authMutation.isPending || authMutation.error || authMutation.isSuccess) + if ( + !code || + authMutation.isPending || + authMutation.error || + authMutation.isSuccess + ) return; const redirectUri = `${import.meta.env.VITE_BASE_URL}/login/google/callback`; @@ -173,7 +178,7 @@ function RouteComponent(): React.ReactNode {