Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
90f6081
feat: LoginPage UI 컴포넌트 작업
leeleeleeleejun Nov 2, 2025
9da927c
feat: 로그인 authorization path 추가
leeleeleeleejun Nov 5, 2025
6eb9a7c
feat: 카카오 로그인을 위해 Link 컴포넌트를 사용하도록 LoginPage 업데이트
leeleeleeleejun Nov 5, 2025
6a2c710
feat: 로그인 성공 및 실패 페이지 컴포넌트 추가
leeleeleeleejun Nov 8, 2025
335066f
feat: MapContainer에서 로딩 컴포넌트를 Spinner로 변경
leeleeleeleejun Nov 9, 2025
f198d0b
feat: client origin 파라미터를 포함하도록 authorization 경로 수정
leeleeleeleejun Nov 12, 2025
2ddcbb5
test: 로그인 성공 페이지에서 accessToken 요청 기능 추가
leeleeleeleejun Nov 15, 2025
492da69
feat: PlaceList 컴포넌트의 리스트에 하단 패딩 추가
leeleeleeleejun Nov 17, 2025
3246332
chore: cookies-next package 추가
leeleeleeleejun Nov 17, 2025
7cc4743
feat: 카카오 SDK 스크립트 및 전역 window 객체에 Kakao 타입 정의 추가
leeleeleeleejun Nov 17, 2025
a8d0270
feat: LOGIN PATH 추가 및 인증 경로(AUTH.AUTHORIZE) 수정
leeleeleeleejun Nov 17, 2025
23e595d
feat: axiosInstance V2 생성 및 기본 설정 추가
leeleeleeleejun Nov 17, 2025
106e705
feat: 카카오 인가 코드 요청 기능 구현 및 LoginButton 컴포넌트 분리
leeleeleeleejun Nov 17, 2025
96b3746
feat: OAuth 인증 요청 처리 및 리다이렉트 응답 구현
leeleeleeleejun Nov 17, 2025
54ece42
feat: 토큰 핸들링 및 에러 리디렉션을 위한 axiosInstance 요청/응답 인터셉터 추가
leeleeleeleejun Nov 17, 2025
3face67
feat: accessToken 유효성 검사 및 로그인 리디렉션을 위한 미들웨어 추가
leeleeleeleejun Nov 17, 2025
f328020
fix: GET 요청 내 백엔드 쿠키 핸들링 로직을 accessToken 설정 밑으로 이동
leeleeleeleejun Nov 17, 2025
36b307e
feat: 토큰 재발근 api getToken 함수 구현
leeleeleeleejun Nov 18, 2025
6bb148c
refactor: route.ts 주석 처리 및 이름 변경
leeleeleeleejun Nov 19, 2025
b121561
feat: API rewrites 및 토큰 처리를 위한 인증 페이지 추가
leeleeleeleejun Nov 19, 2025
4e7c789
feat: axios 인터셉터 내 토큰 갱신 로직 구현 및 토큰 조회 방식 수정
leeleeleeleejun Nov 19, 2025
132040c
feat: 인증 처리 로직 개선 및 토큰 발급 실패 예외 처리
leeleeleeleejun Nov 20, 2025
39e629e
fix: 수정된 쿠키 설정에서 만료 속성 이름을 'expires'로 변경
leeleeleeleejun Nov 20, 2025
eeb68da
feat: 서버/클라이언트 호환 쿠키 조회 유틸리티 함수 `getCookie` 구현
leeleeleeleejun Nov 20, 2025
c9a62e3
feat: 카카오 SDK 연동 script 태그를 next/script로 교체
leeleeleeleejun Nov 20, 2025
b390ae2
feat: 로그인 로딩 스피너 추가 및 인가 코드 누락 예외 처리
leeleeleeleejun Nov 20, 2025
5666f85
feat: 쿠키 설정에 secure, sameSite 및 path 속성 추가
leeleeleeleejun Nov 20, 2025
766cb66
fix: 수정된 로그인 응답에서 반환값 데이터 구조 변경
leeleeleeleejun Nov 20, 2025
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
60 changes: 60 additions & 0 deletions apps/web/app/_apis/services/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { API_PATH, CLIENT_PATH } from '@/_constants/path'
import { setCookie, deleteCookie } from 'cookies-next'
import axios from 'axios'

