diff --git a/src/api/users/getAlias.ts b/src/api/users/getAlias.ts new file mode 100644 index 00000000..743fe2ed --- /dev/null +++ b/src/api/users/getAlias.ts @@ -0,0 +1,22 @@ +import { apiClient } from '../index'; + +export interface AliasChoice { + aliasName: string; + categoryName: string; + imageUrl: string; + color: string; +} + +export interface GetAliasResponse { + success: boolean; + code: number; + message: string; + data: { + aliasChoices: AliasChoice[]; + }; +} + +export const getAlias = async (): Promise => { + const response = await apiClient.get('/users/alias'); + return response.data; +}; diff --git a/src/api/users/postNickname.ts b/src/api/users/postNickname.ts new file mode 100644 index 00000000..6809bc7a --- /dev/null +++ b/src/api/users/postNickname.ts @@ -0,0 +1,19 @@ +import { apiClient } from '../index'; + +export interface PostNicknameRequest { + nickname: string; +} + +export interface PostNicknameResponse { + isSuccess: boolean; + code: number; + message: string; + data: { isVerified: boolean }; +} + +export const postNickname = async (nickname: string): Promise => { + const response = await apiClient.post('/users/nickname', { + nickname, + }); + return response.data; +}; diff --git a/src/api/users/postSignup.ts b/src/api/users/postSignup.ts new file mode 100644 index 00000000..a88a169a --- /dev/null +++ b/src/api/users/postSignup.ts @@ -0,0 +1,20 @@ +import { apiClient } from '../index'; + +export interface PostSignupRequest { + aliasName: string; + nickName: string; +} + +export interface PostSignupResponse { + success: boolean; + code: number; + message: string; + data: { + userId: number; + }; +} + +export const postSignup = async (data: PostSignupRequest): Promise => { + const response = await apiClient.post('/users/signup', data); + return response.data; +}; diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx index 4b05301e..02835ce7 100644 --- a/src/pages/login/Login.tsx +++ b/src/pages/login/Login.tsx @@ -4,6 +4,32 @@ import KaKao from '../../assets/login/kakao.svg'; import Google from '../../assets/login/google.svg'; import { Wrapper } from '@/components/common/Wrapper'; +const Login = () => { + const handleKakaoLogin = () => { + // 직접 카카오 로그인 URL로 리다이렉션 + window.location.href = `${import.meta.env.VITE_API_BASE_URL}/oauth2/authorization/kakao`; + }; + + const handleGoogleLogin = () => { + // 직접 구글 로그인 URL로 리다이렉션 + window.location.href = `${import.meta.env.VITE_API_BASE_URL}/oauth2/authorization/google`; + }; + + return ( + + + + + 카카오계정 로그인 + + + 구글계정 로그인 + + + + ); +}; + const ButtonBox = styled.div` display: flex; flex-direction: column; @@ -35,28 +61,4 @@ const SocialButton = styled.div<{ bg: string }>` cursor: pointer; `; -const Login = () => { - const handleKakaoLogin = () => { - return; - }; - - const handleGoogleLogin = () => { - return; - }; - - return ( - - - - - 카카오계정 로그인 - - - 구글계정 로그인 - - - - ); -}; - export default Login; diff --git a/src/pages/signup/Signup.styled.ts b/src/pages/signup/Signup.styled.ts index 97152d32..ff16d94e 100644 --- a/src/pages/signup/Signup.styled.ts +++ b/src/pages/signup/Signup.styled.ts @@ -2,6 +2,7 @@ import styled from '@emotion/styled'; export const Container = styled.div` display: flex; + position: relative; flex-direction: column; background-color: var(--color-black-main); min-width: 360px; @@ -11,6 +12,17 @@ export const Container = styled.div` padding: 96px 20px 0 20px; gap: 12px; + .errorMessage { + position: absolute; + top: 220px; + left: 24px; + + color: var(--color-text-warning_red, #ff9496); + font-size: var(--string-size-small03, 12px); + font-weight: var(--string-weight-regular, 400); + line-height: normal; + } + .title { margin-top: 40px; color: var(--color-white); @@ -164,7 +176,7 @@ export const Container = styled.div` } `; -export const InputBox = styled.div` +export const InputBox = styled.div<{ hasError?: boolean }>` display: flex; flex-direction: row; justify-content: space-between; @@ -175,9 +187,11 @@ export const InputBox = styled.div` border-radius: 12px; padding: 12px; background-color: var(--color-darkgrey-dark); + border: 1px solid ${props => (props.hasError ? '#FF9496' : 'none')}; + transition: border-color 0.2s ease; `; -export const StyledInput = styled.input` +export const StyledInput = styled.input<{ hasError?: boolean }>` flex: 1; background: none; border: none; diff --git a/src/pages/signup/SignupDone.tsx b/src/pages/signup/SignupDone.tsx index 00404ba7..f5472185 100644 --- a/src/pages/signup/SignupDone.tsx +++ b/src/pages/signup/SignupDone.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { Container } from './Signup.styled'; import leftarrow from '../../assets/common/leftArrow.svg'; import art from '../../assets/genre/art.svg'; @@ -6,29 +6,40 @@ import TitleHeader from '../../components/common/TitleHeader'; const SignupDone = () => { const navigate = useNavigate(); + const location = useLocation(); + + // SignupGenre에서 전달된 데이터 받기 + const { nickName, aliasName } = location.state || {}; + const handleBackClick = () => { - navigate(-1); + navigate('/signup/genre'); }; const handleNextClick = () => { navigate('/feed'); }; + // state가 없으면 이전 페이지로 이동 + if (!nickName || !aliasName) { + navigate('/signup/nickname'); + return null; + } + return ( } onLeftClick={handleBackClick} /> -
안녕하세요, 희용희용님
+
안녕하세요, {nickName}님
이제 Thip에서 활동할 준비를 모두 마쳤어요!
-
희용희용
-
예술가
+
{nickName}
+
{aliasName}
지금 바로 Thip 시작하기 diff --git a/src/pages/signup/SignupGenre.tsx b/src/pages/signup/SignupGenre.tsx index fa9ae6da..5299bb1c 100644 --- a/src/pages/signup/SignupGenre.tsx +++ b/src/pages/signup/SignupGenre.tsx @@ -1,19 +1,68 @@ import { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { Container } from './Signup.styled'; import leftarrow from '../../assets/common/leftArrow.svg'; import TitleHeader from '../../components/common/TitleHeader'; -import type { Genre } from '@/types/genre'; +import { postSignup } from '@/api/users/postSignup'; +import { apiClient } from '@/api/index'; const SignupGenre = () => { - const [genres, setGenres] = useState([]); - const [selectedId, setSelectedId] = useState(null); + const [genres, setGenres] = useState< + Array<{ + id: string; + title: string; + subTitle: string; + iconUrl: string; + color: string; + }> + >([]); + const [selectedAlias, setSelectedAlias] = useState<{ + id: string; + subTitle: string; + } | null>(null); const navigate = useNavigate(); + const location = useLocation(); + + // SignupNickname에서 넘어온 nickname 받기 + const nickname = location.state?.nickname; + + // 쿠키에서 Authorization 토큰 추출 + const getAuthTokenFromCookie = () => { + console.log('=== 쿠키 디버깅 ==='); + console.log('현재 페이지 URL:', window.location.href); + console.log('현재 도메인:', window.location.hostname); + console.log('전체 쿠키:', document.cookie); + + const cookies = document.cookie.split(';'); + console.log('분리된 쿠키들:', cookies); + + for (const cookie of cookies) { + const [name, value] = cookie.trim().split('='); + console.log('쿠키 이름:', name, '값:', value); + if (name === 'Authorization') { + console.log('Authorization 토큰 발견:', value); + return value; + } + } + + console.log('Authorization 토큰을 찾을 수 없습니다.'); + console.log('가능한 원인: 도메인 불일치, 경로 불일치, 쿠키 만료'); + return null; + }; + + // 토큰을 헤더에 설정 + const setAuthTokenToHeader = (token: string) => { + // localStorage에 저장 (페이지 새로고침 시에도 유지) + localStorage.setItem('authToken', token); + + // apiClient 기본 헤더에 설정 + apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`; + }; useEffect(() => { fetch('/genres.json') .then(res => res.json()) - .then((data: Genre[]) => setGenres(data)) + .then(data => setGenres(data)) .catch(console.error); }, []); @@ -21,9 +70,42 @@ const SignupGenre = () => { navigate(-1); }; - const handleNextClick = () => { - if (!selectedId) return; - navigate('/signupdone', { state: { genreId: selectedId } }); + const handleNextClick = async () => { + if (!selectedAlias || !nickname) return; + + // 쿠키에서 토큰 추출 + const authToken = getAuthTokenFromCookie(); + if (!authToken) { + console.log('쿠키에서 Authorization 토큰을 찾을 수 없습니다.'); + console.log('토큰이 없어 회원가입을 진행할 수 없습니다.'); + return; // 토큰이 없으면 함수 종료하여 페이지에 머무름 + } + + // 토큰을 헤더에 설정 + setAuthTokenToHeader(authToken); + console.log('Authorization 토큰을 헤더에 설정했습니다.'); + + try { + const result = await postSignup({ + aliasName: selectedAlias.subTitle, + nickName: nickname, + }); + + if (result.success) { + console.log('회원가입 성공! 사용자 ID:', result.data.userId); + // 회원가입 완료 페이지로 이동 + navigate('/signupdone', { + state: { + aliasName: selectedAlias.subTitle, + nickName: nickname, + }, + }); + } else { + console.error('회원가입 실패:', result.message); + } + } catch (error) { + console.error('회원가입 중 오류 발생:', error); + } }; return ( @@ -34,7 +116,7 @@ const SignupGenre = () => { rightButton={
다음
} onLeftClick={handleBackClick} onRightClick={handleNextClick} - isNextActive={!!selectedId} + isNextActive={!!selectedAlias} />
관심있는 장르를 선택해주세요.
@@ -44,8 +126,8 @@ const SignupGenre = () => { {genres.map(g => (
setSelectedId(g.id)} + className={`genreCard ${g.id === selectedAlias?.id ? 'active' : ''}`} + onClick={() => setSelectedAlias({ id: g.id, subTitle: g.subTitle })} > {g.title}
diff --git a/src/pages/signup/SignupNickname.tsx b/src/pages/signup/SignupNickname.tsx index d2833792..63cd7dc4 100644 --- a/src/pages/signup/SignupNickname.tsx +++ b/src/pages/signup/SignupNickname.tsx @@ -2,9 +2,11 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Container, InputBox, StyledInput, CharCount } from './Signup.styled'; import Header from '../../components/common/TitleHeader'; +import { postNickname } from '@/api/users/postNickname'; const SignupNickname = () => { const [nickname, setNickname] = useState(''); + const [error, setError] = useState(''); const maxLength = 10; const navigate = useNavigate(); @@ -14,14 +16,29 @@ const SignupNickname = () => { navigate(-1); }; - const handleNextClick = () => { + const handleNextClick = async () => { if (!isNextActive) return; - navigate('/signup/genre'); + setError(''); + + try { + const result = await postNickname(nickname); + + if (result.data.isVerified) { + // 닉네임 검증 성공 - 다음 단계로 진행 + navigate('/signup/genre', { state: { nickname } }); + } else { + // 닉네임 검증 실패 - 우리가 정한 에러 메시지 + setError('이미 사용중인 닉네임이에요.'); + } + } catch (error) { + console.error('닉네임 검증 실패:', error); + setError('닉네임 검증 중 오류가 발생했습니다.'); + } }; const handleInputChange = (e: React.ChangeEvent) => { const inputValue = e.target.value; - const filteredValue = inputValue.replace(/[^ㄱ-ㅎ가-힣a-zA-Z0-9]/g, ''); + const filteredValue = inputValue.replace(/[^ㄱ-ㅎ가-힣a-z0-9]/g, ''); setNickname(filteredValue); }; @@ -36,18 +53,20 @@ const SignupNickname = () => { />
닉네임(필수)
- + {nickname.length}/{maxLength} + {error &&
{error}
}
);