From e8627d8a0e310295f7dd8f865b32c2cd1c8a4cb4 Mon Sep 17 00:00:00 2001 From: minsuKang <90169703+minchodang@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:33:48 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=EC=A0=84=EB=B0=98=EC=A0=81=EC=9D=B8=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=ED=8E=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=ED=83=80=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81=20=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: verifyGuard page layout 추가 (#81) * Refactor: 인증 인가 서버 route 처리 및 서버 컴포넌트 가딩 처리 1단계 작업 (#82) * WIP: social-login server component 단 처리중 * Fix: social-login server route 처리 및 예외 처리 추가 * Fix: home header 로그인 여부 server component 처리로 전환 (#83) * Fix: login 유지 서버 처리 추가 및 authProvider 로직 추가, 기타 리팩터링 (#85) * Fix: useSearchParams build prerendering error (#84) * Fix: 로그아웃 기능 및 기타 파일명 등 수정 (#86) * Fix: logout 기능 및 기타 변수명 등 수정 * Fix: rebase 후, 파일 명 수정 반영 --- package.json | 2 +- pnpm-lock.yaml | 103 ++++++++++-------- src/api/postLogin.ts | 3 +- src/app/[locale]/page.tsx | 35 +++--- src/app/actions/renewAllCache.ts | 7 ++ src/app/actions/setAuthCookie.ts | 21 ++++ src/app/api/auth/route.tsx | 83 ++++++-------- src/app/api/social-login/route.tsx | 44 ++++++++ src/app/auth/components/AuthPageClient.tsx | 92 ++++++++++++++++ src/app/auth/layout.tsx | 17 --- src/app/auth/page.tsx | 26 ++++- src/app/layout.tsx | 64 ++++++----- src/app/my-page/page.tsx | 13 ++- src/app/quiz-result/page.tsx | 8 +- src/app/quiz/layout.tsx | 9 -- src/app/quiz/page.tsx | 38 ++++--- src/components/common/modal/GlobalModal.tsx | 4 +- .../QuizResultHistoryGraphCard.tsx | 11 +- .../login/HomeBasicLoginSection.tsx | 2 - .../components/signup/HomeSignupScreen.tsx | 9 +- .../screens/home/section/HomeHeaderButton.tsx | 18 ++- .../my-page/section/MyPageHeaderSection.tsx | 6 +- .../section/MyPageTestRecordSection.tsx | 6 +- .../quiz-result/QuizResultDetailInfoCard.tsx | 1 - .../section/QuizResultHistorySection.tsx | 8 +- src/components/screens/quiz/QuizSwiper.tsx | 1 + src/hooks/useSocialLogin.ts | 56 +++++----- src/lib/axios/defaultRequest.ts | 16 ++- src/lib/server/auth/checkToken.ts | 59 ++++++++++ src/provider/AuthProvider.tsx | 24 ++++ src/service/AuthService.ts | 26 +++-- 31 files changed, 537 insertions(+), 275 deletions(-) create mode 100644 src/app/actions/renewAllCache.ts create mode 100644 src/app/actions/setAuthCookie.ts create mode 100644 src/app/api/social-login/route.tsx create mode 100644 src/app/auth/components/AuthPageClient.tsx delete mode 100644 src/app/auth/layout.tsx delete mode 100644 src/app/quiz/layout.tsx create mode 100644 src/lib/server/auth/checkToken.ts create mode 100644 src/provider/AuthProvider.tsx diff --git a/package.json b/package.json index a437dea..04667e2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@types/recharts": "^1.8.29", "axios": "^1.6.7", "dayjs": "^1.11.11", - "next": "13.5.6", + "next": "14.2.12", "next-intl": "^3.7.0", "react": "^18", "react-cookie": "^7.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ceb5ea1..c622591 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ dependencies: version: 2.0.1(react-dom@18.2.0)(react-hook-form@7.51.0)(react@18.2.0) '@sentry/nextjs': specifier: ^8.7.0 - version: 8.7.0(@opentelemetry/api@1.8.0)(@opentelemetry/core@1.24.1)(@opentelemetry/instrumentation@0.51.1)(@opentelemetry/sdk-trace-base@1.24.1)(@opentelemetry/semantic-conventions@1.24.1)(next@13.5.6)(react@18.2.0)(webpack@5.90.1) + version: 8.7.0(@opentelemetry/api@1.8.0)(@opentelemetry/core@1.24.1)(@opentelemetry/instrumentation@0.51.1)(@opentelemetry/sdk-trace-base@1.24.1)(@opentelemetry/semantic-conventions@1.24.1)(next@14.2.12)(react@18.2.0)(webpack@5.90.1) '@tanstack/react-query': specifier: ^5.28.9 version: 5.28.9(react@18.2.0) @@ -27,11 +27,11 @@ dependencies: specifier: ^1.11.11 version: 1.11.11 next: - specifier: 13.5.6 - version: 13.5.6(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + specifier: 14.2.12 + version: 14.2.12(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) next-intl: specifier: ^3.7.0 - version: 3.7.0(next@13.5.6)(react@18.2.0) + version: 3.7.0(next@14.2.12)(react@18.2.0) react: specifier: ^18 version: 18.2.0 @@ -141,7 +141,7 @@ devDependencies: version: 15.2.1 next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@13.5.6) + version: 4.2.3(next@14.2.12) postcss: specifier: ^8.4.35 version: 8.4.35 @@ -1098,6 +1098,10 @@ packages: /@next/env@13.5.6: resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==} + dev: true + + /@next/env@14.2.12: + resolution: {integrity: sha512-3fP29GIetdwVIfIRyLKM7KrvJaqepv+6pVodEbx0P5CaMLYBtx+7eEg8JYO5L9sveJO87z9eCReceZLi0hxO1Q==} /@next/eslint-plugin-next@14.1.0: resolution: {integrity: sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q==} @@ -1105,72 +1109,72 @@ packages: glob: 10.3.10 dev: true - /@next/swc-darwin-arm64@13.5.6: - resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==} + /@next/swc-darwin-arm64@14.2.12: + resolution: {integrity: sha512-crHJ9UoinXeFbHYNok6VZqjKnd8rTd7K3Z2zpyzF1ch7vVNKmhjv/V7EHxep3ILoN8JB9AdRn/EtVVyG9AkCXw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@next/swc-darwin-x64@13.5.6: - resolution: {integrity: sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==} + /@next/swc-darwin-x64@14.2.12: + resolution: {integrity: sha512-JbEaGbWq18BuNBO+lCtKfxl563Uw9oy2TodnN2ioX00u7V1uzrsSUcg3Ep9ce+P0Z9es+JmsvL2/rLphz+Frcw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] requiresBuild: true optional: true - /@next/swc-linux-arm64-gnu@13.5.6: - resolution: {integrity: sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==} + /@next/swc-linux-arm64-gnu@14.2.12: + resolution: {integrity: sha512-qBy7OiXOqZrdp88QEl2H4fWalMGnSCrr1agT/AVDndlyw2YJQA89f3ttR/AkEIP9EkBXXeGl6cC72/EZT5r6rw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@next/swc-linux-arm64-musl@13.5.6: - resolution: {integrity: sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==} + /@next/swc-linux-arm64-musl@14.2.12: + resolution: {integrity: sha512-EfD9L7o9biaQxjwP1uWXnk3vYZi64NVcKUN83hpVkKocB7ogJfyH2r7o1pPnMtir6gHZiGCeHKagJ0yrNSLNHw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@next/swc-linux-x64-gnu@13.5.6: - resolution: {integrity: sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==} + /@next/swc-linux-x64-gnu@14.2.12: + resolution: {integrity: sha512-iQ+n2pxklJew9IpE47hE/VgjmljlHqtcD5UhZVeHICTPbLyrgPehaKf2wLRNjYH75udroBNCgrSSVSVpAbNoYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@next/swc-linux-x64-musl@13.5.6: - resolution: {integrity: sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==} + /@next/swc-linux-x64-musl@14.2.12: + resolution: {integrity: sha512-rFkUkNwcQ0ODn7cxvcVdpHlcOpYxMeyMfkJuzaT74xjAa5v4fxP4xDk5OoYmPi8QNLDs3UgZPMSBmpBuv9zKWA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@next/swc-win32-arm64-msvc@13.5.6: - resolution: {integrity: sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==} + /@next/swc-win32-arm64-msvc@14.2.12: + resolution: {integrity: sha512-PQFYUvwtHs/u0K85SG4sAdDXYIPXpETf9mcEjWc0R4JmjgMKSDwIU/qfZdavtP6MPNiMjuKGXHCtyhR/M5zo8g==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] requiresBuild: true optional: true - /@next/swc-win32-ia32-msvc@13.5.6: - resolution: {integrity: sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==} + /@next/swc-win32-ia32-msvc@14.2.12: + resolution: {integrity: sha512-FAj2hMlcbeCV546eU2tEv41dcJb4NeqFlSXU/xL/0ehXywHnNpaYajOUvn3P8wru5WyQe6cTZ8fvckj/2XN4Vw==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] requiresBuild: true optional: true - /@next/swc-win32-x64-msvc@13.5.6: - resolution: {integrity: sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==} + /@next/swc-win32-x64-msvc@14.2.12: + resolution: {integrity: sha512-yu8QvV53sBzoIVRHsxCHqeuS8jYq6Lrmdh0briivuh+Brsp6xjg80MAozUsBTAV9KNmY08KlX0KYTWz1lbPzEg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1737,7 +1741,7 @@ packages: '@sentry/utils': 8.7.0 dev: false - /@sentry/nextjs@8.7.0(@opentelemetry/api@1.8.0)(@opentelemetry/core@1.24.1)(@opentelemetry/instrumentation@0.51.1)(@opentelemetry/sdk-trace-base@1.24.1)(@opentelemetry/semantic-conventions@1.24.1)(next@13.5.6)(react@18.2.0)(webpack@5.90.1): + /@sentry/nextjs@8.7.0(@opentelemetry/api@1.8.0)(@opentelemetry/core@1.24.1)(@opentelemetry/instrumentation@0.51.1)(@opentelemetry/sdk-trace-base@1.24.1)(@opentelemetry/semantic-conventions@1.24.1)(next@14.2.12)(react@18.2.0)(webpack@5.90.1): resolution: {integrity: sha512-aD+iDPUy5eC1XqPiFfRU09mlK3iCwxSGMg3y/z3GXrUlR9DLI7lMz90l6qaDGFFnjsCT75JrG37dYWVqILAx3g==} engines: {node: '>=14.18'} peerDependencies: @@ -1759,7 +1763,7 @@ packages: '@sentry/vercel-edge': 8.7.0 '@sentry/webpack-plugin': 2.16.0(webpack@5.90.1) chalk: 3.0.0 - next: 13.5.6(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.12(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 resolve: 1.22.8 rollup: 3.29.4 @@ -1897,9 +1901,13 @@ packages: '@sinonjs/commons': 3.0.1 dev: true - /@swc/helpers@0.5.2: - resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + /@swc/helpers@0.5.5: + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} dependencies: + '@swc/counter': 0.1.3 tslib: 2.6.2 /@tanstack/query-core@5.28.9: @@ -5888,7 +5896,7 @@ packages: /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - /next-intl@3.7.0(next@13.5.6)(react@18.2.0): + /next-intl@3.7.0(next@14.2.12)(react@18.2.0): resolution: {integrity: sha512-wLewkBzUbr/g2hKkI8/M1qYzHEVT4KgDeeayppvu+aDCJSOhfUFuYg0IlGn8+HNlgos2IPRRwZtFrTusiqW+uA==} peerDependencies: next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 @@ -5896,12 +5904,12 @@ packages: dependencies: '@formatjs/intl-localematcher': 0.2.32 negotiator: 0.6.3 - next: 13.5.6(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.12(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 use-intl: 3.7.0(react@18.2.0) dev: false - /next-sitemap@4.2.3(next@13.5.6): + /next-sitemap@4.2.3(next@14.2.12): resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==} engines: {node: '>=14.18'} hasBin: true @@ -5912,44 +5920,47 @@ packages: '@next/env': 13.5.6 fast-glob: 3.3.2 minimist: 1.2.8 - next: 13.5.6(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.12(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) dev: true - /next@13.5.6(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==} - engines: {node: '>=16.14.0'} + /next@14.2.12(@babel/core@7.23.9)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cDOtUSIeoOvt1skKNihdExWMTybx3exnvbFbb9ecZDIxlvIbREQzt9A5Km3Zn3PfU+IFjyYGsHS+lN9VInAGKA==} + engines: {node: '>=18.17.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 react: ^18.2.0 react-dom: ^18.2.0 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': optional: true + '@playwright/test': + optional: true sass: optional: true dependencies: - '@next/env': 13.5.6 + '@next/env': 14.2.12 '@opentelemetry/api': 1.8.0 - '@swc/helpers': 0.5.2 + '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001584 + graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) styled-jsx: 5.1.1(@babel/core@7.23.9)(react@18.2.0) - watchpack: 2.4.0 optionalDependencies: - '@next/swc-darwin-arm64': 13.5.6 - '@next/swc-darwin-x64': 13.5.6 - '@next/swc-linux-arm64-gnu': 13.5.6 - '@next/swc-linux-arm64-musl': 13.5.6 - '@next/swc-linux-x64-gnu': 13.5.6 - '@next/swc-linux-x64-musl': 13.5.6 - '@next/swc-win32-arm64-msvc': 13.5.6 - '@next/swc-win32-ia32-msvc': 13.5.6 - '@next/swc-win32-x64-msvc': 13.5.6 + '@next/swc-darwin-arm64': 14.2.12 + '@next/swc-darwin-x64': 14.2.12 + '@next/swc-linux-arm64-gnu': 14.2.12 + '@next/swc-linux-arm64-musl': 14.2.12 + '@next/swc-linux-x64-gnu': 14.2.12 + '@next/swc-linux-x64-musl': 14.2.12 + '@next/swc-win32-arm64-msvc': 14.2.12 + '@next/swc-win32-ia32-msvc': 14.2.12 + '@next/swc-win32-x64-msvc': 14.2.12 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros diff --git a/src/api/postLogin.ts b/src/api/postLogin.ts index c3fe66c..9553c0c 100644 --- a/src/api/postLogin.ts +++ b/src/api/postLogin.ts @@ -1,5 +1,6 @@ import defaultRequest from '@src/lib/axios/defaultRequest'; import { UserInformationResponse } from '@src/types/api/Signs'; +import axios from 'axios'; interface PostLoginBody { email: string; @@ -7,6 +8,6 @@ interface PostLoginBody { } export const postLogin = async (body: PostLoginBody) => { - const data = await defaultRequest.post('/auth/token', { ...body }); + const data = await axios.post('/api/auth', { ...body }); return data; }; diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 9f7af31..932958f 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,23 +1,24 @@ import HomeFooterButton from '@src/components/screens/home/components/HomeFooterButton'; import HomeBody from '@src/components/screens/home/section/HomeBody'; import HomeFooter from '@src/components/screens/home/section/HomeFooter'; -import dynamic from 'next/dynamic'; +import HomeHeaderButton from '@src/components/screens/home/section/HomeHeaderButton'; +import { checkToken } from '@src/lib/server/auth/checkToken'; +import { AuthService } from '@src/service/AuthService'; +import { cookies } from 'next/headers'; -const HomeHeaderButton = dynamic( - () => import('@src/components/screens/home/section/HomeHeaderButton'), - { - ssr: false, - }, -); +const HomePage = async () => { + const accessToken = cookies().get('atk')?.value; + const { isLogin } = await checkToken(accessToken); -const HomePage = () => ( -
-
- -
- - - -
-); + return ( +
+
+ +
+ + + +
+ ); +}; export default HomePage; diff --git a/src/app/actions/renewAllCache.ts b/src/app/actions/renewAllCache.ts new file mode 100644 index 0000000..5a17a14 --- /dev/null +++ b/src/app/actions/renewAllCache.ts @@ -0,0 +1,7 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; + +export async function renewAllCache() { + return revalidatePath('/'); +} diff --git a/src/app/actions/setAuthCookie.ts b/src/app/actions/setAuthCookie.ts new file mode 100644 index 0000000..3b8f63e --- /dev/null +++ b/src/app/actions/setAuthCookie.ts @@ -0,0 +1,21 @@ +'use server'; + +import { cookies } from 'next/headers'; + +export const setAuthCookie = async (token: { atk: string; rtk: string }) => { + await cookies().set('rtk', token.rtk, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 14, // 2주 + path: '/', + sameSite: 'strict', + }); + // Refresh Token을 HttpOnly 쿠키로 설정 (기간 2주) + await cookies().set('atk', token.atk, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 30, // 30분 + path: '/', + sameSite: 'strict', + }); +}; diff --git a/src/app/api/auth/route.tsx b/src/app/api/auth/route.tsx index d52759a..b94b05b 100644 --- a/src/app/api/auth/route.tsx +++ b/src/app/api/auth/route.tsx @@ -1,3 +1,4 @@ +import { setAuthCookie } from '@src/app/actions/setAuthCookie'; import axios from 'axios'; import { cookies } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; @@ -5,7 +6,6 @@ import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { const { email, password } = await request.json(); - console.log(email, password); try { const res = await axios.post( `${process.env.NEXT_PUBLIC_META_TEST_SERVER_HOST_URL}/auth/token`, @@ -14,26 +14,27 @@ export async function POST(request: NextRequest) { password, }, ); - console.log(res.data); - const { accessToken, refreshToken } = res.data; - - // Refresh Token을 HttpOnly 쿠키로 설정 (기간 2주) - cookies().set('rtk', refreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 14, // 2주 - path: '/', - sameSite: 'strict', - }); - // Refresh Token을 HttpOnly 쿠키로 설정 (기간 2주) - cookies().set('atk', accessToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 30, // 30분 - path: '/', - sameSite: 'strict', - }); + const refreshToken = res.headers.refresh_toke; + const accessToken = res.headers.access_token; + + // // Refresh Token을 HttpOnly 쿠키로 설정 (기간 2주) + // cookies().set('rtk', refresh_token, { + // httpOnly: true, + // secure: process.env.NODE_ENV === 'production', + // maxAge: 60 * 60 * 24 * 14, // 2주 + // path: '/', + // sameSite: 'strict', + // }); + // // Refresh Token을 HttpOnly 쿠키로 설정 (기간 2주) + // cookies().set('atk', accessToken, { + // httpOnly: true, + // secure: process.env.NODE_ENV === 'production', + // maxAge: 60 * 30, // 30분 + // path: '/', + // sameSite: 'strict', + // }); + setAuthCookie({ atk: accessToken, rtk: refreshToken }); // Access Token을 응답 헤더로 클라이언트에 전달 const response = NextResponse.json({ success: true }); @@ -46,8 +47,8 @@ export async function POST(request: NextRequest) { } export async function PUT(request: NextRequest) { - const refreshToken = cookies().get('rtk')?.value; - const accessToken = cookies().get('atk')?.value; + const { headers } = await request.json(); + const refreshToken = headers['refreshToken']; if (!refreshToken) { return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); @@ -57,32 +58,16 @@ export async function PUT(request: NextRequest) { const res = await axios.post( `${process.env.NEXT_PUBLIC_META_TEST_SERVER_HOST_URL}/auth/token/refresh`, { - refreshToken, - accessToken, + refresh_token: refreshToken, }, ); - const newResponse = res; - - // 새로운 Refresh Token을 HttpOnly 쿠키로 설정 - cookies().set('rtk', newResponse.data.refreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 14, // 2주 - path: '/', - sameSite: 'strict', - }); - cookies().set('atk', newResponse.data.accessToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 30, // 30분 - path: '/', - sameSite: 'strict', - }); - + const newAccessToken = res.headers.access_token; + const newRefreshToken = res.headers.refresh_token; // 새로운 Access Token을 응답 헤더로 클라이언트에 전달 const response = NextResponse.json({ success: true }); - response.headers.set('Authorization', `Bearer ${newResponse.data.accessToken}`); + response.headers.set('atk', newAccessToken); + response.headers.set('rtk', newRefreshToken); return response; } catch (error) { console.log(error, '리이슈'); @@ -93,17 +78,19 @@ export async function PUT(request: NextRequest) { export async function DELETE(request: NextRequest) { const refreshToken = cookies().get('rtk')?.value; const accessToken = cookies().get('atk')?.value; + console.log(accessToken, '어 뜨잖아?'); if (!refreshToken) { return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); } try { - await axios.delete(`${process.env.NEXT_PUBLIC_META_TEST_SERVER_HOST_URL}/auth/token`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); + //TODO 현재 백엔드 로그아웃 기능 미구축으로 서버 통신은 잠시 보류, 추후 재구축 예정. + // await axios.delete(`${process.env.NEXT_PUBLIC_META_TEST_SERVER_HOST_URL}/auth/token`, { + // headers: { + // Authorization: `Bearer ${accessToken}`, + // }, + // }); cookies().delete('rtk'); cookies().delete('atk'); diff --git a/src/app/api/social-login/route.tsx b/src/app/api/social-login/route.tsx new file mode 100644 index 0000000..99dc39d --- /dev/null +++ b/src/app/api/social-login/route.tsx @@ -0,0 +1,44 @@ +import { setAuthCookie } from '@src/app/actions/setAuthCookie'; +import { SocialLoginRequestParameter } from '@src/app/auth/page'; +import axios, { isAxiosError } from 'axios'; +import { NextRequest, NextResponse } from 'next/server'; + +const metaTestServerHost = process.env.NEXT_PUBLIC_META_TEST_SERVER_HOST_URL; + +export async function POST(request: NextRequest) { + const { code, socialType }: Omit = + await request.json(); + + if (!code || !socialType) { + return NextResponse.json({ error: 'Bad Request' }, { status: 400 }); + } + + try { + const serverResponse = await axios.get(`${metaTestServerHost}/auth/login/${socialType}`, { + params: { + code, + }, + }); + + const accessToken = serverResponse.headers.access_token; + const refreshToken = serverResponse.headers.refresh_token; + + // NextResponse에 쿠키 설정 + const response = NextResponse.json({ success: true }); + + setAuthCookie({ atk: accessToken, rtk: refreshToken }); + // 새로운 Access Token을 응답 헤더로 클라이언트에 전달 + response.headers.set('Authorization', `Bearer ${accessToken}`); + return response; + } catch (error) { + if (isAxiosError(error) && error.response?.data) { + return NextResponse.json( + { error: error.response.data.message }, + { + status: error.response?.status || 500, + }, + ); + } + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/src/app/auth/components/AuthPageClient.tsx b/src/app/auth/components/AuthPageClient.tsx new file mode 100644 index 0000000..0724944 --- /dev/null +++ b/src/app/auth/components/AuthPageClient.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { renewAllCache } from '@src/app/actions/renewAllCache'; +import { ToastService } from '@src/service/ToastService'; +import axios from 'axios'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useCookies } from 'react-cookie'; + +type SocialLoginType = 'google' | 'kakao'; + +interface SocialLoginInfo { + loginPath: string; + socialType: SocialLoginType; +} + +export interface SocialLoginRequestParameter { + loginPath: string; + code: string; + socialType: SocialLoginType; +} + +interface AuthPageClientProps { + code?: string | string[]; +} + +const toastService = ToastService.getInstance(); + +const processSocialLogin = async ({ code, socialType, loginPath }: SocialLoginRequestParameter) => { + try { + const { data } = await axios.post( + `${process.env.NEXT_PUBLIC_MATE_TEST_WEB_HOST_URL}/api/social-login`, + { + code, + socialType, + }, + ); + return data; + } catch (error) { + throw error; + } +}; + +const AuthPageClient = ({ code }: AuthPageClientProps) => { + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [cookies] = useCookies(['social-login-info']); + const socialLoginInfo = cookies['social-login-info']; + + useEffect(() => { + const handleLogin = async () => { + if (!socialLoginInfo || !code || typeof code !== 'string') { + router.replace('/'); + return; + } + + try { + const { loginPath, socialType } = socialLoginInfo as SocialLoginInfo; + const data = await processSocialLogin({ + code, + socialType, + loginPath, + }); + + if (data) { + toastService.addToast('로그인 되었습니다.'); + renewAllCache(); + router.replace(loginPath ?? '/'); + } else { + toastService.addToast('로그인에 실패하였습니다.'); + router.replace(loginPath ?? '/'); + } + } catch (error) { + console.log(error); + toastService.addToast('로그인 중 오류가 발생했습니다.'); + router.replace('/'); + } finally { + setLoading(false); + } + }; + + handleLogin(); + }, [router, socialLoginInfo, code]); + + if (loading) { + return

로그인 처리 중...

; + } + + return null; +}; + +export default AuthPageClient; diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx deleted file mode 100644 index 2e645a1..0000000 --- a/src/app/auth/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { FC } from 'react'; - -import { useTranslations } from 'next-intl'; -import Head from 'next/head'; - -interface HomePageLayoutProps { - children: React.ReactNode; - params: { locale: string }; -} - -const HomePageLayout: FC = ({ children, params: { locale } }) => ( -
-
{children}
-
-); - -export default HomePageLayout; diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index dc4a73f..37d84e4 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -1,10 +1,26 @@ -'use client'; +import AuthPageClient from './components/AuthPageClient'; -import useSocialLogin from '@src/hooks/useSocialLogin'; +type SocialLoginType = 'google' | 'kakao'; -const AuthPage = () => { - useSocialLogin(); - return
; +interface SocialLoginInfo { + loginPath: string; + socialType: SocialLoginType; +} + +export interface SocialLoginRequestParameter { + loginPath: string; + code: string; + socialType: SocialLoginType; +} + +interface AuthPageProps { + searchParams: { [key: string]: string | string[] | undefined }; +} + +const AuthPage = ({ searchParams }: AuthPageProps) => { + const code = searchParams.code; + + return ; }; export default AuthPage; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ce068b2..31f24d0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,9 @@ import './globals.css'; import TanstackQueryProvider from '@src/provider/TanstackQueryProvider'; import { Metadata } from 'next'; import Head from 'next/head'; +import { checkToken } from '@src/lib/server/auth/checkToken'; +import { cookies } from 'next/headers'; +import { AuthProvider } from '@src/provider/AuthProvider'; interface LocaleLayoutProps { children: React.ReactNode; @@ -34,31 +37,40 @@ export const metadata: Metadata = { }, }; -const LocaleLayout: FC = ({ children }) => ( - - - - - - - - - - - - - {children} - - - - - -); +const LocaleLayout: FC = async ({ children }) => { + const accessToken = cookies().get('atk')?.value; + const { token } = await checkToken(accessToken); + + return ( + + + + + + + + + + + + + + {children} + + + + + + ); +}; export default LocaleLayout; diff --git a/src/app/my-page/page.tsx b/src/app/my-page/page.tsx index 0d961cf..967e5b5 100644 --- a/src/app/my-page/page.tsx +++ b/src/app/my-page/page.tsx @@ -1,16 +1,19 @@ import MyPageHeaderSection from '@src/components/screens/my-page/section/MyPageHeaderSection'; import MyPageTestRecordSection from '@src/components/screens/my-page/section/MyPageTestRecordSection'; import MyPageUserInformationSection from '@src/components/screens/my-page/section/MyPageUserInformationSection'; +import { checkToken } from '@src/lib/server/auth/checkToken'; +import { AuthService } from '@src/service/AuthService'; import { cookies } from 'next/headers'; -const MyPage = () => { - const cookie = cookies(); +const MyPage = async () => { + const accessToken = cookies().get('atk')?.value; + const { isLogin } = await checkToken(accessToken); return (
- - - + + +
); }; diff --git a/src/app/quiz-result/page.tsx b/src/app/quiz-result/page.tsx index 731d16c..812f516 100644 --- a/src/app/quiz-result/page.tsx +++ b/src/app/quiz-result/page.tsx @@ -2,16 +2,18 @@ import QuizResultFooter from '@src/components/screens/quiz-result/QuizResultFoot import QuizResultDetailSection from '@src/components/screens/quiz-result/section/QuizResultDetailSection'; import QuizResultHistorySection from '@src/components/screens/quiz-result/section/QuizResultHistorySection'; import QuizResultSummarySection from '@src/components/screens/quiz-result/section/QuizResultSummarySection'; +import { checkToken } from '@src/lib/server/auth/checkToken'; import { cookies } from 'next/headers'; -const QuizAnswerPage = () => { - const cookie = cookies(); +const QuizAnswerPage = async () => { + const accessToken = cookies().get('atk')?.value; + const { isLogin } = await checkToken(accessToken); return (
- +
); diff --git a/src/app/quiz/layout.tsx b/src/app/quiz/layout.tsx deleted file mode 100644 index f2e7728..0000000 --- a/src/app/quiz/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -type Props = { - children: React.ReactNode; -}; - -const QuizLayout = ({ children }: Props) =>
{children}
; - -export default QuizLayout; diff --git a/src/app/quiz/page.tsx b/src/app/quiz/page.tsx index 98d1d77..ed7ff69 100644 --- a/src/app/quiz/page.tsx +++ b/src/app/quiz/page.tsx @@ -1,21 +1,23 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import QuizBanner from '@src/components/screens/quiz/QuizBanner'; import QuizSection from '@src/components/screens/quiz/QuizSection'; -const QuizPageIndex = () => ( -
-
- - -
-
-); - -export default React.memo(QuizPageIndex); +export default function QuizPageIndex() { + return ( +
+
+ + + + +
+
+ ); +} diff --git a/src/components/common/modal/GlobalModal.tsx b/src/components/common/modal/GlobalModal.tsx index 26dd7f2..6c20f3c 100644 --- a/src/components/common/modal/GlobalModal.tsx +++ b/src/components/common/modal/GlobalModal.tsx @@ -10,7 +10,7 @@ const GlobalModal: React.FC = () => { const [contents, setContents] = useState< Array<{ content: ReactNode; backGroundColor?: string }> >([]); - + useEffect(() => { const handleModalChange = ({ isOpen: isModalOpen, @@ -23,7 +23,7 @@ const GlobalModal: React.FC = () => { return () => { modalService.unsubscribe(handleModalChange); }; - }, []); + }, [modalService]); if (!isOpen) { return null; diff --git a/src/components/common/quiz-result/QuizResultHistoryGraphCard.tsx b/src/components/common/quiz-result/QuizResultHistoryGraphCard.tsx index 55c5450..0bc2b9b 100644 --- a/src/components/common/quiz-result/QuizResultHistoryGraphCard.tsx +++ b/src/components/common/quiz-result/QuizResultHistoryGraphCard.tsx @@ -1,6 +1,5 @@ 'use client'; -import { FC, useEffect, useMemo, useState } from 'react'; import { API_GET_USER_PROFILE, getUserProfile } from '@src/api/getUserProfile'; import { API_GET_USER_TEST_LIST, getUserTestList } from '@src/api/getUserTestList'; import SelectBox, { SelectBoxOptionType } from '@src/components/common/SelectBox'; @@ -8,11 +7,11 @@ import HomeLoginModalScreen from '@src/components/screens/home/components/login/ import { ModalService } from '@src/service/ModalService'; import { QuizTestLevel } from '@src/types/api/test'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; -import { useCookies } from 'react-cookie'; +import { FC, useState } from 'react'; import { QuizResultBarChart } from './QuizResultBarChart'; interface QuizResultHistoryGraphCardProps { - isLoginSns: boolean; + isLogin: boolean; } const options: SelectBoxOptionType[] = [ @@ -36,7 +35,7 @@ function isValidTestLevelType(value: unknown): value is QuizTestLevel { ); } -const QuizResultHistoryGraphCard: FC = ({ isLoginSns }) => { +const QuizResultHistoryGraphCard: FC = ({ isLogin }) => { const [selectedLevel, setSelectedLevel] = useState('all'); const { data: userData } = useQuery({ queryKey: [API_GET_USER_PROFILE], @@ -72,7 +71,7 @@ const QuizResultHistoryGraphCard: FC = ({ isLog return (
- {!isLoginSns ? ( + {!isLogin ? (
) : null} -
+

테스트 점수 기록

diff --git a/src/components/screens/home/components/login/HomeBasicLoginSection.tsx b/src/components/screens/home/components/login/HomeBasicLoginSection.tsx index 1d95379..0666463 100644 --- a/src/components/screens/home/components/login/HomeBasicLoginSection.tsx +++ b/src/components/screens/home/components/login/HomeBasicLoginSection.tsx @@ -21,7 +21,6 @@ export interface HomeBasicLoginFormValue { } const HomeBasicLoginSection: FC = () => { - const [, setCookie] = useCookies(['refreshToken']); const modalService = ModalService.getInstance(); const toastService = ToastService.getInstance(); const login = useMutation({ @@ -31,7 +30,6 @@ const HomeBasicLoginSection: FC = () => { const accessToken = data.headers.access_token; const refreshToken = data.headers.refresh_token; defaultRequest.defaults.headers.common.Authorization = `Bearer ${accessToken}`; - await setCookie('refreshToken', refreshToken); toastService.addToast('로그인 되었습니다.'); modalService.closeEntireModal(); } diff --git a/src/components/screens/home/components/signup/HomeSignupScreen.tsx b/src/components/screens/home/components/signup/HomeSignupScreen.tsx index 56a0a0e..97c7b31 100644 --- a/src/components/screens/home/components/signup/HomeSignupScreen.tsx +++ b/src/components/screens/home/components/signup/HomeSignupScreen.tsx @@ -1,4 +1,3 @@ -import { FC, useState } from 'react'; import { patchEmailVerification } from '@src/api/patchEmailVerification'; import { postEmailVerification } from '@src/api/postEmailVerification'; import { postLogin } from '@src/api/postLogin'; @@ -9,7 +8,7 @@ import { ModalService } from '@src/service/ModalService'; import { ToastService } from '@src/service/ToastService'; import { useMutation } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; -import { useCookies } from 'react-cookie'; +import { FC, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import HomeSignupButton from './HomeSignupButton'; import HomeSignupPassword from './HomeSignupPassword'; @@ -24,7 +23,6 @@ export interface HomeSignupFormValue { } const HomeSignupScreen: FC = () => { - const [, setCookie] = useCookies(['refreshToken']); const [step, setStep] = useState(1); const toastService = ToastService.getInstance(); const modalService = ModalService.getInstance(); @@ -56,11 +54,12 @@ const HomeSignupScreen: FC = () => { onSuccess: async (data) => { if (data) { const accessToken = data.headers.access_token; - const refreshToken = data.headers.refresh_token; defaultRequest.defaults.headers.common.Authorization = `Bearer ${accessToken}`; - await setCookie('refreshToken', refreshToken); } }, + onError: (error) => { + toastService.addToast('로그인에 실패했습니다. 다시 시도해 주세요.'); + }, }); const onSubmit = async ({ email, password }: HomeSignupFormValue) => { diff --git a/src/components/screens/home/section/HomeHeaderButton.tsx b/src/components/screens/home/section/HomeHeaderButton.tsx index acaf5ac..6cc2ad5 100644 --- a/src/components/screens/home/section/HomeHeaderButton.tsx +++ b/src/components/screens/home/section/HomeHeaderButton.tsx @@ -1,19 +1,17 @@ 'use client'; - -import React, { FC, useCallback, useMemo } from 'react'; import Button from '@src/components/common/Button'; import { ModalService } from '@src/service/ModalService'; -import { useRouter } from 'next/navigation'; -import { useCookies } from 'react-cookie'; +import { useCallback } from 'react'; import HomeLoginModalScreen from '../components/login/HomeLoginModalScreen'; +import { useRouter } from 'next/navigation'; -const HomeHeaderButton: FC = () => { - const [cookie] = useCookies(['refreshToken']); +interface HomeHeaderButtonProps { + isLogin: boolean; +} + +const HomeHeaderButton = ({ isLogin }: HomeHeaderButtonProps) => { const { push } = useRouter(); const modalService = ModalService.getInstance(); - const token = cookie.refreshToken; - - const isLogin = useMemo(() => !!token, [token]); const onClickLoginButton = useCallback(() => { if (isLogin) { @@ -32,4 +30,4 @@ const HomeHeaderButton: FC = () => { ); }; -export default React.memo(HomeHeaderButton); +export default HomeHeaderButton; diff --git a/src/components/screens/my-page/section/MyPageHeaderSection.tsx b/src/components/screens/my-page/section/MyPageHeaderSection.tsx index 3423591..29d7e87 100644 --- a/src/components/screens/my-page/section/MyPageHeaderSection.tsx +++ b/src/components/screens/my-page/section/MyPageHeaderSection.tsx @@ -8,13 +8,14 @@ import { ToastService } from '@src/service/ToastService'; import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { useCookies } from 'react-cookie'; +import axios from 'axios'; +import { renewAllCache } from '@src/app/actions/renewAllCache'; interface MyPageHeaderSectionProps { isLogin: boolean; } const MyPageHeaderSection: FC = ({ isLogin }) => { - const [, , removeCookies] = useCookies(['refreshToken']); const queryClient = useQueryClient(); const toastService = ToastService.getInstance(); const { replace } = useRouter(); @@ -22,7 +23,8 @@ const MyPageHeaderSection: FC = ({ isLogin }) => { replace('/'); }; const onClickLogout = async () => { - await removeCookies('refreshToken'); + await axios.delete(`${process.env.NEXT_PUBLIC_META_TEST_WEB_HOST_URL}/api/auth`); + await renewAllCache(); await queryClient.removeQueries({ queryKey: [API_GET_USER_PROFILE], }); diff --git a/src/components/screens/my-page/section/MyPageTestRecordSection.tsx b/src/components/screens/my-page/section/MyPageTestRecordSection.tsx index d7850fe..d9a1f91 100644 --- a/src/components/screens/my-page/section/MyPageTestRecordSection.tsx +++ b/src/components/screens/my-page/section/MyPageTestRecordSection.tsx @@ -2,13 +2,13 @@ import { FC } from 'react'; import QuizResultHistoryGraphCard from '@src/components/common/quiz-result/QuizResultHistoryGraphCard'; interface MyPageTestRecordSectionProps { - isLoginSns: boolean; + isLogin: boolean; } -const MyPageTestRecordSection: FC = ({ isLoginSns }) => ( +const MyPageTestRecordSection: FC = ({ isLogin }) => (

나의 테스트 기록

- +
); diff --git a/src/components/screens/quiz-result/QuizResultDetailInfoCard.tsx b/src/components/screens/quiz-result/QuizResultDetailInfoCard.tsx index 376aa8e..d312b6d 100644 --- a/src/components/screens/quiz-result/QuizResultDetailInfoCard.tsx +++ b/src/components/screens/quiz-result/QuizResultDetailInfoCard.tsx @@ -1,5 +1,4 @@ 'use client'; - import { FC } from 'react'; import { API_GET_USER_TEST_DETAIL, getUserTestDetail } from '@src/api/getUserTestDetail'; import QuizCorrectCircleIcon from '@src/components/common/Icons/QuizCorrectCircleIcon'; diff --git a/src/components/screens/quiz-result/section/QuizResultHistorySection.tsx b/src/components/screens/quiz-result/section/QuizResultHistorySection.tsx index 282cf46..b7304f2 100644 --- a/src/components/screens/quiz-result/section/QuizResultHistorySection.tsx +++ b/src/components/screens/quiz-result/section/QuizResultHistorySection.tsx @@ -7,10 +7,10 @@ import QuizResultRankingCard from '@src/components/common/quiz-result/QuizResult import { useRouter } from 'next/navigation'; interface QuizResultHistorySectionProps { - isLoginSns: boolean; + isLogin: boolean; } -const QuizResultHistorySection: FC = ({ isLoginSns }) => { +const QuizResultHistorySection: FC = ({ isLogin }) => { const { push } = useRouter(); const onClick = () => { push('/my-page'); @@ -19,7 +19,7 @@ const QuizResultHistorySection: FC = ({ isLoginSn

기록

- {isLoginSns && ( + {isLogin && (
)}
- +
); }; diff --git a/src/components/screens/quiz/QuizSwiper.tsx b/src/components/screens/quiz/QuizSwiper.tsx index cc61e90..421cd72 100644 --- a/src/components/screens/quiz/QuizSwiper.tsx +++ b/src/components/screens/quiz/QuizSwiper.tsx @@ -1,3 +1,4 @@ +'use client'; import React, { FC, useEffect, useRef, useState } from 'react'; import { getRandomQuizList } from '@src/lib/quiz/getRandomQuizList'; import { QuizListService } from '@src/service/QuizListService'; diff --git a/src/hooks/useSocialLogin.ts b/src/hooks/useSocialLogin.ts index 60490d4..1cac589 100644 --- a/src/hooks/useSocialLogin.ts +++ b/src/hooks/useSocialLogin.ts @@ -1,10 +1,9 @@ -import { useEffect, useMemo } from 'react'; import { API_GET_GOOGLE_LOGIN, getGoogleLogin } from '@src/api/getGoogleLogin'; import { API_GET_KAKAKO_LOGIN, getKakaoLogin } from '@src/api/getKakaoLogin'; -import defaultRequest from '@src/lib/axios/defaultRequest'; import { ToastService } from '@src/service/ToastService'; import { useQuery } from '@tanstack/react-query'; import { useRouter, useSearchParams } from 'next/navigation'; +import { useMemo } from 'react'; import { useCookies } from 'react-cookie'; export type SocialType = 'google' | 'kakao'; @@ -20,6 +19,7 @@ const useSocialLogin = () => { const toastService = ToastService.getInstance(); const code = get('code'); const socialInfo = getCookie['social-login-info']; + console.log(socialInfo, '소셜 로그인 인포!'); const socialInfoObj: SocialLoginInformationType = useMemo(() => { if (socialInfo) return socialInfo; @@ -37,32 +37,32 @@ const useSocialLogin = () => { enabled: !!code && socialInfoObj?.socialType === 'kakao', }); - useEffect(() => { - async function kakaoLoginProcess() { - if (kakaoLogin.data) { - const accessToken = kakaoLogin.data.headers.access_token; - const refreshToken = kakaoLogin.data.headers.refresh_token; - defaultRequest.defaults.headers.common.Authorization = `Bearer ${accessToken}`; - await setCookie('refreshToken', refreshToken); - toastService.addToast('로그인 되었습니다.'); - push(socialInfoObj.loginPath ?? '/'); - } - } - kakaoLoginProcess(); - }, [kakaoLogin.data, push, setCookie, socialInfoObj?.loginPath, toastService]); - useEffect(() => { - async function googleLoginProcess() { - if (googleLogin.data) { - const accessToken = googleLogin.data.headers.access_token; - const refreshToken = googleLogin.data.headers.refresh_token; - defaultRequest.defaults.headers.common.Authorization = `Bearer ${accessToken}`; - await setCookie('refreshToken', refreshToken); - toastService.addToast('로그인 되었습니다.'); - push(socialInfoObj.loginPath ?? '/'); - } - } - googleLoginProcess(); - }, [googleLogin.data, push, setCookie, socialInfoObj?.loginPath, toastService]); + // useEffect(() => { + // async function kakaoLoginProcess() { + // if (kakaoLogin.data) { + // const accessToken = kakaoLogin.data.headers.access_token; + // const refreshToken = kakaoLogin.data.headers.refresh_token; + // defaultRequest.defaults.headers.common.Authorization = `Bearer ${accessToken}`; + // await setCookie('refreshToken', refreshToken); + // toastService.addToast('로그인 되었습니다.'); + // push(socialInfoObj.loginPath ?? '/'); + // } + // } + // kakaoLoginProcess(); + // }, [kakaoLogin.data, push, setCookie, socialInfoObj?.loginPath, toastService]); + // useEffect(() => { + // async function googleLoginProcess() { + // if (googleLogin.data) { + // const accessToken = googleLogin.data.headers.access_token; + // const refreshToken = googleLogin.data.headers.refresh_token; + // defaultRequest.defaults.headers.common.Authorization = `Bearer ${accessToken}`; + // await setCookie('refreshToken', refreshToken); + // toastService.addToast('로그인 되었습니다.'); + // push(socialInfoObj.loginPath ?? '/'); + // } + // } + // googleLoginProcess(); + // }, [googleLogin.data, push, setCookie, socialInfoObj?.loginPath, toastService]); }; export default useSocialLogin; diff --git a/src/lib/axios/defaultRequest.ts b/src/lib/axios/defaultRequest.ts index 87a88d9..fbf127a 100644 --- a/src/lib/axios/defaultRequest.ts +++ b/src/lib/axios/defaultRequest.ts @@ -1,10 +1,14 @@ +import { AuthService } from '@src/service/AuthService'; import axios from 'axios'; -import { Cookies, useCookies } from 'react-cookie'; +import { Cookies } from 'react-cookie'; + +// const authService = AuthService.getInstance(); const defaultRequest = axios.create({ baseURL: process.env.NEXT_PUBLIC_META_TEST_SERVER_HOST_URL, headers: { 'Content-Type': 'application/json', + // Authorization: `Bearer ${authService.getAccessToken()}`, // 초기 토큰 설정 }, withCredentials: true, }); @@ -17,19 +21,13 @@ defaultRequest.interceptors.response.use( async (response) => response, async (error) => { if (error.response && error.response.status === 401) { - const refreshToken = cookies.get('refreshToken'); - if (!refreshToken) { - return Promise.reject(error); - } - if (retryCount < maxRetries) { retryCount += 1; try { - const response = await defaultRequest.post('/auth/token/refresh', { - refresh_token: refreshToken, - }); + const response = await defaultRequest.post('/auth/token/refresh'); const accessToken = response.headers.access_token; defaultRequest.defaults.headers.Authorization = `Bearer ${accessToken}`; + // authService.setAccessToken(accessToken); // 갱신된 토큰을 AuthService에 전달 await defaultRequest.request(error.config); } catch (refreshError) { return Promise.reject(refreshError); diff --git a/src/lib/server/auth/checkToken.ts b/src/lib/server/auth/checkToken.ts new file mode 100644 index 0000000..d6c6063 --- /dev/null +++ b/src/lib/server/auth/checkToken.ts @@ -0,0 +1,59 @@ +'use server'; +import axios, { isAxiosError } from 'axios'; +import { cookies } from 'next/headers'; + +interface LoginState { + isLogin: boolean; + token?: { + access_token?: string; + refresh_token?: string; + }; +} + +const reissueToken = async (): Promise => { + try { + const { headers } = await axios.put( + `${process.env.NEXT_PUBLIC_META_TEST_WEB_HOST_URL}/api/auth`, + { + headers: { + refreshToken: cookies().get('rtk')?.value, + }, + }, + ); // 서버 사이드에서 토큰 재발급 + const accessToken = headers['atk']; + const refreshToken = headers['rtk']; + return { + isLogin: true, + token: { + access_token: accessToken, + refresh_token: refreshToken, + }, + }; + } catch (error) { + console.log('토큰 재발급 실패:', error); + return { + isLogin: false, + }; + } +}; + +export const checkToken = async (accessToken?: string): Promise => { + try { + await axios.get(`${process.env.NEXT_PUBLIC_META_TEST_SERVER_HOST_URL}/users`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return { + isLogin: true, + token: { + access_token: accessToken, + }, + }; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 401) { + return reissueToken(); // 토큰 만료 시 재발급 + } + return { isLogin: false }; + } +}; diff --git a/src/provider/AuthProvider.tsx b/src/provider/AuthProvider.tsx new file mode 100644 index 0000000..cb6e00f --- /dev/null +++ b/src/provider/AuthProvider.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { setAuthCookie } from '@src/app/actions/setAuthCookie'; +import defaultRequest from '@src/lib/axios/defaultRequest'; +import { FC, useEffect } from 'react'; + +interface IAuthProviderProps { + accessToken?: string; + refreshToken?: string; +} + +export const AuthProvider: FC = ({ accessToken, refreshToken }) => { + useEffect(() => { + if (accessToken && refreshToken) { + setAuthCookie({ rtk: refreshToken, atk: accessToken }); + } + }, [accessToken, refreshToken]); + + useEffect(() => { + defaultRequest.defaults.headers.common.Authorization = `Bearer ${accessToken}`; + }, [accessToken]); + + return <>; +}; diff --git a/src/service/AuthService.ts b/src/service/AuthService.ts index 4f7217c..7051ef8 100644 --- a/src/service/AuthService.ts +++ b/src/service/AuthService.ts @@ -1,12 +1,13 @@ export interface AuthState { accessToken?: string; + isLogin: boolean; } type Subscriber = (state: AuthState) => void; export class AuthService { private static instance: AuthService; - private currentState: AuthState = { accessToken: undefined }; + private currentState: AuthState = { accessToken: undefined, isLogin: false }; private subscribers: Subscriber[] = []; private constructor() {} @@ -24,21 +25,32 @@ export class AuthService { this.subscribers.push(callback); } + // 구독 해제 메서드 + unsubscribe(callback: Subscriber) { + this.subscribers = this.subscribers.filter((sub) => sub !== callback); + } + // 현재 상태 반환 getAccessToken(): string | undefined { return this.currentState.accessToken; } + // 현재 로그인 상태 반환 + + getIsLogin(): boolean | undefined { + return this.currentState.isLogin; + } + // 토큰 설정 (토큰 변경 시 구독자들에게 알림) setAccessToken(token: string | undefined) { this.currentState.accessToken = token; this.notifySubscribers(); - // 로컬 스토리지에 저장할 경우 - if (token) { - localStorage.setItem('accessToken', token); - } else { - localStorage.removeItem('accessToken'); - } + } + + setLoginState(isLogin: boolean) { + this.currentState.isLogin = isLogin; + console.log('Login State Updated: ', this.currentState.isLogin); // 상태가 제대로 변경되는지 확인 + this.notifySubscribers(); } // 상태가 변경되면 구독자들에게 알림