Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions apps/client/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions apps/client/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
};
15 changes: 15 additions & 0 deletions apps/client/src/domains/auth/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ const postAuthorizationCode = async (
);
};

const postGoogleAuthorizationCode = async (
code: string,
redirectUri: string
): AxiosPromise<KakaoLoginResponse> => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

KakaoLoginResponse 타입을 구글 로그인 응답에도 사용하고 있습니다. 서버에서 동일한 형식의 응답을 반환하더라도, 타입 이름이 특정 소셜 로그인(카카오)을 가리키고 있어 혼란을 줄 수 있습니다. 향후 유지보수성을 위해 SocialLoginResponse와 같이 좀 더 일반적인 이름으로 변경하는 것을 고려해보세요.

return serverInstance.post(
`/auth/google-login`,
{
code,
redirect_uri: redirectUri
},
{ withCredentials: true }
);
};

const getUserInfo = async (
context: GetServerSidePropsContext
): AxiosPromise<UserInfo> => {
Expand Down Expand Up @@ -60,6 +74,7 @@ const deleteUser = async (): AxiosPromise<void> => {

export {
postAuthorizationCode,
postGoogleAuthorizationCode,
getUserInfo,
logout,
updateUserProfile,
Expand Down
16 changes: 8 additions & 8 deletions apps/client/src/pages/login/callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,7 +38,7 @@ export default function KakaoCallbackPage({
onError: (error) => {
console.error("로그인 실패:", error);
},
retry: 1,
retry: 1
});

useEffect(() => {
Expand All @@ -54,7 +54,7 @@ export default function KakaoCallbackPage({

authMutation.mutate({
code: code as string,
redirectUri,
redirectUri
});
}, [router.isReady, code, state, authMutation]);

Expand Down Expand Up @@ -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) || "/"
}
};
};
226 changes: 226 additions & 0 deletions apps/client/src/pages/login/google/callback.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="max-w-md w-full text-center px-4">
<div className="bg-white rounded-lg shadow-lg p-8">
<div className="mx-auto w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6">
<svg
className="animate-spin w-8 h-8 text-blue-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
로그인 처리 중...
</h2>
<p className="text-gray-600">구글 로그인을 완료하고 있습니다</p>
</div>
</div>
</div>
);
}

// 성공 상태
if (authMutation.isSuccess) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="max-w-md w-full text-center px-4">
<div className="bg-white rounded-lg shadow-lg p-8">
<div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-6">
<svg
className="w-8 h-8 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
로그인 완료!
</h2>
<p className="text-gray-600 mb-6">
로그인이 성공적으로 완료되었습니다
</p>
<div className="space-y-4">
<div className="text-sm text-gray-500">
<span>페이지가 곧 이동됩니다...</span>
</div>
<Link
href={state || "/"}
className="inline-block w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
지금 이동하기
</Link>
</div>
</div>
</div>
</div>
);
}

// 에러 상태
if (authMutation.isError) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="max-w-md w-full text-center px-4">
<div className="bg-white rounded-lg shadow-lg p-8">
<div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-6">
<svg
className="w-8 h-8 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
로그인 실패
</h2>
<p className="text-gray-600 mb-6">
로그인 처리 중 문제가 발생했습니다. 다시 시도해주세요.
</p>
<div className="space-y-3">
<button
onClick={() => {
const redirectUri = `${process.env.NEXT_PUBLIC_BASE_URL}/login/google/callback`;
authMutation.mutate({ code, redirectUri });
}}
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700 transition-colors disabled:opacity-50"
disabled={authMutation.isPending}
>
다시 시도하기
</button>
<Link
href="/login"
className="inline-block w-full bg-gray-100 text-gray-700 py-3 px-6 rounded-lg font-medium hover:bg-gray-200 transition-colors"
>
로그인 페이지로 돌아가기
</Link>
<Link
href="/"
className="inline-block text-blue-600 hover:text-blue-800 text-sm"
>
홈으로 이동
</Link>
</div>
</div>
</div>
</div>
);
}

return null;
}

export const getServerSideProps = (
context: GetServerSidePropsContext
): GetServerSidePropsResult<GoogleCallbackPageProps> => {
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) || "/"
}
};
};
Loading