diff --git a/src/apis/postGoogleOauth.ts b/src/apis/postGoogleOauth.ts new file mode 100644 index 00000000..66e9e737 --- /dev/null +++ b/src/apis/postGoogleOauth.ts @@ -0,0 +1,11 @@ +import httpClient from '.'; + +const postGoogleOauth = async (code: string) => { + const response = await httpClient.post('/auth/signIn/GOOGLE', { + redirectUri: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI, + token: code, + }); + return response.data; +}; + +export default postGoogleOauth; diff --git a/src/apis/postNaverOauth.ts b/src/apis/postNaverOauth.ts new file mode 100644 index 00000000..56434bb0 --- /dev/null +++ b/src/apis/postNaverOauth.ts @@ -0,0 +1,12 @@ +import httpClient from '.'; + +const postNaverOauth = async (code: string, state: string) => { + const response = await httpClient.post('/auth/signIn/NAVER', { + state, + redirectUri: process.env.NEXT_PUBLIC_NAVER_REDIRECT_URI, + token: code, + }); + return response.data; +}; + +export default postNaverOauth; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index d342347f..14df0b93 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -18,9 +18,10 @@ import SHARE_ICON from '../../../public/icon/share-icon.svg'; // NOTE isButton 일 경우 textInButton의 값을 무조건 지정해줘야 합니다. // NOTE SHARE_ICON 추가 시 토스트 기능도 사용하려면 해당 컴포넌트 아래 를 추가해주세요. +// TODO 새로 바뀐 피그마 시안으로 바꿀지 추후 결정 + export interface HeaderProps { icon: 'back' | 'search' | ''; - routerPage: string; isLogo: boolean; insteadOfLogo: string; isProfileIcon: boolean; @@ -31,10 +32,15 @@ export interface HeaderProps { onClick: (e: React.MouseEvent) => void; } -function Header({ isLogo, icon, insteadOfLogo, isButton, isProfileIcon, isShareIcon, textInButton, routerPage, disabled, onClick }: HeaderProps) { +function Header({ isLogo, icon, insteadOfLogo, isButton, isProfileIcon, isShareIcon, textInButton, disabled, onClick }: HeaderProps) { const router = useRouter(); const { toast } = useToast(); + // 뒤로가기 + const handleBack = () => { + router.back(); + }; + // 페이지 이동 함수 const handleNavigateTo = (path: string) => { router.push(path); @@ -65,7 +71,7 @@ function Header({ isLogo, icon, insteadOfLogo, isButton, isProfileIcon, isShareI
{icon === 'back' && ( - )} diff --git a/src/components/epigram/EditEpigram.tsx b/src/components/epigram/EditEpigram.tsx index 1b7e5f1a..35cbe728 100644 --- a/src/components/epigram/EditEpigram.tsx +++ b/src/components/epigram/EditEpigram.tsx @@ -121,18 +121,7 @@ function EditEpigram({ epigram }: EditEpigramProps) { return ( <> -
{}} - /> +
{}} />
diff --git a/src/hooks/useGoogleLogin.ts b/src/hooks/useGoogleLogin.ts new file mode 100644 index 00000000..46045f44 --- /dev/null +++ b/src/hooks/useGoogleLogin.ts @@ -0,0 +1,41 @@ +import postGoogleOauth from '@/apis/postGoogleOauth'; +import { toast } from '@/components/ui/use-toast'; +import { useMutation } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import { useRouter } from 'next/router'; + +const useGoogleLogin = () => { + const router = useRouter(); + + return useMutation({ + mutationFn: async (code: string) => { + const result = await postGoogleOauth(code); + localStorage.setItem('accessToken', result.accessToken); + localStorage.setItem('refreshToken', result.refreshToken); + return result; + }, + onSuccess: () => { + router.push('/epigrams'); + }, + onError: (error) => { + if (isAxiosError(error)) { + const status = error.response?.status; + + if (!status) return; + + if (status === 400) { + toast({ description: '잘못된 요청입니다. 요청을 확인해 주세요.', className: 'bg-state-error text-white font-semibold' }); + router.push('/auth/SignIn'); + return; + } + + if (status >= 500) { + toast({ description: '서버에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.', className: 'bg-state-error text-white font-semibold' }); + } + } + toast({ description: '알 수 없는 에러가 발생했습니다.', className: 'bg-state-error text-white font-semibold' }); + }, + }); +}; + +export default useGoogleLogin; diff --git a/src/hooks/useKakaoLogin.ts b/src/hooks/useKakaoLogin.ts index 14d8d81b..ea627868 100644 --- a/src/hooks/useKakaoLogin.ts +++ b/src/hooks/useKakaoLogin.ts @@ -15,7 +15,7 @@ const useKakaoLogin = () => { return result; }, onSuccess: () => { - router.push('/'); + router.push('/epigrams'); }, onError: (error) => { if (isAxiosError(error)) { diff --git a/src/hooks/useNaverLogin.ts b/src/hooks/useNaverLogin.ts new file mode 100644 index 00000000..9ff23b36 --- /dev/null +++ b/src/hooks/useNaverLogin.ts @@ -0,0 +1,42 @@ +import postNaverOauth from '@/apis/postNaverOauth'; +import { toast } from '@/components/ui/use-toast'; +import { useMutation } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import { useRouter } from 'next/router'; + +const useNaverLogin = () => { + const router = useRouter(); + + return useMutation({ + mutationFn: async ({ code, state }: { code: string; state: string }) => { + const result = await postNaverOauth(code, state); + localStorage.setItem('accessToken', result.accessToken); + localStorage.setItem('refreshToken', result.refreshToken); + return result; + }, + onSuccess: () => { + router.push('/epigrams'); + }, + onError: (error) => { + if (isAxiosError(error)) { + const status = error.response?.status; + + if (!status) return; + + if (status === 400) { + toast({ description: '잘못된 요청입니다. 요청을 확인해 주세요.', className: 'bg-state-error text-white font-semibold' }); + router.push('/auth/SignIn'); + return; + } + + if (status >= 500) { + toast({ description: '서버에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.', className: 'bg-state-error text-white font-semibold' }); + } + } + + toast({ description: '알 수 없는 에러가 발생했습니다.', className: 'bg-state-error text-white font-semibold' }); + }, + }); +}; + +export default useNaverLogin; diff --git a/src/hooks/useSignInMutation.ts b/src/hooks/useSignInMutation.ts index 2f4ebb5e..0ed58dc1 100644 --- a/src/hooks/useSignInMutation.ts +++ b/src/hooks/useSignInMutation.ts @@ -2,6 +2,7 @@ import { postSignin } from '@/apis/auth'; import { toast } from '@/components/ui/use-toast'; import { useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/router'; +import { isAxiosError } from 'axios'; const useSigninMutation = () => { const router = useRouter(); @@ -11,10 +12,36 @@ const useSigninMutation = () => { onSuccess: (data) => { localStorage.setItem('accessToken', data.accessToken); localStorage.setItem('refreshToken', data.refreshToken); - router.push('/'); + router.push('/epigrams'); }, - onError: () => { - toast({ description: '이메일 혹은 비밀번호를 확인해주세요.', className: 'border-state-error text-state-error font-semibold' }); + onError: (error) => { + if (!isAxiosError(error)) { + return; + } + + const { status } = error.response || {}; + + if (status === 500) { + toast({ + description: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', + className: 'border-state-error text-state-error font-semibold', + }); + return; + } + + // NOTE: status값은 항상 있으며 undefined와 숫자를 비교연산 할 수 없어 Number로 설정 + if (Number(status) >= 500) { + toast({ + description: '서버에서 예상치 못한 문제가 발생했습니다. 잠시 후 다시 시도해주세요.', + className: 'border-state-error text-state-error font-semibold', + }); + return; + } + + toast({ + description: '이메일 혹은 비밀번호를 확인해주세요.', + className: 'border-state-error text-state-error font-semibold', + }); }, }); }; diff --git a/src/pageLayout/Epigram/AddEpigram.tsx b/src/pageLayout/Epigram/AddEpigram.tsx index 5738f1d4..97391b8f 100644 --- a/src/pageLayout/Epigram/AddEpigram.tsx +++ b/src/pageLayout/Epigram/AddEpigram.tsx @@ -106,7 +106,7 @@ function AddEpigram() { return ( <> -
{}} /> +
{}} />
diff --git a/src/pageLayout/Epigrams/MainLayout.tsx b/src/pageLayout/Epigrams/MainLayout.tsx index ab5baa66..b805fec6 100644 --- a/src/pageLayout/Epigrams/MainLayout.tsx +++ b/src/pageLayout/Epigrams/MainLayout.tsx @@ -9,7 +9,7 @@ import FAB from '@/components/main/FAB'; function MainLayout() { return ( <> -
{}} /> +
{}} />
diff --git a/src/pageLayout/MypageLayout/MyPageLayout.tsx b/src/pageLayout/MypageLayout/MyPageLayout.tsx index 46594787..3ae052d7 100644 --- a/src/pageLayout/MypageLayout/MyPageLayout.tsx +++ b/src/pageLayout/MypageLayout/MyPageLayout.tsx @@ -27,7 +27,7 @@ export default function MyPageLayout() { return (
-
{}} /> +
{}} />
diff --git a/src/pageLayout/SearchLayout/SearchLayout.tsx b/src/pageLayout/SearchLayout/SearchLayout.tsx index 02c68db9..e83b7785 100644 --- a/src/pageLayout/SearchLayout/SearchLayout.tsx +++ b/src/pageLayout/SearchLayout/SearchLayout.tsx @@ -100,7 +100,7 @@ function SearchLayout() { return ( <> -
{}} />; +
{}} />;
diff --git a/src/pages/auth/SignIn.tsx b/src/pages/auth/SignIn.tsx index c1bfd025..8e165591 100644 --- a/src/pages/auth/SignIn.tsx +++ b/src/pages/auth/SignIn.tsx @@ -77,19 +77,24 @@ export default function SignIn() {

회원이 아니신가요?

- +
- + logo-naver - - logo-google - + {/* // FIXME: 구글 간편 로그인 리다이렉트시 500에러가 발생하는 부분으로 주석 처리하였음 */} + {/* */} + logo-google + {/* */} logo-kakao diff --git a/src/pages/auth/redirect/google-callback/index.ts b/src/pages/auth/redirect/google-callback/index.ts new file mode 100644 index 00000000..1f925901 --- /dev/null +++ b/src/pages/auth/redirect/google-callback/index.ts @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'next/navigation'; +import useGoogleLogin from '@/hooks/useGoogleLogin'; + +export default function Google() { + const searchParams = useSearchParams(); + const code = searchParams.get('code'); + const { mutate: login } = useGoogleLogin(); + + useEffect(() => { + if (code) { + login(code); + } else { + /* eslint-disable no-console */ + console.log(code); // code가 없을 때 콘솔에 출력 + } + }, [code, login]); +} + +// code가 없는 경우의 예시 http://localhost:3000/auth/redirect/kakao +// 토스트로 에러 메시지 띄우고, 로그인 페이지로 리다이렉트 diff --git a/src/pages/auth/redirect/naver/index.ts b/src/pages/auth/redirect/naver/index.ts new file mode 100644 index 00000000..ff844a3d --- /dev/null +++ b/src/pages/auth/redirect/naver/index.ts @@ -0,0 +1,19 @@ +import useNaverLogin from '@/hooks/useNaverLogin'; +import { useSearchParams } from 'next/navigation'; +import { useEffect } from 'react'; + +export default function Naver() { + const searchParams = useSearchParams(); + const code = searchParams.get('code'); + const state = searchParams.get('state'); + const { mutate: login } = useNaverLogin(); + + useEffect(() => { + if (code && state) { + login({ code, state }); + } else { + /* eslint-disable no-console */ + console.log(code, state); // code가 없을 때 콘솔에 출력 + } + }, [code, state, login]); +} diff --git a/src/pages/epigrams/[id]/index.tsx b/src/pages/epigrams/[id]/index.tsx index 9f5424ae..ffbd6f79 100644 --- a/src/pages/epigrams/[id]/index.tsx +++ b/src/pages/epigrams/[id]/index.tsx @@ -22,7 +22,7 @@ function DetailPage() { return (
-
{}} /> +
{}} />
diff --git a/src/user/ui-profile/Calendar.tsx b/src/user/ui-profile/Calendar.tsx deleted file mode 100644 index 4ce6d06a..00000000 --- a/src/user/ui-profile/Calendar.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useState } from 'react'; -import Image from 'next/image'; -import { subMonths } from 'date-fns'; -import { EmotionLog, EmotionTypeEN } from '@/types/emotion'; -import useCalendar from '../../hooks/useCalendar'; -import { DAY_LIST, DATE_MONTH_FIXER, iconPaths } from '../utill/constants'; -import CalendarHeader from './CalendarHeader'; - -interface CalendarProps { - currentDate: Date; // 현재 날짜 - setCurrentDate: React.Dispatch>; // 현재 날짜를 설정하는 함수 - monthlyEmotionLogs: EmotionLog[]; -} - -export default function Calendar({ currentDate, setCurrentDate, monthlyEmotionLogs }: CalendarProps) { - // 캘린더 함수 호출 - const { weekCalendarList } = useCalendar(currentDate); - // 감정 필터 - const [selectedEmotion, setSelectedEmotion] = useState(null); - - // 달력에 출력할 수 있게 매핑 - const emotionMap: Record = Array.isArray(monthlyEmotionLogs) - ? monthlyEmotionLogs.reduce>((acc, log) => { - const date = new Date(log.createdAt); - const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; - acc[dateString] = log.emotion as EmotionTypeEN; - return acc; - }, {}) - : {}; - - // 이전 달 클릭 - const handlePrevMonth = () => setCurrentDate((prevDate) => subMonths(prevDate, DATE_MONTH_FIXER)); - // 다음 달 클릭 - const handleNextMonth = () => setCurrentDate((prevDate) => subMonths(prevDate, -DATE_MONTH_FIXER)); - - // 감정 필터 - const handleEmotionSelect = (emotion: EmotionTypeEN) => { - // 현재 선택된 감정과 같으면 초기화 - if (selectedEmotion === emotion) { - setSelectedEmotion(null); - } else { - setSelectedEmotion(emotion); - } - }; - - // 필터링된 감정 맵 생성 - const filteredEmotionMap = selectedEmotion ? Object.fromEntries(Object.entries(emotionMap).filter(([, value]) => value === selectedEmotion)) : emotionMap; - - return ( -
- {/* 캘린더 헤더 */} - - {/* 캘린더 */} -
-
- {DAY_LIST.map((day) => ( -
- {day} -
- ))} -
- {weekCalendarList.map((week, weekIndex) => ( - // TODO: index 값 Lint error. 임시로 주석 사용. 추후 수정 예정 - // eslint-disable-next-line react/no-array-index-key -
- {week.map((day, dayIndex) => { - // 현재 날짜와 비교 - const isToday = day === currentDate.getDate() && currentDate.getMonth() === new Date().getMonth() && currentDate.getFullYear() === new Date().getFullYear(); - const dateString = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - const emotion: EmotionTypeEN = filteredEmotionMap[dateString]; // 날짜에 해당하는 감정 가져오기 - const iconPath = emotion && iconPaths[emotion] ? iconPaths[emotion].path : '/icon/BW/SmileFaceBWIcon.svg'; - - return ( -
- {emotion ? ( -
-

{day}

- 감정 -
- ) : ( -

{day}

- )} -
- ); - })} -
- ))} -
-
- ); -} diff --git a/src/user/ui-profile/CalendarHeader.tsx b/src/user/ui-profile/CalendarHeader.tsx deleted file mode 100644 index 2c337e1c..00000000 --- a/src/user/ui-profile/CalendarHeader.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuGroup, DropdownMenu } from '@/components/ui/dropdown-menu'; -import { Button } from '@/components/ui/button'; -import Image from 'next/image'; -import { EmotionTypeEN } from '@/types/emotion'; -import ARROW_BOTTOM_ICON from '../../../public/icon/arrow-bottom-icon.svg'; -import ARROW_RIGHT_ICON from '../../../public/icon/arrow-right-icon.svg'; -import ARROW_LEFT_ICON from '../../../public/icon/arrow-left-icon.svg'; -import { iconPaths } from '../utill/constants'; - -interface CalendarHeaderProps { - currentDate: Date; - onPrevMonth: () => void; - onNextMonth: () => void; - onEmotionSelect: (emotion: EmotionTypeEN) => void; - selectEmotion: EmotionTypeEN | null; -} - -export default function CalendarHeader({ currentDate, onPrevMonth, onNextMonth, onEmotionSelect, selectEmotion }: CalendarHeaderProps) { - return ( -
-
-
{`${currentDate.getFullYear()}년 ${currentDate.getMonth() + 1}월`}
- - - - - - - {Object.entries(iconPaths).map(([emotionKey, { path, name }]) => ( - - - - ))} - - - -
-
- - -
-
- ); -} diff --git a/src/user/ui-profile/Chart.tsx b/src/user/ui-profile/Chart.tsx deleted file mode 100644 index 6e89af4c..00000000 --- a/src/user/ui-profile/Chart.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { EmotionLog, EmotionTypeEN } from '@/types/emotion'; -import Image from 'next/image'; -import { iconPaths } from '../utill/constants'; - -interface ChartProps { - monthlyEmotionLogs: EmotionLog[]; -} - -export default function Chart({ monthlyEmotionLogs }: ChartProps) { - // 감정별 빈도수 계산 - const emotionCounts = monthlyEmotionLogs.reduce( - (count, log) => { - const { emotion } = log; - return { - ...count, // 기존의 count를 복사 - [emotion]: (count[emotion] || 0) + 1, // 현재 감정의 개수 증가 - }; - }, - {} as Record, - ); - - // 감정 종류 및 총 감정 수 계산 - const TOTAL_COUNT = monthlyEmotionLogs.length; - const EMOTIONS: EmotionTypeEN[] = ['MOVED', 'HAPPY', 'WORRIED', 'SAD', 'ANGRY']; - const RADIUS = 90; // 원의 반지름 - const CIRCUMFERENCE = 2 * Math.PI * RADIUS; - - // 가장 많이 나타나는 감정 찾기 - const maxEmotion = EMOTIONS.reduce((max, emotion) => (emotionCounts[emotion] > emotionCounts[max] ? emotion : max), EMOTIONS[0]); - - // 원형 차트의 각 감정에 대한 strokeDasharray와 strokeDashoffset 계산 - let offset = 0; - - return ( -
-

감정 차트

-
-
- - - {EMOTIONS.map((emotion) => { - const count = emotionCounts[emotion] || 0; - const percentage = TOTAL_COUNT > 0 ? count / TOTAL_COUNT : 0; // 0으로 나누기 방지 - const strokeDasharray = `${CIRCUMFERENCE * percentage} ${CIRCUMFERENCE * (1 - percentage)}`; - - // 색상 설정 - let strokeColor; - switch (emotion) { - case 'HAPPY': - strokeColor = '#FBC85B'; - break; - case 'SAD': - strokeColor = '#E3E9F1'; - break; - case 'WORRIED': - strokeColor = '#C7D1E0'; - break; - case 'ANGRY': - strokeColor = '#EFF3F8'; - break; - default: - strokeColor = '#48BB98'; - } - - const circle = ; - - offset += CIRCUMFERENCE * percentage; // 다음 원을 위한 offset 업데이트 - return circle; - })} - - {/* 중앙에 가장 많이 나타나는 감정 출력 */} -
- 감정 -

{iconPaths[maxEmotion].name}

-
-
-
-
- {EMOTIONS.map((emotion) => { - const count = emotionCounts[emotion] || 0; - const percentage = TOTAL_COUNT > 0 ? Math.floor((count / TOTAL_COUNT) * 100) : 0; // 퍼센트 계산 및 소수점 버리기 - - return ( -
-

- 감정 -

{percentage}%

-
- ); - })} -
-
-
-
- ); -} diff --git a/src/user/ui-profile/EmotionMonthlyLogs.tsx b/src/user/ui-profile/EmotionMonthlyLogs.tsx deleted file mode 100644 index 18d4ada8..00000000 --- a/src/user/ui-profile/EmotionMonthlyLogs.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useMonthlyEmotionLogs } from '@/hooks/useGetEmotion'; -import { Emotion } from '@/types/emotion'; -import { useEffect, useState } from 'react'; -import Calendar from './Calendar'; -import Chart from './Chart'; - -interface EmotionMonthlyLogsProps { - userId: number; -} - -export default function EmotionMonthlyLogs({ userId }: EmotionMonthlyLogsProps) { - // 현재 날짜를 상태로 관리 - const [currentDate, setCurrentDate] = useState(new Date()); - - // 감정 달력 객체 상태 추가 - const [emotionRequest, setEmotionRequest] = useState({ - userId, - year: currentDate.getFullYear(), - month: currentDate.getMonth() + 1, - }); - - // '월'이 변경될 때마다 request 업데이트 - useEffect(() => { - setEmotionRequest({ - userId, - year: currentDate.getFullYear(), - month: currentDate.getMonth() + 1, - }); - }, [currentDate]); - - // 월별 감정 로그 조회 - const { data: monthlyEmotionLogs = [] } = useMonthlyEmotionLogs(emotionRequest); - - return ( - <> - - - - ); -}