const CLIENT_URL = process.env.NEXT_PUBLIC_CLIENT_URL || ''
const KAKAO_CLIENT_ID = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID || ''

export const getKakaoInga = async () => {
const isAndroid = /Android/.test(navigator.userAgent)
// const isiOS = /(iPhone|iPad|iPod)/.test(navigator.userAgent);
try {
if (isAndroid) {
window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_CLIENT_ID}&redirect_uri=${CLIENT_URL + CLIENT_PATH.LOGIN_SUCCESS}&response_type=code`
} else {
await window.Kakao.Auth.authorize({
redirectUri: CLIENT_URL + CLIENT_PATH.LOGIN_SUCCESS,
})
}
} catch (error) {
console.log(error)
}
}
Comment on lines +8 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

타입 안전성 및 에러 처리 개선 필요.

이 함수에는 몇 가지 개선이 필요한 부분이 있습니다:

  1. window.Kakao에 대한 타입 정의가 없어 타입 안전성이 부족합니다.
  2. 에러가 발생해도 사용자에게 알리지 않고 콘솔에만 로그를 남깁니다.
  3. 주석 처리된 iOS 감지 코드는 제거하거나 구현을 완료해야 합니다.

타입 안전성을 위해 전역 타입 정의를 추가하세요 (예: types/kakao.d.ts):

interface Window {
  Kakao: {
    Auth: {
      authorize: (options: { redirectUri: string }) => Promise<void>
    }
  }
}

에러 처리를 개선하세요:

  } catch (error) {
-   console.log(error)
+   console.error('Kakao login failed:', error)
+   alert('로그인에 실패했습니다. 다시 시도해주세요.')
+   throw error
  }
🤖 Prompt for AI Agents
In apps/web/app/_apis/services/login.ts around lines 8 to 22, this function
lacks type safety for window.Kakao, swallows errors to console only, and leaves
commented iOS detection; add a global type declaration file (e.g.,
types/kakao.d.ts) that declares window.Kakao with Auth.authorize signature,
remove or implement the commented iOS detection (use the same regex as Android
counterpart), add a runtime guard to check window.Kakao before calling authorize
and fall back to the redirect URL for non-present SDKs, and replace console.log
with user-facing error handling (e.g., throw a typed error or call a toast/error
modal) while still logging the error details for diagnostics.


export const getToken = async (): Promise<{
tokenType: string
accessToken: string
accessTokenExpiresIn: number
}> => {
try {
const res = await axios.post(
process.env.NEXT_PUBLIC_API_URL + API_PATH.AUTH.TOKEN,
{},
{
withCredentials: true,
},
)

const { data } = res
const { accessToken, accessTokenExpiresIn } = data.data
const expireDate = new Date(Date.now() + accessTokenExpiresIn)

setCookie('accessToken', accessToken, {
expires: expireDate,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
})

return data.data
} catch (error) {
console.error('토큰 재발급 실패(세션 만료):', error)
deleteCookie('accessToken')

if (typeof window !== 'undefined') {
window.location.href = CLIENT_PATH.LOGIN
}

throw error
}
}
Comment on lines +24 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

getToken 반환 타입과 실제 반환값이 일치하지 않습니다 (Critical).

Line 24-28의 타입 선언은 평탄화된 객체 { tokenType, accessToken, accessTokenExpiresIn }를 반환한다고 명시하지만, Line 49는 res.data를 그대로 반환합니다. Line 39에서 data.data로 중첩된 구조를 사용하는 것을 보면 실제 반환값은 { data: { accessToken, ... } } 형태입니다.

이로 인해 apps/web/app/_lib/axiosInstance.ts의 Line 40에서 const { accessToken: newAccessToken } = await getToken()로 구조분해할 때 newAccessTokenundefined가 되어 토큰 갱신이 실패합니다.

다음 중 하나를 적용하여 수정하세요:

방법 1: 반환 타입과 일치하도록 평탄화된 객체 반환 (권장)

    setCookie('accessToken', accessToken, {
      expires: expireDate,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      path: '/',
    })

-   return res.data
+   return data.data
  } catch (error) {

방법 2: 타입 선언을 실제 반환값과 일치시키기

-export const getToken = async (): Promise<{
-  tokenType: string
-  accessToken: string
-  accessTokenExpiresIn: number
-}> => {
+export const getToken = async (): Promise<{
+  data: {
+    tokenType: string
+    accessToken: string
+    accessTokenExpiresIn: number
+  }
+}> => {

그리고 axiosInstance.ts의 사용처도 수정:

- const { accessToken: newAccessToken } = await getToken()
+ const { data: { accessToken: newAccessToken } } = await getToken()

방법 1을 권장합니다.

7 changes: 7 additions & 0 deletions apps/web/app/_constants/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export const API_PATH = {
LIST: '/requests/places',
DETAIL: (id: string) => `/requests/places/${id}`,
},
AUTH: {
AUTHORIZE: (code: string, redirectUri: string) =>
`/auth/oauth2?code=${code}&redirectUri=${redirectUri}`,
TOKEN: '/auth/token',
},
}

export const CLIENT_PATH = {
Expand All @@ -56,4 +61,6 @@ export const CLIENT_PATH = {
EVENTS_LUCKY_DRAW: '/events/lucky-draw',
EVENT_GIFTICON: '/events/gifticon',
EVENT_GIFTICON_DETAIL: (id: string | number) => `/events/gifticon/${id}`,
LOGIN: '/login',
LOGIN_SUCCESS: '/login/loading/success', // Todo: path 변경 필요
}
51 changes: 50 additions & 1 deletion apps/web/app/_lib/axiosInstance.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,57 @@
import axios from 'axios'
import axios, {
AxiosError,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios'
import { getToken } from '@/_apis/services/login'
import { getCookie } from '@/_utils/getCookie'

const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
})

axiosInstance.interceptors.request.use(
async (config) => {
const token = await getCookie('accessToken')

if (token) {
config.headers.Authorization = `Bearer ${token}`
}

return config
},
(error) => Promise.reject(error),
)

axiosInstance.interceptors.response.use(
(response: AxiosResponse) => response.data,
async (error: AxiosError): Promise<AxiosResponse | AxiosError> => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean
}
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true

try {
const { accessToken: newAccessToken } = await getToken()

if (originalRequest.headers) {
originalRequest.headers.Authorization = newAccessToken
? `Bearer ${newAccessToken}`
: ''
}

return await axiosInstance(originalRequest)
} catch (refreshError) {
return Promise.reject(refreshError)
}
}
return Promise.reject(error)
},
)
Comment on lines +30 to +55

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

401 처리부의 getToken 반환 구조 및 타입 불일치 가능성

401 응답 처리 로직에서 다음과 같이 getToken 결과를 구조분해하고 있습니다:

const { accessToken: newAccessToken } = await getToken()

하지만 제공된 getToken 구현 스니펫을 보면 return res.data를 반환하고 있어, 실제 구조가 { data: { accessToken, ... } } 형태일 수 있습니다. 이 경우:

  • newAccessTokenundefined가 되어 의도대로 Authorization 헤더를 설정하지 못할 수 있습니다.
  • 다만 getToken 내부에서 이미 setCookie('accessToken', accessToken)를 수행하므로, 재시도 요청에서는 request 인터셉터가 쿠키에서 토큰을 읽어와 보정해주는 형태가 됩니다. 즉 “우연히 동작하긴 하지만 코드 간의 계약이 어긋난 상태”입니다.

또한 이 인터셉터는 성공 시 response.data를 반환하지만, 에러 핸들러 시그니처는 Promise<AxiosResponse | AxiosError>로 선언되어 있어 실제 반환 타입과 어긋납니다. 나중에 제너릭 타입을 붙여 사용할 때 타입 추론이 꼬일 수 있습니다.

권장 사항:

  • getToken이 실제로 어떤 shape을 반환하는지(평탄화된 { accessToken, ... }인지, data.data 구조인지)를 이 파일과 일치시키고, 타입 선언도 맞춰 주세요.
  • 이 인터셉터에서의 반환 타입도 AxiosResponse 대신 “실제 data 타입”을 반영하도록 정리하는 편이 이후 DX에 도움이 됩니다.

getToken의 실제 반환 타입과 이곳 구조분해가 일치하는지, 그리고 axiosInstance를 사용하는 쪽에서 기대하는 응답 타입과 현재 인터셉터 반환값이 일관적인지 다시 한 번 점검해 주세요.


I'll verify the review comment by examining the getToken function, the interceptor type signatures, and how axiosInstance is used throughout the codebase.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find the getToken function definition and its return type
fd -t f "\.(ts|tsx|js|jsx)$" | xargs rg -n "export.*function.*getToken|const.*getToken.*=" -A 10 --type=ts --type=tsx | head -100

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Find imports and usages of axiosInstance to understand expected response types
fd -t f "\.(ts|tsx)$" | xargs rg -n "axiosInstance" -B 2 -A 5 --type=ts --type=tsx | head -150

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Check the full axios interceptor setup and response type declarations
cat apps/web/app/_lib/axiosInstance.ts

Length of output: 1667


🏁 Script executed:

#!/bin/bash
# Search for getToken function and setCookie usage to verify the implementation
fd -t f "\.(ts|tsx)$" | xargs rg -n "setCookie.*accessToken|getToken" -B 3 -A 10 --type=ts --type=tsx | head -150

Length of output: 27



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the login service file
fd -t f "login\.(ts|js)" --path "*/_apis/*"

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Search for getToken in all TypeScript files using correct rg syntax
rg -n "export.*function.*getToken|export.*const.*getToken" -A 15 --type ts

Length of output: 1017


🏁 Script executed:

#!/bin/bash
# Find all files in _apis directory
fd --path "*/_apis/*" -t f

Length of output: 215



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the complete getToken implementation
cat apps/web/app/_apis/services/login.ts

Length of output: 1582


🏁 Script executed:

#!/bin/bash
# Search for usages of axiosInstance in the codebase to check how responses are typed
rg -n "axiosInstance\." --type ts -A 3 -B 1 | head -100

Length of output: 5814



---

🏁 Script executed:

```shell
#!/bin/bash
# Verify getToken's actual return type annotation matches implementation
rg -n "return res\.data" apps/web/app/_apis/services/login.ts -B 5 -A 2

Length of output: 191


🏁 Script executed:

#!/bin/bash
# Check if there are any test files or type validation for getToken
rg -n "getToken" apps/web/app --type ts | grep -E "test|spec|mock" | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at how axiosInstance response is typed when used
rg -n "await axiosInstance" --type ts -B 2 -A 5 | head -80

Length of output: 5030


getToken 반환 타입과 response interceptor 타입 선언 불일치 - 검증 완료

검증 결과 리뷰의 우려사항이 확인되었습니다:

  1. getToken 반환 타입 불일치 (apps/web/app/_apis/services/login.ts 46줄):

    • 선언된 타입: Promise<{ tokenType: string; accessToken: string; accessTokenExpiresIn: number }>
    • 실제 반환값: res.data (중첩 구조 { data: { accessToken, ... } })
    • axiosInstance 라인 49의 구조분해 const { accessToken: newAccessToken } = await getToken()는 최상위 레벨의 accessToken을 기대하지만, 실제로는 undefined가 됨
    • 요청 인터셉터에서 쿠키로부터 토큰을 읽어서 우연히 동작함
  2. Response interceptor 타입 선언 오류 (apps/web/app/_lib/axiosInstance.ts 40줄):

    • 에러 핸들러 시그니처: Promise<AxiosResponse | AxiosError>
    • 실제 반환: 성공 시 data 객체, 실패 시 Promise.reject()
    • 사용처들은 const { data } = await axiosInstance(...)로 구조분해하고 있음

필수 수정 사항:

  • getToken 반환 타입을 실제 반환값과 일치시키거나, 구현을 타입과 맞춰서 평탄화된 객체를 반환하도록 수정
  • Response interceptor의 제너릭 타입 선언을 정리하여 실제 반환 타입과 일치
🤖 Prompt for AI Agents
In apps/web/app/_lib/axiosInstance.ts around lines 38 to 63, the response
interceptor and getToken usage are mismatched with actual shapes: getToken
currently returns a nested { data: { accessToken, ... } } but the interceptor
expects a flat { accessToken }, and the interceptor error handler return type
Promise<AxiosResponse | AxiosError> is incorrect for an instance that resolves
to response.data; fix by updating getToken to return the flattened shape
(Promise<{ tokenType: string; accessToken: string; accessTokenExpiresIn: number
}>) or change its caller here to extract accessToken from the nested data (e.g.,
const { data: { accessToken } } = await getToken()), and update the interceptor
typings to reflect the instance resolves to the data payload (adjust
generic/return types to the data type or Promise<any> and ensure the error
handler returns Promise.reject(error) on failure) so runtime behavior and
TypeScript signatures align.


export default axiosInstance
8 changes: 8 additions & 0 deletions apps/web/app/_lib/axiosInstanceV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import axios from 'axios'

const axiosInstanceV2 = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL_V2,
withCredentials: true,
})
Comment on lines +3 to +6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

