diff --git a/src/apis/getEmotion.ts b/src/apis/getEmotion.ts new file mode 100644 index 00000000..044ff3c3 --- /dev/null +++ b/src/apis/getEmotion.ts @@ -0,0 +1,25 @@ +import { EmotionType } from '@/types/emotion'; +import type { GetEmotionResponseType } from '@/schema/emotion'; +import { translateEmotionToKorean } from '@/utils/emotionMap'; +import httpClient from '.'; +import { getMe } from './user'; + +const getEmotion = async (): Promise => { + const user = await getMe(); + if (!user) { + throw new Error('로그인이 필요합니다.'); + } + + const response = await httpClient.get('/emotionLogs/today', { + params: { userId: user.id }, + }); + + if (response.status === 204) { + return null; // No content + } + + const koreanEmotion = translateEmotionToKorean(response.data.emotion); + return koreanEmotion; +}; + +export default getEmotion; diff --git a/src/apis/postEmotion.ts b/src/apis/postEmotion.ts new file mode 100644 index 00000000..98130bc7 --- /dev/null +++ b/src/apis/postEmotion.ts @@ -0,0 +1,24 @@ +import { EmotionType } from '@/types/emotion'; +import type { PostEmotionRequestType, PostEmotionResponseType } from '@/schema/emotion'; +import { translateEmotionToEnglish } from '@/utils/emotionMap'; +import httpClient from '.'; +import { getMe } from './user'; + +const postEmotion = async (emotion: EmotionType): Promise => { + const user = await getMe(); + if (!user) { + throw new Error('로그인이 필요합니다.'); + } + + const englishEmotion = translateEmotionToEnglish(emotion); + const request: PostEmotionRequestType = { emotion: englishEmotion }; + + const response = await httpClient.post('/emotionLogs/today', { + ...request, + userId: user.id, + }); + + return response.data; +}; + +export default postEmotion; diff --git a/src/components/Emotion/EmotionCard.tsx b/src/components/Emotion/EmotionCard.tsx index 1896f7cd..74134281 100644 --- a/src/components/Emotion/EmotionCard.tsx +++ b/src/components/Emotion/EmotionCard.tsx @@ -7,7 +7,7 @@ import React from 'react'; import cn from '@/lib/utils'; import Image from 'next/image'; -import { EmotionIconCardProps } from '@/types/EmotionTypes'; +import { EmotionIconCardProps } from '@/types/emotion'; // 아이콘 파일 경로 매핑 const iconPaths = { diff --git a/src/components/Emotion/EmotionSaveToast.tsx b/src/components/Emotion/EmotionSaveToast.tsx new file mode 100644 index 00000000..e7d24105 --- /dev/null +++ b/src/components/Emotion/EmotionSaveToast.tsx @@ -0,0 +1,34 @@ +/* + * 오늘의 감정을 선택하면 표시되는 toast입니다. + * 감정을 확인하기 위해 마이페이지로 연결됩니다. + */ + +import React, { useEffect } from 'react'; +import { useToast } from '@/components/ui/use-toast'; +import { ToastAction } from '@/components/ui/toast'; +import { useRouter } from 'next/router'; + +interface EmotionSaveToastProps { + iconType: string; +} + +function EmotionSaveToast({ iconType }: EmotionSaveToastProps) { + const { toast } = useToast(); + const router = useRouter(); + + useEffect(() => { + toast({ + title: '오늘의 감정이 저장되었습니다.', + description: `오늘의 감정: ${iconType}`, + action: ( + router.push('/mypage')}> + 확인하기 + + ), + }); + }, [iconType, toast, router]); + + return null; +} + +export default EmotionSaveToast; diff --git a/src/components/Emotion/EmotionSelector.tsx b/src/components/Emotion/EmotionSelector.tsx index 5a73639e..fc92728b 100644 --- a/src/components/Emotion/EmotionSelector.tsx +++ b/src/components/Emotion/EmotionSelector.tsx @@ -1,19 +1,21 @@ -/* - 여러 개의 EmotionIconCard를 관리합니다. - 사용자 인터페이스에 필요한 상호 작용 로직을 포함합니다. - */ - -import React, { useState } from 'react'; -import EmotionIconCard from '@/components/Emotion/EmotionCard'; +import React, { useState, useEffect } from 'react'; import useMediaQuery from '@/hooks/useMediaQuery'; -import { EmotionType, EmotionState } from '@/types/EmotionTypes'; +import EmotionIconCard from '@/components/Emotion/EmotionCard'; +import { EmotionType, EmotionState } from '@/types/emotion'; +import usePostEmotion from '@/hooks/usePostEmotion'; +import useGetEmotion from '@/hooks/useGetEmotion'; +import EmotionSaveToast from './EmotionSaveToast'; -// EmotionSelector 컴포넌트 함수 선언 +/** + * EmotionSelector 컴포넌트는 여러 개의 EmotionIconCard를 관리하고 + * 사용자의 오늘의 감정을 선택하고 저장하고 출력합니다. + */ function EmotionSelector() { + // 반응형 디자인을 위한 미디어 쿼리 훅 const isTablet = useMediaQuery('(min-width: 768px) and (max-width: 1024px)'); const isMobile = useMediaQuery('(max-width: 767px)'); - // 감정 카드 상태 관리 + // 감정 카드 상태 관리를 위한 useState 훅 const [states, setStates] = useState>({ 감동: 'Default', 기쁨: 'Default', @@ -22,13 +24,37 @@ function EmotionSelector() { 분노: 'Default', }); - // 감정 카드 클릭 핸들러 - const handleCardClick = (iconType: EmotionType) => { + // 현재 선택된 감정을 관리하는 useState 훅 + const [selectedEmotion, setSelectedEmotion] = useState(null); + // 오늘의 감정을 조회하기 위한 훅 + const { data: emotion, error: getError, isLoading: isGetLoading } = useGetEmotion(); + // 감정을 저장하기 위한 훅 + const postEmotionMutation = usePostEmotion(); + + // 컴포넌트가 마운트될 때 한 번만 실행되는 useEffect 훅 + // 오늘의 감정을 조회하고 상태를 업데이트합니다. + useEffect(() => { + if (emotion) { + setStates((prevStates) => ({ + ...prevStates, + [emotion]: 'Clicked', + })); + } + }, [emotion]); + + /** + * 감정 카드 클릭 핸들러 + * 사용자가 감정 카드를 클릭했을 때 호출됩니다. + * 클릭된 감정 카드를 'Clicked' 상태로 설정하고 나머지 카드는 'Unclicked' 상태로 설정합니다. + * 감정을 서버에 저장합니다. + * @param iconType - 클릭된 감정의 타입 + */ + const handleCardClick = async (iconType: EmotionType) => { setStates((prevStates) => { const newStates = { ...prevStates }; if (prevStates[iconType] === 'Clicked') { - // 현재 클릭된 카드가 다시 클릭되면 모두 Default로 설정 + // 현재 클릭된 카드가 다시 클릭되면 모든 카드를 Default로 설정 Object.keys(newStates).forEach((key) => { newStates[key as EmotionType] = 'Default'; }); @@ -41,8 +67,20 @@ function EmotionSelector() { return newStates; }); + + // 오늘의 감정 저장 + postEmotionMutation.mutate(iconType, { + onSuccess: (_, clickedIconType) => { + setSelectedEmotion(clickedIconType); + }, + onError: (error: unknown) => { + // eslint-disable-next-line + console.error(error); + }, + }); }; + // 반응형 디자인을 위한 카드 크기 설정 let containerClass = 'w-[544px] h-[136px] gap-4'; let cardSize: 'lg' | 'md' | 'sm' = 'lg'; @@ -54,12 +92,19 @@ function EmotionSelector() { cardSize = 'sm'; } + if (isGetLoading) return

Loading...

; + if (getError) return

{getError.message}

; + return ( -
- {(['감동', '기쁨', '고민', '슬픔', '분노'] as const).map((iconType) => ( - handleCardClick(iconType)} /> - ))} -
+ <> +
+ {(['감동', '기쁨', '고민', '슬픔', '분노'] as const).map((iconType) => ( + handleCardClick(iconType)} /> + ))} +
+ {/* 감정이 선택되었을 때 토스트 메시지 표시 */} + {selectedEmotion && } + ); } diff --git a/src/hooks/useGetEmotion.ts b/src/hooks/useGetEmotion.ts new file mode 100644 index 00000000..d8017abc --- /dev/null +++ b/src/hooks/useGetEmotion.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import getEmotion from '@/apis/getEmotion'; +import { EmotionType } from '@/types/emotion'; + +const useGetEmotion = () => + useQuery({ + queryKey: ['emotion'], + queryFn: getEmotion, + }); + +export default useGetEmotion; diff --git a/src/hooks/usePostEmotion.ts b/src/hooks/usePostEmotion.ts new file mode 100644 index 00000000..f13540ae --- /dev/null +++ b/src/hooks/usePostEmotion.ts @@ -0,0 +1,11 @@ +import { useMutation } from '@tanstack/react-query'; +import postEmotion from '@/apis/postEmotion'; +import { EmotionType } from '@/types/emotion'; +import { PostEmotionResponseType } from '@/schema/emotion'; + +const usePostEmotion = () => + useMutation({ + mutationFn: postEmotion, + }); + +export default usePostEmotion; diff --git a/src/schema/emotion.ts b/src/schema/emotion.ts new file mode 100644 index 00000000..a2239be9 --- /dev/null +++ b/src/schema/emotion.ts @@ -0,0 +1,23 @@ +import * as z from 'zod'; + +export const PostEmotionRequest = z.object({ + emotion: z.enum(['MOVED', 'JOY', 'WORRY', 'SADNESS', 'ANGER']), +}); + +export const PostEmotionResponse = z.object({ + createdAt: z.coerce.date(), + emotion: z.enum(['MOVED', 'JOY', 'WORRY', 'SADNESS', 'ANGER']), + userId: z.number(), + id: z.number(), +}); + +export const GetEmotionResponse = z.object({ + createdAt: z.coerce.date(), + emotion: z.enum(['MOVED', 'JOY', 'WORRY', 'SADNESS', 'ANGER']), + userId: z.number(), + id: z.number(), +}); + +export type PostEmotionRequestType = z.infer; +export type PostEmotionResponseType = z.infer; +export type GetEmotionResponseType = z.infer; diff --git a/src/types/EmotionTypes.ts b/src/types/emotion.ts similarity index 100% rename from src/types/EmotionTypes.ts rename to src/types/emotion.ts diff --git a/src/utils/emotionMap.ts b/src/utils/emotionMap.ts new file mode 100644 index 00000000..84b5663e --- /dev/null +++ b/src/utils/emotionMap.ts @@ -0,0 +1,23 @@ +import { EmotionType } from '@/types/emotion'; + +const emotionMap: Record = { + 감동: 'MOVED', + 기쁨: 'JOY', + 고민: 'WORRY', + 슬픔: 'SADNESS', + 분노: 'ANGER', +}; + +const reverseEmotionMap: Record<'MOVED' | 'JOY' | 'WORRY' | 'SADNESS' | 'ANGER', EmotionType> = { + MOVED: '감동', + JOY: '기쁨', + WORRY: '고민', + SADNESS: '슬픔', + ANGER: '분노', +}; + +const translateEmotionToEnglish = (emotion: EmotionType): 'MOVED' | 'JOY' | 'WORRY' | 'SADNESS' | 'ANGER' => emotionMap[emotion]; + +const translateEmotionToKorean = (emotion: 'MOVED' | 'JOY' | 'WORRY' | 'SADNESS' | 'ANGER'): EmotionType => reverseEmotionMap[emotion]; + +export { translateEmotionToEnglish, translateEmotionToKorean };