Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3761645
Merge pull request #103 from JECT-Study/develop
kimYunHyeong May 6, 2026
795f70d
feat(auth): /auth/login GET → POST 방식으로 변경 및 reissue 응답 스펙 반영
kimYunHyeong May 6, 2026
63a5032
Merge branch 'feature/login' into main
kimYunHyeong May 6, 2026
e737ac2
fix(login): 로그인 로직 수정(GET->POST)에 따른 수정
kimYunHyeong May 6, 2026
895e9d1
Merge branch 'feature/login'
kimYunHyeong May 7, 2026
3829a3f
백엔드 로그 수집
kimYunHyeong May 8, 2026
a8b02e6
Merge branch 'feature/login'
kimYunHyeong May 8, 2026
c7ed695
Merge branch 'develop' into feature/login
kimYunHyeong May 9, 2026
4a3cf07
fix(login): COOP 에러 해결
kimYunHyeong May 9, 2026
415d9ee
Merge branch 'feature/login'
kimYunHyeong May 9, 2026
fa753c9
Update route.ts
kimYunHyeong May 9, 2026
49979f0
Merge branch 'feature/login'
kimYunHyeong May 9, 2026
da22a7e
refactor(login): 헤더 origin추가
kimYunHyeong May 9, 2026
b3fe6de
Merge branch 'feature/login'
kimYunHyeong May 9, 2026
a536257
Update route.ts
kimYunHyeong May 10, 2026
d5b9690
Merge branch 'feature/login'
kimYunHyeong May 10, 2026
854201a
Update route.ts
kimYunHyeong May 10, 2026
bcbfad9
Update route.ts
kimYunHyeong May 10, 2026
2ab9622
Update route.ts
kimYunHyeong May 10, 2026
eb13007
Update route.ts
kimYunHyeong May 10, 2026
8d9cdc3
Update usePopupOAuth.ts
kimYunHyeong May 10, 2026
fa6b690
Update usePopupOAuth.ts
kimYunHyeong May 10, 2026
0c1c32a
coop-> localstorage
kimYunHyeong May 10, 2026
7c3266d
local->coop rollback
kimYunHyeong May 10, 2026
b25193c
Merge branch 'main' into feature/login
kimYunHyeong May 10, 2026
00aecbd
Update route.ts
kimYunHyeong May 10, 2026
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
62 changes: 40 additions & 22 deletions app/auth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,40 @@ export async function GET(request: NextRequest) {

cookieStore.delete('oauth_state')

const origin = process.env.NEXT_PUBLIC_APP_URL!

if (error || !code || !state || state !== storedState) {
const errorMessage = error || 'OAuth 검증에 실패했습니다.'
return new NextResponse(
buildPostMessageHtml('AUTH_ERROR', origin, { error: errorMessage }),
buildPostMessageHtml('AUTH_ERROR', { error: errorMessage }),
{
headers: { 'Content-Type': 'text/html' },
headers: {
'Content-Type': 'text/html',
'Cross-Origin-Opener-Policy': 'unsafe-none',
},
}
)
}

// authorization code를 백엔드 GET /auth/login에 Query Parameter로 전달
// authorization code를 백엔드 POST /auth/login에 JSON body로 전달
try {
const backendResponse = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/login?provider=google&code=${code}`,
{ method: 'GET' }
`${process.env.NEXT_PUBLIC_API_URL}/auth/login`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Origin: process.env.NEXT_PUBLIC_APP_URL!,
},
body: JSON.stringify({ provider: 'google', code }),
}
)

if (!backendResponse.ok) {
throw new Error('백엔드 인증에 실패했습니다.')
const errText = await backendResponse.text()
throw new Error(`백엔드 ${backendResponse.status}: ${errText}`)
}

const data: LoginResponse = await backendResponse.json()
console.log('[auth/callback] backend data:', JSON.stringify(data))

if (!data.success) {
throw new Error(
Expand All @@ -50,7 +60,7 @@ export async function GET(request: NextRequest) {
}

if (typeof data.responseDto !== 'object') {
throw new Error('백엔드 인증에 실패했습니다.')
throw new Error(`responseDto 타입 오류: ${typeof data.responseDto}`)
}

const { accessToken, userDetails, userChannelDetails } = data.responseDto
Expand All @@ -63,7 +73,7 @@ export async function GET(request: NextRequest) {
if (refreshToken) {
cookieStore.set('refreshToken', refreshToken, {
httpOnly: true,
secure: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 7,
Expand All @@ -73,34 +83,42 @@ export async function GET(request: NextRequest) {
const user = { userDetails, userChannelDetails: userChannelDetails ?? null }

return new NextResponse(
buildPostMessageHtml('AUTH_SUCCESS', origin, {
buildPostMessageHtml('AUTH_SUCCESS', {
accessToken,
user,
}),
{ headers: { 'Content-Type': 'text/html' } }
{
headers: {
'Content-Type': 'text/html',
'Cross-Origin-Opener-Policy': 'unsafe-none',
},
}
)
} catch {
} catch (e) {
console.error('[auth/callback] error:', e)
return new NextResponse(
buildPostMessageHtml('AUTH_ERROR', origin, {
error: '로그인 처리 중 오류가 발생했습니다.',
buildPostMessageHtml('AUTH_ERROR', {
error: e instanceof Error ? e.message : String(e),
}),
{ headers: { 'Content-Type': 'text/html' } }
{
headers: {
'Content-Type': 'text/html',
'Cross-Origin-Opener-Policy': 'unsafe-none',
},
}
)
}
}

//HTML 페이지를 반환하여 postMessage로 accessToken + user를 부모 창에 전달하고 팝업을 닫는다
function buildPostMessageHtml(
type: string,
origin: string,
payload: Record<string, unknown>
) {
function buildPostMessageHtml(type: string, payload: Record<string, unknown>) {
const targetOrigin = process.env.NEXT_PUBLIC_APP_URL!
const message = JSON.stringify({ type, ...payload })
return `<!DOCTYPE html>
<html>
<body>
<script>
window.opener.postMessage(${message}, "${origin}");
window.opener.postMessage(${message}, "${targetOrigin}");
window.close();
</script>
</body>
Expand Down
11 changes: 1 addition & 10 deletions app/auth/refresh/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
mockRefreshToken,
mockNewRefreshToken,
mockReissueResponse,
mockUserDetails,
} from '@/shared/api/mock/mockUser'

const mockCookieStore = {
Expand Down Expand Up @@ -80,7 +79,7 @@ describe('POST /auth/reissue', () => {
)
})

it('백엔드 성공 시 { accessToken, user }를 반환한다', async () => {
it('백엔드 성공 시 { accessToken }를 반환한다', async () => {
mockCookieStore.get.mockReturnValue({ value: mockRefreshToken })
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
new Response(JSON.stringify(mockReissueResponse), { status: 200 })
Expand All @@ -91,14 +90,6 @@ describe('POST /auth/reissue', () => {
const data = await response.json()

expect(data.accessToken).toBe(mockAccessToken)
expect(data.user).toEqual(
expect.objectContaining({
userDetails: expect.objectContaining({
id: mockUserDetails.id,
isOnboardingCompleted: false,
}),
})
)
})

it('백엔드 실패 시 RT 쿠키를 삭제하고 401을 반환한다', async () => {
Expand Down
19 changes: 11 additions & 8 deletions app/auth/refresh/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,28 @@ export async function POST() {
`${process.env.NEXT_PUBLIC_API_URL}/auth/reissue`,
{
method: 'POST',
headers: { Cookie: `refreshToken=${refreshToken}` },
headers: {
Cookie: `refreshToken=${refreshToken}`,
Origin: process.env.NEXT_PUBLIC_APP_URL!,
},
}
)

const responseText = await backendResponse.text()

console.log('[auth/refresh] backend status:', backendResponse.status)
console.log('[auth/refresh] backend body:', responseText)

if (!backendResponse.ok) {
cookieStore.delete('refreshToken')
return NextResponse.json(
{ error: '토큰 갱신에 실패했습니다.' },
{ status: 401 }
{ status: backendResponse.status }
)
}

const { responseDto } = JSON.parse(responseText)
const { accessToken, userDetails, userChannelDetails } = responseDto
const { accessToken } = responseDto

// 백엔드가 새 refreshToken을 Set-Cookie로 내려주는 경우 재설정
const setCookieHeader = backendResponse.headers.get('set-cookie')
Expand All @@ -44,17 +50,14 @@ export async function POST() {
if (newRefreshToken) {
cookieStore.set('refreshToken', newRefreshToken, {
httpOnly: true,
secure: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 7,
})
}

const user = { userDetails, userChannelDetails: userChannelDetails ?? null }

//새 AT와 user 정보는 response body로 전달
return NextResponse.json({ accessToken, user })
return NextResponse.json({ accessToken })
} catch {
return NextResponse.json(
{ error: '토큰 갱신 중 오류가 발생했습니다.' },
Expand Down
12 changes: 12 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
/* Cross-Origin-Opener-Policy로 인해 팝업이 닫히지 않는 문제 해결 */
async headers() {
return [
{
source: '/auth/callback',
headers: [
{ key: 'Cross-Origin-Opener-Policy', value: 'unsafe-none' },
{ key: 'Cross-Origin-Embedder-Policy', value: 'unsafe-none' },
],
},
]
},
webpack(config) {
const fileLoaderRule = config.module.rules.find(
(rule: { test?: { test?: (s: string) => boolean } }) =>
Expand Down
9 changes: 5 additions & 4 deletions src/features/auth/mock/mockAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
} from '@/shared/api/mock/mockUser'

import type { LoginResponse } from '../model/types'
import type { ApiResponse } from '@/shared/api/types'

/* 로그인 응답 DTO */
const mockResponseDto = {
const mockLoginResponseDto = {
accessToken: mockAccessToken,
userDetails: mockUserDetails,
userChannelDetails: mockUserChannelDetails,
Expand All @@ -16,14 +17,14 @@ const mockResponseDto = {
/* 로그인 응답 */
export const mockLoginResponse: LoginResponse = {
success: true,
responseDto: mockResponseDto,
responseDto: mockLoginResponseDto,
error: null,
}

/* 리프레시 응답 */
export const mockReissueResponse: LoginResponse = {
export const mockReissueResponse: ApiResponse<{ accessToken: string } | string> = {
success: true,
responseDto: mockResponseDto,
responseDto: { accessToken: mockAccessToken },
error: null,
}

Expand Down
8 changes: 1 addition & 7 deletions src/features/auth/model/usePopupOAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function usePopupOAuth({ apiPath, popupName }: PopupOAuthConfig) {
const { type, error: authError } = event.data

if (type === 'AUTH_SUCCESS') {
setIsLoading(false)
window.location.href = '/'
} else if (type === 'AUTH_ERROR') {
setError(authError || '로그인에 실패했습니다.')
Expand Down Expand Up @@ -54,13 +55,6 @@ export function usePopupOAuth({ apiPath, popupName }: PopupOAuthConfig) {
}

popupRef.current = popup

const timer = setInterval(() => {
if (popup.closed) {
clearInterval(timer)
setIsLoading(false)
}
}, 500)
}

return { isLoading, error, handleClick }
Expand Down
2 changes: 0 additions & 2 deletions src/shared/api/mock/mockUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,5 @@ export const mockReissueResponse = {
error: null,
responseDto: {
accessToken: mockAccessToken,
userDetails: mockUserDetails,
userChannelDetails: mockUserChannelDetails,
},
}