환경 변수가 정의되지 않은 경우를 처리하세요.

NEXT_PUBLIC_API_URL_V2가 정의되지 않은 경우 axios 인스턴스의 baseURLundefined가 되어 모든 API 요청이 실패합니다.

다음과 같이 환경 변수 검증을 추가하세요:

  import axios from 'axios'

+ const apiUrlV2 = process.env.NEXT_PUBLIC_API_URL_V2
+ if (!apiUrlV2) {
+   throw new Error('NEXT_PUBLIC_API_URL_V2 environment variable is not defined')
+ }
+
  const axiosInstanceV2 = axios.create({
-   baseURL: process.env.NEXT_PUBLIC_API_URL_V2,
+   baseURL: apiUrlV2,
    withCredentials: true,
  })

  export default axiosInstanceV2
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const axiosInstanceV2 = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL_V2,
withCredentials: true,
})
import axios from 'axios'
const apiUrlV2 = process.env.NEXT_PUBLIC_API_URL_V2
if (!apiUrlV2) {
throw new Error('NEXT_PUBLIC_API_URL_V2 environment variable is not defined')
}
const axiosInstanceV2 = axios.create({
baseURL: apiUrlV2,
withCredentials: true,
})
export default axiosInstanceV2
🤖 Prompt for AI Agents
In apps/web/app/_lib/axiosInstanceV2.ts around lines 3 to 6, the axios instance
is created with baseURL set directly to process.env.NEXT_PUBLIC_API_URL_V2 which
can be undefined and cause all requests to fail; validate the environment
variable at module initialization and either throw a clear error (so the
developer notices misconfiguration) or set a sensible fallback, and then create
the axios instance using the validated value; ensure the validation uses a
single source (e.g. const BASE_URL = process.env.NEXT_PUBLIC_API_URL_V2) and
reference that when creating the instance so you don't risk passing undefined.


