diff --git a/public/svgs/GoogleIcon.svg b/public/assets/GoogleIcon.svg similarity index 100% rename from public/svgs/GoogleIcon.svg rename to public/assets/GoogleIcon.svg diff --git a/public/svgs/KakaoIcon.svg b/public/assets/KakaoIcon.svg similarity index 100% rename from public/svgs/KakaoIcon.svg rename to public/assets/KakaoIcon.svg diff --git a/src/pages/auth/callback.tsx b/src/pages/auth/callback.tsx new file mode 100644 index 0000000..fce8549 --- /dev/null +++ b/src/pages/auth/callback.tsx @@ -0,0 +1,41 @@ +'use client'; +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { exchangeTempToken } from '@/shared/api/auth'; +import Loading from '@/pages/loading'; + +export default function AuthCallbackPage() { + const router = useRouter(); + + useEffect(() => { + if (!router.isReady) return; + + const searchParams = new URLSearchParams(router.asPath.split('?')[1]); + const tempToken = searchParams.get('temp_token'); + console.log('tempToken:', tempToken); + + if (!tempToken) return; + + const handleLogin = async () => { + try { + const res = await exchangeTempToken(tempToken); + console.log('API 응답:', res); + + if (res.success) { + console.log('redirect to main 페이지'); + router.replace('/main'); + } else { + alert(res.message || '로그인에 실패했습니다.'); + router.replace('/auth'); + } + } catch (error) { + console.error('오류:', error); + router.replace('/auth'); + } + }; + + handleLogin(); + }, [router.isReady, router.asPath]); + + return ; +} diff --git a/src/pages/auth/index.tsx b/src/pages/auth/index.tsx index 89e40c0..d0d3db9 100644 --- a/src/pages/auth/index.tsx +++ b/src/pages/auth/index.tsx @@ -1,17 +1,49 @@ 'use client'; -import { useState } from 'react'; +import Image from 'next/image'; import { Icon } from '@/shared/icons'; +import { useRouter } from 'next/router'; import { cn } from '@/shared/lib'; -import LoginButton from '@/pages/auth/components/LoginButton'; -import RecentLoginBubble from '@/pages/auth/components/RecentLoginBubble'; +import LoginButton from '@/shared/components/auth/LoginButton'; +import RecentLoginBubble from '@/shared/components/auth/RecentLoginBubble'; +import { useRecentLogin } from '@/shared/hooks/useRecentLogin'; export default function LoginPage() { - const [recentPlatform, setRecentPlatform] = useState(null); + const { recentPlatform, saveRecentPlatform } = useRecentLogin(); + const router = useRouter(); + const PLATFORM = { + KAKAO: 'kakao', + GOOGLE: 'google', + } as const; - const handleLoginClick = (platform: string) => { - alert(`${platform} 로그인 준비중`); - console.log(`${platform} 로그인 버튼 클릭`); - setRecentPlatform(platform); + const PLATFORM_DISPLAY = { + [PLATFORM.KAKAO]: '카카오', + [PLATFORM.GOOGLE]: '구글', + } as const; + + //로그인 + const handleLoginClick = (platformDisplay: string) => { + const platform = Object.entries(PLATFORM_DISPLAY).find( + ([_, display]) => display === platformDisplay, + )?.[0]; + if (!platform) return; + + saveRecentPlatform(platform); + const base = process.env.NEXT_PUBLIC_BACKEND_URL; + if (!base) { + console.error('NEXT_PUBLIC_BACKEND_URL is not defined'); + return; + } + const url = `${base}/oauth2/authorization/${platform}`; + window.location.href = url; + }; + + //비회원 로그인 + const handleGuestClick = () => { + if (document.referrer && document.referrer !== window.location.href) { + router.back(); + } else { + router.push('/main'); + } }; return ( @@ -19,8 +51,8 @@ export default function LoginPage() { {/* 그라데이션 영역 */}
-

글다

-

+ {/* 로고 영역 */} +

+ +

만화 속 부천 여행
10개 명소를 탐험하고 엽서를 모아보세요!

- {/* 로고 */} -
- -
- -
+

start with

@@ -80,7 +107,7 @@ export default function LoginPage() { /> {recentPlatform === '카카오' && (
@@ -96,7 +123,7 @@ export default function LoginPage() { /> {recentPlatform === '구글' && (
@@ -105,14 +132,18 @@ export default function LoginPage() {
-

+

비회원 로그인

+ {/* 안내문 */} -

- 비회원은 스탬프 저장과 공유 기능을 사용할 수 없습니다. -

+
+

비회원은 스탬프 저장과 공유 기능을 사용할 수 없습니다.

+
); diff --git a/src/shared/api/auth.ts b/src/shared/api/auth.ts new file mode 100644 index 0000000..a8887d0 --- /dev/null +++ b/src/shared/api/auth.ts @@ -0,0 +1,17 @@ +import { apiAuth } from '@/shared/api/instance'; +import type { ApiResponse, TokenData } from '@/shared/types/authtypes'; +import { setTokens } from '@/shared/utils/token'; + +export const exchangeTempToken = async (tempToken: string) => { + const { data } = await apiAuth.post>( + '/api/auth/temp-token/exchange', + { tempToken }, + ); + + if (data.success) { + const { accessToken, refreshToken } = data.data; + setTokens(accessToken, refreshToken); + } + + return data; +}; diff --git a/src/shared/api/instance.ts b/src/shared/api/instance.ts index a1e6cd1..49f8f12 100644 --- a/src/shared/api/instance.ts +++ b/src/shared/api/instance.ts @@ -6,7 +6,7 @@ import { setTokens, clearTokens, } from '@/shared/utils/token'; -import type { ApiResponse, TokenData } from '@/shared/api/types'; +import type { ApiResponse, TokenData } from '@/shared/types/authtypes'; const BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL; @@ -50,7 +50,9 @@ const processQueue = (error: unknown, token: string | null = null) => { apiWithToken.interceptors.response.use( (response) => response, async (error: AxiosError>) => { - const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; + const originalRequest = error.config as InternalAxiosRequestConfig & { + _retry?: boolean; + }; if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { diff --git a/src/pages/auth/components/LoginButton.tsx b/src/shared/components/auth/LoginButton.tsx similarity index 55% rename from src/pages/auth/components/LoginButton.tsx rename to src/shared/components/auth/LoginButton.tsx index 990fe77..9d5d4ac 100644 --- a/src/pages/auth/components/LoginButton.tsx +++ b/src/shared/components/auth/LoginButton.tsx @@ -1,67 +1,91 @@ -// SVGO 해결후 Image → Icon으로 교체 예정 -'use client'; -import Image from 'next/image'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/shared/lib'; - - -const loginButtonVariants = cva( - ` - flex justify-center items-center flex-shrink-0 - w-[5rem] h-[5rem] rounded-full - shadow-[0_0_4px_rgba(0,0,0,0.30)] - transition-all duration-150 active:scale-95 - `, - { - variants: { - platform: { - google: 'bg-white', - kakao: 'bg-[#FEE500]', - }, - }, - defaultVariants: { - platform: 'google', - }, - } -); - -interface LoginButtonProps extends VariantProps { - onClick: () => void; - className?: string; -} - -export default function LoginButton({ - onClick, - platform, - className, -}: LoginButtonProps) { - const iconData = { - google: { - src: '/svgs/GoogleIcon.svg', - alt: 'Google Logo', - width: 36, - height: 36, - label: '구글 로그인', - }, - kakao: { - src: '/svgs/KakaoIcon.svg', - alt: 'Kakao Logo', - width: 28, - height: 28, - label: '카카오 로그인', - }, - }; - - const { src, alt, width, height, label } = iconData[platform ?? 'google']; - - return ( - - ); -} +'use client'; +import Image from 'next/image'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/shared/lib'; + +const loginButtonVariants = cva( + ` + flex justify-center items-center flex-shrink-0 + w-[5rem] h-[5rem] rounded-full + shadow-[0_0_4px_rgba(0,0,0,0.30)] + transition-all duration-150 active:scale-95 + `, + { + variants: { + platform: { + google: 'bg-white', + kakao: 'bg-[#FEE500]', + }, + }, + defaultVariants: { + platform: 'google', + }, + }, +); + +interface LoginButtonProps extends VariantProps { + onClick?: () => void; + className?: string; +} + +export default function LoginButton({ + onClick, + platform, + className, +}: LoginButtonProps) { + const iconData = { + google: { + src: '/assets/GoogleIcon.svg', + alt: 'Google Logo', + width: 36, + height: 36, + label: '구글 로그인', + }, + kakao: { + src: '/assets/KakaoIcon.svg', + alt: 'Kakao Logo', + width: 28, + height: 28, + label: '카카오 로그인', + }, + }; + + if (!platform || !(platform in iconData)) { + if (process.env.NODE_ENV === 'development') { + console.error(`Invalid platform: ${platform}. Falling back to google.`); + } + platform = 'google'; + } + const { src, alt, width, height, label } = + iconData[platform as keyof typeof iconData]; + + const handleClick = () => { + if (onClick) return onClick(); + const base = process.env.NEXT_PUBLIC_BACKEND_URL; + if (!base) { + const message = 'NEXT_PUBLIC_BACKEND_URL is not defined'; + if (process.env.NODE_ENV === 'development') { + throw new Error(message); + } + console.error(message); + return; + } + const url = + platform === 'kakao' + ? `${base}/oauth2/authorization/kakao` + : `${base}/oauth2/authorization/google`; + + window.location.href = url; + }; + + return ( + + ); +} diff --git a/src/pages/auth/components/RecentLoginBubble.tsx b/src/shared/components/auth/RecentLoginBubble.tsx similarity index 96% rename from src/pages/auth/components/RecentLoginBubble.tsx rename to src/shared/components/auth/RecentLoginBubble.tsx index c8429ed..f4e0e5b 100644 --- a/src/pages/auth/components/RecentLoginBubble.tsx +++ b/src/shared/components/auth/RecentLoginBubble.tsx @@ -1,29 +1,29 @@ -'use client'; -import { cva } from 'class-variance-authority'; -import { cn } from '@/shared/lib'; - -interface RecentLoginBubbleProps { - className?: string; -} - -const bubbleVariants = cva( - 'relative inline-flex justify-center items-center px-[0.8rem] py-[0.4rem] bg-pink-200 rounded-[2rem]', -); - -const RecentLoginBubble = ({ className }: RecentLoginBubbleProps) => { - return ( -
- 최근 로그인 - {/* 말풍선 꼬리 */} -
-
- ); -}; - -export default RecentLoginBubble; +'use client'; +import { cva } from 'class-variance-authority'; +import { cn } from '@/shared/lib'; + +interface RecentLoginBubbleProps { + className?: string; +} + +const bubbleVariants = cva( + 'relative inline-flex justify-center items-center px-[0.8rem] py-[0.4rem] bg-pink-200 rounded-[2rem]', +); + +const RecentLoginBubble = ({ className }: RecentLoginBubbleProps) => { + return ( +
+ 최근 로그인 + {/* 말풍선 꼬리 */} +
+
+ ); +}; + +export default RecentLoginBubble; diff --git a/src/shared/hooks/useRecentLogin.ts b/src/shared/hooks/useRecentLogin.ts new file mode 100644 index 0000000..3b47745 --- /dev/null +++ b/src/shared/hooks/useRecentLogin.ts @@ -0,0 +1,21 @@ +'use client'; +import { useEffect, useState } from 'react'; + +const RECENT_LOGIN_KEY = 'recentLoginPlatform'; + +export function useRecentLogin() { + const [recentPlatform, setRecentPlatform] = useState(null); + + useEffect(() => { + if (typeof window === 'undefined') return; + const saved = localStorage.getItem(RECENT_LOGIN_KEY); + if (saved) setRecentPlatform(saved); + }, []); + + const saveRecentPlatform = (platform: string) => { + localStorage.setItem(RECENT_LOGIN_KEY, platform); + setRecentPlatform(platform); + }; + + return { recentPlatform, saveRecentPlatform }; +} diff --git a/src/shared/icons/iconNames.ts b/src/shared/icons/iconNames.ts index 8c5ca55..23fca9f 100644 --- a/src/shared/icons/iconNames.ts +++ b/src/shared/icons/iconNames.ts @@ -12,6 +12,9 @@ export const iconNames = [ "HouseSimple", "KakaoIcon", "ListButton", + "Logo", + "LogoMint", + "LogoPink", "MapPin", "MapPin_", "NextButton", diff --git a/src/shared/icons/index.ts b/src/shared/icons/index.ts index 835330b..a50e17a 100644 --- a/src/shared/icons/index.ts +++ b/src/shared/icons/index.ts @@ -1,4 +1,5 @@ // 이 파일은 자동 생성 파일입니다. (직접 수정 금지) +import './source/backto.svg'; import './source/CalendarBlank.svg'; import './source/Caret.svg'; import './source/ChatCircle.svg'; @@ -11,6 +12,9 @@ import './source/HeartStraight.svg'; import './source/HouseSimple.svg'; import './source/KakaoIcon.svg'; import './source/ListButton.svg'; +import './source/Logo.svg'; +import './source/LogoMint.svg'; +import './source/LogoPink.svg'; import './source/MapPin.svg'; import './source/MapPin_.svg'; import './source/NextButton.svg'; @@ -18,7 +22,6 @@ import './source/PressStamp.svg'; import './source/Save.svg'; import './source/Stamp.svg'; import './source/User.svg'; -import './source/backto.svg'; import './source/x.svg'; export { Icon } from './components/icon'; diff --git a/src/shared/icons/source/CalendarBlank.svg b/src/shared/icons/source/CalendarBlank.svg index 8ab3820..0851db2 100644 --- a/src/shared/icons/source/CalendarBlank.svg +++ b/src/shared/icons/source/CalendarBlank.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/Caret.svg b/src/shared/icons/source/Caret.svg index 4db0a90..fb33445 100644 --- a/src/shared/icons/source/Caret.svg +++ b/src/shared/icons/source/Caret.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/ChatCircle.svg b/src/shared/icons/source/ChatCircle.svg index e73465d..b289096 100644 --- a/src/shared/icons/source/ChatCircle.svg +++ b/src/shared/icons/source/ChatCircle.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/Check.svg b/src/shared/icons/source/Check.svg index c4939b8..5e50825 100644 --- a/src/shared/icons/source/Check.svg +++ b/src/shared/icons/source/Check.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/CopySimple.svg b/src/shared/icons/source/CopySimple.svg index 58c1c6a..099b89c 100644 --- a/src/shared/icons/source/CopySimple.svg +++ b/src/shared/icons/source/CopySimple.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/Export.svg b/src/shared/icons/source/Export.svg index 02f8aa9..089855a 100644 --- a/src/shared/icons/source/Export.svg +++ b/src/shared/icons/source/Export.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/FadersHorizontal.svg b/src/shared/icons/source/FadersHorizontal.svg index 0f7b09b..7270ce9 100644 --- a/src/shared/icons/source/FadersHorizontal.svg +++ b/src/shared/icons/source/FadersHorizontal.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/GoogleIcon.svg b/src/shared/icons/source/GoogleIcon.svg index 41a6676..7c56b0e 100644 --- a/src/shared/icons/source/GoogleIcon.svg +++ b/src/shared/icons/source/GoogleIcon.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/HeartStraight.svg b/src/shared/icons/source/HeartStraight.svg index 0985d2b..e9e427b 100644 --- a/src/shared/icons/source/HeartStraight.svg +++ b/src/shared/icons/source/HeartStraight.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/HouseSimple.svg b/src/shared/icons/source/HouseSimple.svg index 13fc140..f064541 100644 --- a/src/shared/icons/source/HouseSimple.svg +++ b/src/shared/icons/source/HouseSimple.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/KakaoIcon.svg b/src/shared/icons/source/KakaoIcon.svg index 07d40a9..4831b35 100644 --- a/src/shared/icons/source/KakaoIcon.svg +++ b/src/shared/icons/source/KakaoIcon.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/ListButton.svg b/src/shared/icons/source/ListButton.svg index 18792d2..b87320b 100644 --- a/src/shared/icons/source/ListButton.svg +++ b/src/shared/icons/source/ListButton.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/shared/icons/source/Logo.svg b/src/shared/icons/source/Logo.svg new file mode 100644 index 0000000..bb90a59 --- /dev/null +++ b/src/shared/icons/source/Logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/shared/icons/source/LogoMint.svg b/src/shared/icons/source/LogoMint.svg new file mode 100644 index 0000000..7ee4e8b --- /dev/null +++ b/src/shared/icons/source/LogoMint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/shared/icons/source/LogoPink.svg b/src/shared/icons/source/LogoPink.svg new file mode 100644 index 0000000..1e5e5d0 --- /dev/null +++ b/src/shared/icons/source/LogoPink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/shared/icons/source/MapPin.svg b/src/shared/icons/source/MapPin.svg index 9f33716..3a11e82 100644 --- a/src/shared/icons/source/MapPin.svg +++ b/src/shared/icons/source/MapPin.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/MapPin_.svg b/src/shared/icons/source/MapPin_.svg index 27a0199..3e9f7c4 100644 --- a/src/shared/icons/source/MapPin_.svg +++ b/src/shared/icons/source/MapPin_.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/NextButton.svg b/src/shared/icons/source/NextButton.svg index d2a6493..46ac6e8 100644 --- a/src/shared/icons/source/NextButton.svg +++ b/src/shared/icons/source/NextButton.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/shared/icons/source/PressStamp.svg b/src/shared/icons/source/PressStamp.svg index 2b0612b..e06a427 100644 --- a/src/shared/icons/source/PressStamp.svg +++ b/src/shared/icons/source/PressStamp.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/shared/icons/source/Save.svg b/src/shared/icons/source/Save.svg index 83d6194..a2a33c3 100644 --- a/src/shared/icons/source/Save.svg +++ b/src/shared/icons/source/Save.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/Stamp.svg b/src/shared/icons/source/Stamp.svg index ec6116e..5b43245 100644 --- a/src/shared/icons/source/Stamp.svg +++ b/src/shared/icons/source/Stamp.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/shared/icons/source/User.svg b/src/shared/icons/source/User.svg index 0fbb539..91cbec4 100644 --- a/src/shared/icons/source/User.svg +++ b/src/shared/icons/source/User.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/backto.svg b/src/shared/icons/source/backto.svg index 40ca826..6891eb3 100644 --- a/src/shared/icons/source/backto.svg +++ b/src/shared/icons/source/backto.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/icons/source/x.svg b/src/shared/icons/source/x.svg index 435b208..4c3dc61 100644 --- a/src/shared/icons/source/x.svg +++ b/src/shared/icons/source/x.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/api/types.ts b/src/shared/types/authtypes.ts similarity index 100% rename from src/shared/api/types.ts rename to src/shared/types/authtypes.ts