Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
12 commits
Select commit Hold shift + click to select a range
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
71 changes: 64 additions & 7 deletions src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { apiClient } from './client';
import { useAuthStore } from '@/store/authStore';
import type {
SendVerificationRequest,
SendVerificationResponse,
Expand All @@ -14,6 +15,10 @@ import type {
RefreshTokenResponse,
CheckNicknameRequest,
CheckNicknameResponse,
KakaoLoginRequest,
KakaoLoginResponse,
KakaoSignupRequest,
KakaoSignupResponse,
} from '@/types/api';

// ํšŒ์›๊ฐ€์ž… - ์ธ์ฆ๋ฒˆํ˜ธ ๋ฐœ์†ก
Expand Down Expand Up @@ -61,10 +66,12 @@ export const login = async (data: LoginRequest): Promise<LoginResponse> => {
try {
const response = await apiClient.post<LoginResponse>('/auth/login', data);

// ๐Ÿ’ก ์•ˆ์ „ํ•œ ํ† ํฐ ์ €์žฅ์„ ์œ„ํ•ด ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ ์œ ์ง€
if (typeof window !== 'undefined' && response.data.data) {
localStorage.setItem('accessToken', response.data.data.accessToken);
localStorage.setItem('refreshToken', response.data.data.refreshToken);
// ์ „์—ญ ์ƒํƒœ์— ํ† ํฐ ์ €์žฅ
if (response.data.data) {
useAuthStore.getState().setTokens(
response.data.data.accessToken,
response.data.data.refreshToken
);
}

return response.data;
Expand All @@ -78,9 +85,13 @@ export const refreshAccessToken = async (data: RefreshTokenRequest): Promise<Ref
try {
const response = await apiClient.post<RefreshTokenResponse>('/auth/refresh', data);

// ๐Ÿ’ก ์žฌ๋ฐœ๊ธ‰ ๋ฐ›์€ ํ† ํฐ๋„ ๋‹ค์‹œ ์ €์žฅํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.
if (typeof window !== 'undefined' && response.data.data) {
localStorage.setItem('accessToken', response.data.data.accessToken);
// ์žฌ๋ฐœ๊ธ‰ ๋ฐ›์€ ํ† ํฐ์„ ์ „์—ญ ์ƒํƒœ์— ์ €์žฅ
if (response.data.data) {
const { accessToken, refreshToken: newRefreshToken } = response.data.data;
useAuthStore.getState().setTokens(
accessToken,
newRefreshToken || data.refreshToken // ์ƒˆ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ์—†์œผ๋ฉด ๊ธฐ์กด ๊ฒƒ ์œ ์ง€
);
}

return response.data;
Expand All @@ -97,4 +108,50 @@ export const checkNickname = async (data: CheckNicknameRequest): Promise<CheckNi
} catch (error: any) {
throw error.response?.data || { message: '๋‹‰๋„ค์ž„ ํ™•์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.' };
}
};

// ์นด์นด์˜ค ๋กœ๊ทธ์ธ
export const kakaoLogin = async (data: KakaoLoginRequest): Promise<KakaoLoginResponse> => {
try {
const response = await apiClient.post<KakaoLoginResponse>('/auth/kakao/login', data);

// localStorage์— ์ง์ ‘ ํ† ํฐ ์ €์žฅ
if (response.data.data) {
const { accessToken, refreshToken } = response.data.data;

if (accessToken && refreshToken) {
// localStorage์— ์ง์ ‘ ์ €์žฅ
if (typeof window !== 'undefined') {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);

// authStore์—๋„ ๋™๊ธฐํ™”
useAuthStore.getState().setTokens(accessToken, refreshToken);
}
}
}

return response.data;
} catch (error: any) {
throw error.response?.data || { message: '์นด์นด์˜ค ๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.' };
}
};

// ์นด์นด์˜ค ํšŒ์›๊ฐ€์ž…
export const kakaoSignup = async (data: KakaoSignupRequest): Promise<KakaoSignupResponse> => {
try {
const response = await apiClient.post<KakaoSignupResponse>('/auth/kakao/signup', data);

// ์ „์—ญ ์ƒํƒœ์— ํ† ํฐ ์ €์žฅ
if (response.data.data) {
useAuthStore.getState().setTokens(
response.data.data.accessToken,
response.data.data.refreshToken
);
}

return response.data;
} catch (error: any) {
throw error.response?.data || { message: '์นด์นด์˜ค ํšŒ์›๊ฐ€์ž…์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.' };
}
};
96 changes: 96 additions & 0 deletions src/app/(auth)/auth/kakao/callback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use client';

import { useEffect, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { kakaoLogin } from '@/api/auth';

function KakaoCallbackContent() {
const router = useRouter();
const searchParams = useSearchParams();

useEffect(() => {
const handleCallback = async () => {
try {
// URL์—์„œ code ์ถ”์ถœ
const code = searchParams.get('code');
const error = searchParams.get('error');

// ์—๋Ÿฌ๊ฐ€ ์žˆ์œผ๋ฉด ์ฒ˜๋ฆฌ
if (error) {
alert('์นด์นด์˜ค ๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
router.push('/');
return;
}

// code๊ฐ€ ์—†์œผ๋ฉด ์—๋Ÿฌ
if (!code) {
alert('์นด์นด์˜ค ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.');
router.push('/');
return;
}

// ์นด์นด์˜ค ๋กœ๊ทธ์ธ API ํ˜ธ์ถœ
const response = await kakaoLogin({ code });

// ์‹ ๊ทœ ๊ฐ€์ž…์ž๋ฉด ํšŒ์›๊ฐ€์ž…์œผ๋กœ, ๊ธฐ์กด ์‚ฌ์šฉ์ž๋ฉด ํ™ˆ์œผ๋กœ
if (response.data?.isNewUser) {
// ์‹ ๊ทœ ๊ฐ€์ž…์ž - ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€๋กœ kakaoId ์ „๋‹ฌ
router.push(`/signup?kakaoId=${response.data.kakaoId}`);
} else {
// ๊ธฐ์กด ์‚ฌ์šฉ์ž - ํ™ˆ์œผ๋กœ ์ด๋™
router.push('/home');
}
} catch (error: any) {
alert(error.message || '์นด์นด์˜ค ๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
router.push('/');
}
};

handleCallback();
}, [searchParams, router]);

// ๋กœ๋”ฉ ํ™”๋ฉด
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#131416',
}}
>
<div style={{ color: '#EAEFF5', fontSize: '16px' }}>์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ์ค‘...</div>
</div>
);
}

export default function KakaoCallbackPage() {
return (
<Suspense
fallback={
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#131416',
}}
>
<div style={{ color: '#EAEFF5', fontSize: '16px' }}>๋กœ๋”ฉ ์ค‘...</div>
</div>
}
>
<KakaoCallbackContent />
</Suspense>
);
}
81 changes: 69 additions & 12 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
'use client';

import Image from 'next/image';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useState, useEffect, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import AuthContainer from '../../(auth)/AuthContainer';
import { sendVerificationCode, verifyCode, checkNickname } from '@/api/auth';
import { sendVerificationCode, verifyCode, checkNickname, kakaoSignup } from '@/api/auth';

export default function SignupPage() {
function SignupPageContent() {
const router = useRouter();
const searchParams = useSearchParams();

// ์นด์นด์˜ค ํšŒ์›๊ฐ€์ž… ๋ชจ๋“œ ํ™•์ธ
const kakaoId = searchParams.get('kakaoId');
const isKakaoMode = !!kakaoId;

const [step, setStep] = useState(1);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
Expand Down Expand Up @@ -215,7 +221,25 @@ const handleVerifyCode = async () => {
}
};

const handleNext = () => {
const handleNext = async () => {
// ์นด์นด์˜ค ๋ชจ๋“œ์ด๊ณ  step 1(๋‹‰๋„ค์ž„ ์ž…๋ ฅ) ์™„๋ฃŒ ์‹œ ์นด์นด์˜ค ํšŒ์›๊ฐ€์ž… API ํ˜ธ์ถœ
if (isKakaoMode && step === 1 && isNicknameChecked) {
try {
setLoading(true);
await kakaoSignup({
kakaoId: kakaoId!,
nickname: name.trim(),
});
// ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต ์‹œ ํ™ˆ์œผ๋กœ ์ด๋™
router.push('/home');
} catch (err: any) {
setNicknameError(err.message || 'ํšŒ์›๊ฐ€์ž…์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
setLoading(false);
}
return;
}

// ์ผ๋ฐ˜ ํšŒ์›๊ฐ€์ž… ํ”Œ๋กœ์šฐ
if (step < 4) {
setStep(step + 1);
} else {
Expand All @@ -236,6 +260,11 @@ const handleVerifyCode = async () => {
};

const renderStepContent = () => {
// ์นด์นด์˜ค ๋ชจ๋“œ์ผ ๋•Œ๋Š” step 1๋งŒ ๋ณด์—ฌ์คŒ
if (isKakaoMode && step !== 1) {
return null;
}

switch (step) {
case 1:
return (
Expand Down Expand Up @@ -951,15 +980,17 @@ const handleVerifyCode = async () => {

return (
<AuthContainer
title="ํšŒ์›๊ฐ€์ž…"
title={isKakaoMode ? "์นด์นด์˜ค ํšŒ์›๊ฐ€์ž…" : "ํšŒ์›๊ฐ€์ž…"}
currentStep={step}
totalSteps={4}
totalSteps={isKakaoMode ? 0 : 4}
buttonText={
step === 2
? (isCodeSent ? "์ธ์ฆํ•˜๊ธฐ" : "์ธ์ฆ๋ฒˆํ˜ธ ์ „์†ก")
: step === 4
? "๊ฐ€์ž…ํ•˜๊ธฐ"
: "๋‹ค์Œ"
isKakaoMode && step === 1
? "์™„๋ฃŒ"
: step === 2
? (isCodeSent ? "์ธ์ฆํ•˜๊ธฐ" : "์ธ์ฆ๋ฒˆํ˜ธ ์ „์†ก")
: step === 4
? "๊ฐ€์ž…ํ•˜๊ธฐ"
: "๋‹ค์Œ"
}
buttonDisabled={
step === 1 ? (!name || !isNicknameChecked) :
Expand All @@ -973,6 +1004,12 @@ const handleVerifyCode = async () => {
false
}
onNext={() => {
// ์นด์นด์˜ค ๋ชจ๋“œ์ผ ๋•Œ๋Š” step 1์—์„œ ๋ฐ”๋กœ ํšŒ์›๊ฐ€์ž… ์ฒ˜๋ฆฌ
if (isKakaoMode && step === 1) {
handleNext();
return;
}

if (step === 2) {
if (!isCodeSent) {
handleSendVerificationCode();
Expand All @@ -996,4 +1033,24 @@ const handleVerifyCode = async () => {
{renderStepContent()}
</AuthContainer>
);
}

export default function SignupPage() {
return (
<Suspense fallback={
<div className="flex flex-col mx-auto" style={{
width: '390px',
height: '844px',
background: 'var(--color-bg-100, #131416)',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<p style={{ color: '#fff' }}>๋กœ๋”ฉ ์ค‘...</p>
</div>
}>
<SignupPageContent />
</Suspense>
);
}
18 changes: 16 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,22 @@ function LoginSelectScreen() {

const handleKakaoLogin = () => {
setLoading(true);
alert('์นด์นด์˜ค ๋กœ๊ทธ์ธ (๊ฐœ๋ฐœ ์ค‘)');
setLoading(false);

// ์นด์นด์˜ค OAuth ์ธ์ฆ URL ์ƒ์„ฑ
const kakaoClientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID;
// ํ”„๋ก ํŠธ์—”๋“œ callback URL ์‚ฌ์šฉ
const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI ||
`${window.location.origin}/auth/kakao/callback`;

if (!kakaoClientId) {
alert('์นด์นด์˜ค ๋กœ๊ทธ์ธ ์„ค์ •์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.');
setLoading(false);
return;
}

// ์นด์นด์˜ค ์ธ์ฆ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${kakaoClientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`;
window.location.href = kakaoAuthUrl;
};

return (
Expand Down
Loading