export default axiosInstanceV2
35 changes: 35 additions & 0 deletions apps/web/app/_utils/getCookie/getCookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getCookie as ClientGetCookie } from 'cookies-next'

/**
* 서버(Next.js App Router)와 클라이언트(Browser) 환경 모두에서 작동하는 하이브리드 쿠키 조회 함수입니다.
*
* @remarks
* 이 함수는 실행 환경을 자동으로 감지하여 적절한 방식으로 쿠키를 가져옵니다.
* - **Server Environment**: `next/headers`의 `cookies()`를 동적으로 임포트하여 조회합니다. (클라이언트 번들링 제외)
* - **Client Environment**: `cookies-next` 라이브러리를 사용하여 브라우저 쿠키를 조회합니다.
* - `next/headers`의 비동기 특성에 맞춰 인터페이스 통일을 위해 항상 `Promise`를 반환합니다.
*
* @param name - 조회할 쿠키의 키(Key) 이름 (예: 'accessToken')
* @returns 쿠키의 값(`string`)을 반환하며, 쿠키가 존재하지 않을 경우 `undefined`를 반환합니다.
*
* @example
* ```typescript
* // async 함수 내부에서 사용
* const token = await getCookie('accessToken');
*
* if (token) {
* console.log('Token exists:', token);
* }
* ```
*/
export const getCookie = async (name: string): Promise<string | undefined> => {
// 서버 환경 (Next.js Server Component / Server Action / Route Handler)
if (typeof window === 'undefined') {
const { cookies } = await import('next/headers')
const cookieStore = await cookies()
return cookieStore.get(name)?.value
}

// 클라이언트 환경 (Browser)
return ClientGetCookie(name) as string | undefined
}
1 change: 1 addition & 0 deletions apps/web/app/_utils/getCookie/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getCookie } from './getCookie'
10 changes: 10 additions & 0 deletions apps/web/app/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
interface Window {
Kakao: {
init: (appKey: string) => void
isInitialized: () => boolean
Auth: {
authorize: (params: Record<string, unknown>) => Promise<void>
}
[key: string]: unknown
}
}
7 changes: 7 additions & 0 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { MSWProvider } from '@/_mocks/MSWProvider'
import { Column } from '@repo/ui/components/Layout'
import { NaverMapProvider } from '@/NaverMapProvider'
import { HeroProvider } from '@/HeroProvider'
import Script from 'next/script'

const SITE_URL = new URL('https://knu-matzip.vercel.app')

Expand Down Expand Up @@ -60,6 +61,12 @@ export default async function RootLayout({
</HeroProvider>
</QueryProvider>
</MSWProvider>
<Script
src='https://t1.kakaocdn.net/kakao_js_sdk/2.7.9/kakao.min.js'
integrity='sha384-JpLApTkB8lPskhVMhT+m5Ln8aHlnS0bsIexhaak0jOhAkMYedQoVghPfSpjNi9K1'
crossOrigin='anonymous'
strategy='lazyOnload'
/>
</body>
</html>
)
Expand Down
28 changes: 28 additions & 0 deletions apps/web/app/login/components/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'

import { useEffect } from 'react'
import { Icon } from '@repo/ui/components/Icon'
import { Text } from '@repo/ui/components/Text'
import { getKakaoInga } from '@/_apis/services/login'

export const LoginButton = () => {
useEffect(() => {
if (!window.Kakao) return
if (!window.Kakao.isInitialized()) {
window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY || '')
console.log('Kakao initialized')
} else {
console.log('Kakao already initialized')
}
}, [])

return (
<button
onClick={getKakaoInga}
className={'relative rounded-lg bg-[#FEE500] p-3 text-center'}
>
<Icon type={'kakaoLogo'} className={'absolute left-[20px]'} />
<Text>카카오 로그인</Text>
</button>
)
}
45 changes: 45 additions & 0 deletions apps/web/app/login/loading/success/_route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// import { NextRequest, NextResponse } from 'next/server'
// import axiosInstanceV2 from '@/_lib/axiosInstanceV2'
// import { API_PATH, CLIENT_PATH } from '@/_constants/path'
//
// export const GET = async (request: NextRequest) => {
// try {
// const searchParams = request.nextUrl.searchParams
// const code = searchParams.get('code') || ''
// const redirectUri =
// (process.env.NEXT_PUBLIC_CLIENT_URL || '') + CLIENT_PATH.LOGIN_SUCCESS
//
// // 백엔드로 OAuth 인증 요청
// const response = await axiosInstanceV2.get(
// API_PATH.AUTH.AUTHORIZE(code, redirectUri),
// )
//
// // 브라우저로 리다이렉트 응답 준비
// const nextRes = NextResponse.redirect(
// new URL(CLIENT_PATH.MAIN, request.url),
// )
//
// const accessToken = response?.data?.data
// if (accessToken) {
// nextRes.cookies.set('accessToken', accessToken, {
// path: '/',
// httpOnly: false,
// })
// }
//
// const backendCookies = response.headers['set-cookie']
// if (backendCookies) {
// backendCookies.forEach((cookie) => {
// nextRes.headers.append('Set-Cookie', cookie)
// })
// }
//
// return nextRes
// } catch (error) {
// console.log(error)
// return NextResponse.json(
// { message: 'Internal Server Error' },
// { status: 500 },
// )
// }
// }
52 changes: 52 additions & 0 deletions apps/web/app/login/loading/success/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client'

import { useEffect } from 'react'
import { Spinner } from '@heroui/react'
import { useSearchParams, useRouter } from 'next/navigation'
import { setCookie } from 'cookies-next'
import axiosInstanceV2 from '@/_lib/axiosInstanceV2'
import { API_PATH, CLIENT_PATH } from '@/_constants/path'

const Page = () => {
const { replace } = useRouter()
const searchParams = useSearchParams()
const code = searchParams.get('code') || ''
const clientUrl = process.env.NEXT_PUBLIC_CLIENT_URL || ''
const redirectUri = clientUrl + CLIENT_PATH.LOGIN_SUCCESS

useEffect(() => {
if (!code || !clientUrl) {
console.error('Authorization code is missing')
replace(`${CLIENT_PATH.LOGIN}?error=code-missing`)
return
}

;(async () => {
try {
const response = await axiosInstanceV2.get(
API_PATH.AUTH.AUTHORIZE(code, redirectUri),
)

const accessToken = response?.data?.data
if (accessToken) {
setCookie('accessToken', accessToken, {
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
})
replace(CLIENT_PATH.MAIN)
} else {
console.error('Access token missing in response')
replace(`${CLIENT_PATH.LOGIN}?error=token-missing`)
}
} catch (error) {
console.error('Login process failed:', error)
replace(`${CLIENT_PATH.LOGIN}?error=auth-failed`)
}
})()
}, [clientUrl, code, redirectUri, replace])

return <Spinner className={'m-auto'} />
}

export default Page
21 changes: 21 additions & 0 deletions apps/web/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Icon } from '@repo/ui/components/Icon'
import { Column } from '@repo/ui/components/Layout'
import { Text } from '@repo/ui/components/Text'
import { LoginButton } from './components/LoginButton'

const LoginPage = () => {
return (
<Column className={'h-full px-5 py-10'}>
<Column className={'my-auto items-center pb-5'}>
<Icon type={'logo'} size={150} className={'mb-2'} />
<Text fontSize={'2xl'}>공주대학교</Text>
<Text fontSize={'2xl'} fontWeight={'bold'}>
맛집
</Text>
</Column>
<LoginButton />
</Column>
)
}

export default LoginPage
3 changes: 2 additions & 1 deletion apps/web/app/map/MapContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client'

import dynamic from 'next/dynamic'
import { Spinner } from '@heroui/react'

const MapComponent = dynamic(() => import('./MapComponent'), {
ssr: false,
loading: () => <div>지도 로딩 중...</div>,
loading: () => <Spinner className={'m-auto'} />,
})

export const MapContainer = () => <MapComponent />
2 changes: 1 addition & 1 deletion apps/web/app/map/_components/PlaceList/PlaceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const PlaceList = ({ places }: { places: PlaceByMap[] }) => {
expandOnContentDrag
>
{places.length > 0 ? (
<ul className={'px-5'}>
<ul className={'pb-15 px-5'}>
{places.map((place) => (
<PlaceListItem key={place.placeId} {...place} />
))}
Expand Down
Loading