diff --git a/src/entities/chart/lib/isDailyEmotion.ts b/src/entities/chart/lib/isDailyEmotion.ts index 5c08c8e..3b287ae 100644 --- a/src/entities/chart/lib/isDailyEmotion.ts +++ b/src/entities/chart/lib/isDailyEmotion.ts @@ -1,12 +1,12 @@ import { - DailyEmotionType, - WeeklyEmotionSummaryType -} from '@/shared/model/moodTypes'; + DailyConditionType, + WeeklyConditionSummaryType +} from '@/shared/model/conditionTypes'; const isDailyEmotion = ( - data: DailyEmotionType[] | WeeklyEmotionSummaryType[] -): data is DailyEmotionType[] => { - return (data as DailyEmotionType[])[0]?.day !== undefined; + data: DailyConditionType[] | WeeklyConditionSummaryType[] +): data is DailyConditionType[] => { + return (data as DailyConditionType[])[0]?.day !== undefined; }; export default isDailyEmotion; diff --git a/src/entities/chart/ui/Chart.stories.tsx b/src/entities/chart/ui/Chart.stories.tsx index ddc5dd2..9f101ef 100644 --- a/src/entities/chart/ui/Chart.stories.tsx +++ b/src/entities/chart/ui/Chart.stories.tsx @@ -1,8 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import Chart from './Chart'; import { - DailyEmotionType, -} from '@/shared/model/moodTypes'; + DailyConditionType, +} from '@/shared/model/conditionTypes'; const meta: Meta = { component: Chart, @@ -14,7 +14,7 @@ export default meta; type Story = StoryObj; -const dailyEmotionData : DailyEmotionType[] = [ +const dailyEmotionData : DailyConditionType[] = [ { day: 'Mon' as const, mood: null }, { day: 'Tue' as const, mood: '나쁨' }, { day: 'Wed' as const, mood: '나쁨' }, diff --git a/src/entities/chart/ui/Chart.tsx b/src/entities/chart/ui/Chart.tsx index 62878be..8d79e66 100644 --- a/src/entities/chart/ui/Chart.tsx +++ b/src/entities/chart/ui/Chart.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { - DailyEmotionType, - WeeklyEmotionSummaryType -} from '@/shared/model/moodTypes'; + DailyConditionType, + WeeklyConditionSummaryType +} from '@/shared/model/conditionTypes'; import { LineChart, @@ -18,7 +18,7 @@ import isDailyEmotion from '../lib/isDailyEmotion'; const emotions = ['매우 나쁨', '나쁨', '보통', '좋음', '매우 좋음']; interface ChartProps { - data: DailyEmotionType[] | WeeklyEmotionSummaryType[]; + data: DailyConditionType[] | WeeklyConditionSummaryType[]; } const Chart = ({ data }: ChartProps) => { diff --git a/src/entities/music/api/fetchMusicRecommendation.ts b/src/entities/music/api/fetchMusicRecommendation.ts new file mode 100644 index 0000000..96a12ab --- /dev/null +++ b/src/entities/music/api/fetchMusicRecommendation.ts @@ -0,0 +1,35 @@ +import { createGptRequestQuery } from '../lib/createGptRequestQuery'; +import { gptAnswerType, MoodDataType } from '../model/type'; +import { fetchGptRecommend } from './fetchGptRecommend'; + +/** + * 음악 아이텝을 반환합니다. + * @param emotionData d + * @param param1 + * @returns + */ +export const fetchMusicRecommendation = async ( + emotionData: MoodDataType | null, + { + onSuccess, + onError, + onValidationError + }: { + onSuccess: (data: gptAnswerType) => void; + onError: () => void; + onValidationError: () => void; + } +) => { + if (!emotionData) { + onValidationError(); + return; + } + try { + const requestQuery = createGptRequestQuery(emotionData); + const recommendedMusic = await fetchGptRecommend(requestQuery); + onSuccess(recommendedMusic); + } catch (error) { + onError(); + throw error; + } +}; diff --git a/src/entities/music/index.ts b/src/entities/music/index.ts index 11aaf20..1aa1221 100644 --- a/src/entities/music/index.ts +++ b/src/entities/music/index.ts @@ -1,3 +1,8 @@ +export { fetchGptRecommend } from './api/fetchGptRecommend'; +export { youtubeSearch } from './api/fetchMusicList'; +export { spotifySearch } from './api/fetchMusicList'; +export { fetchMusicRecommendation } from './api/fetchMusicRecommendation'; +export { useMusicSearch } from './hooks/useMusicSearch'; +export { createGptRequestQuery } from './lib/createGptRequestQuery'; export { MusicCard } from './ui/MusicCard'; export { EmptyMusicCard } from './ui/EmptyMusicCard'; -export { useMusicSearch } from './hooks/useMusicSearch'; diff --git a/src/entities/music/lib/createGptRequestQuery.ts b/src/entities/music/lib/createGptRequestQuery.ts new file mode 100644 index 0000000..6821bdb --- /dev/null +++ b/src/entities/music/lib/createGptRequestQuery.ts @@ -0,0 +1,28 @@ +import { gptQueryParamsType, MoodDataType } from '../model/type'; + +// 테스트 다이러리 +const INITIAL_DIARY = { + title: '우울해', + content: '너무 우울해서 빵샀어' +}; + +/** + * 감정데이터, 일기데이터를 조합해 쿼리 데이터를 생성 + * @param emotionData + * @returns + */ +export const createGptRequestQuery = ( + emotionData: MoodDataType +): gptQueryParamsType => { + return { + title: INITIAL_DIARY.title, + content: INITIAL_DIARY.content, + ...(emotionData?.mood && { mood: emotionData.mood }), + ...(emotionData?.emotion && { emotion: emotionData.emotion }), + ...(emotionData?.subEmotion && { + subemotion: emotionData.subEmotion.filter( + (emotion): emotion is string => emotion !== null + ) + }) + }; +}; diff --git a/src/entities/music/model/type.ts b/src/entities/music/model/type.ts index c7fe70b..0ad2eb5 100644 --- a/src/entities/music/model/type.ts +++ b/src/entities/music/model/type.ts @@ -1,3 +1,5 @@ +import { ConditionType } from '@/shared/model/conditionTypes'; + export interface MusicItem { youtubeId: string; thumbnailUrl: string; @@ -48,3 +50,8 @@ export interface gptQueryParamsType { } export type gptAnswerType = string[]; +export interface MoodDataType { + mood: ConditionType; + emotion: string | null; + subEmotion: (string | null)[]; +} diff --git a/src/entities/music/model/useMusicStore.ts b/src/entities/music/model/useMusicStore.ts deleted file mode 100644 index 52872f6..0000000 --- a/src/entities/music/model/useMusicStore.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { create } from 'zustand'; -import { MusicItem } from './type'; -import React from 'react'; - -interface MusicStore { - selectedMusic: MusicItem | null; - setSelectedMusic: (music: MusicItem | null) => void; - clearSelectedMusic: () => void; -} - -export const useMusicStore = create((set) => ({ - selectedMusic: null, - setSelectedMusic: (music) => set({ selectedMusic: music }), - clearSelectedMusic: () => set({ selectedMusic: null }) -})); - -export default useMusicStore; diff --git a/src/features/chart/lib/isWeekly.ts b/src/features/chart/lib/isWeekly.ts index 5de4e2b..46cd774 100644 --- a/src/features/chart/lib/isWeekly.ts +++ b/src/features/chart/lib/isWeekly.ts @@ -1,7 +1,7 @@ import { WeeklyDataType, MonthlyDataType -} from '../../../shared/model/moodTypes'; +} from '../../../shared/model/conditionTypes'; const isWeekly = ( data: WeeklyDataType | MonthlyDataType diff --git a/src/features/diary-write/musicList/ui/MusicCardList.tsx b/src/features/diary-write/musicList/ui/MusicCardList.tsx index 3767a3c..73b2653 100644 --- a/src/features/diary-write/musicList/ui/MusicCardList.tsx +++ b/src/features/diary-write/musicList/ui/MusicCardList.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react'; import { Container, HiddenYoutubeContainer } from './MusicCardList.styled'; import { MusicItem, MusicCardListProps } from '../model/type'; import { EmptyMusicCard, MusicCard } from '../../../../entities/music'; -import useMusicStore from '@/entities/music/model/useMusicStore'; export const MusicCardList = ({ responseMusicList, @@ -38,7 +37,6 @@ export const MusicCardList = ({ } }; - // TODO - iframe 유튜브 api 모듈로 변경 return ( diff --git a/src/pages/DiaryWritePage/model/type.ts b/src/pages/DiaryWritePage/model/type.ts index 3c806d6..f3bdd10 100644 --- a/src/pages/DiaryWritePage/model/type.ts +++ b/src/pages/DiaryWritePage/model/type.ts @@ -1,7 +1,7 @@ -import { EmotionType } from '@/shared/model/moodTypes'; +import { ConditionType } from '@/shared/model/conditionTypes'; export interface MoodDataType { - mood: EmotionType; + mood: ConditionType; emotion: string | null; subEmotion: (string | null)[]; } diff --git a/src/pages/DiaryWritePage/ui/DiaryWritePage.tsx b/src/pages/DiaryWritePage/ui/DiaryWritePage.tsx index dfd9ff0..26e29b2 100644 --- a/src/pages/DiaryWritePage/ui/DiaryWritePage.tsx +++ b/src/pages/DiaryWritePage/ui/DiaryWritePage.tsx @@ -1,32 +1,20 @@ -import React, { useEffect, useState } from 'react'; -import { SelectMusicContainer } from '@/widgets/select-music/ui/SelectMusicContainer'; -import Header from '@/widgets/header/ui/Header'; -import { Container, Section } from './DiaryWritePage.styled'; +import React, { useState } from 'react'; +import { fetchMusicRecommendation } from '@/entities/music'; +import { gptAnswerType, MusicItem } from '@/entities/music/model/type'; +import { useToastStore } from '@/features/Toast/hooks/useToastStore'; +import { SelectMusicContainer } from '@/widgets/select-music'; import { SelectEmotionContainer } from '@/widgets/select-emotion'; -import { WriteDiaryContainer } from '@/widgets/write-diary'; -import { fetchGptRecommend } from '@/entities/music/api/fetchGptRecommend'; -import { - gptAnswerType, - gptQueryParamsType, - MusicItem -} from '@/entities/music/model/type'; -import { EmotionType } from '@/shared/model/moodTypes'; -import { Emotions } from '@/shared/model/EmotionEnum'; +import { Container, Section } from './DiaryWritePage.styled'; import { MoodDataType } from '../model/type'; // TODO - 리팩토링 (로직 분리) export const DiaryWritePage = () => { - // 테스트 다이러리 - const diary = { - title: '우울해', - content: '너무 우울해서 빵샀어' - }; - + const { addToast } = useToastStore(); // const [diaryData, setDiaryData] = useState(); - const [emotionData, setEmotionData] = useState(null); - const [musicData, setMusicData] = useState(null); - - const [gptRecommendMusicList, setGptRecommendMusicList] = + const [userEmotionState, setUserEmotionState] = + useState(null); + const [selectedMusic, setSelectedMusic] = useState(null); + const [recommendedMusicList, setRecommendedMusicList] = useState([]); // // 일기 데이터가 넘어오면 셋팅 @@ -34,54 +22,38 @@ export const DiaryWritePage = () => { // setDiaryData(diaryData); // }; - const handleMoodSelect = (moodState: MoodDataType) => { - console.log('감정 데이터 셋팅 : ', moodState); - setEmotionData(moodState); - }; - - const handleMusicSelect = (music: MusicItem | null) => { - console.log('음악 데이터 셋팅 : ', music); - setMusicData(music); + const handleEmotionSubmit = (submittedEmotion: MoodDataType) => { + console.log('사용자 감정 데이터 저장:', submittedEmotion); + setUserEmotionState(submittedEmotion); }; - // TODO - diary 파라미터 넣어야함 - const createGptQuery = (mood: MoodDataType) => { - const gptQuery: gptQueryParamsType = { - title: '우울해', - content: '너무 우울해서 빵샀어', - ...(mood?.mood && { mood: mood.mood }), - ...(mood?.emotion && { emotion: mood.emotion }), - ...(mood?.subEmotion && { - subemotion: mood.subEmotion.filter( - (item): item is string => item !== null - ) - }) - }; - return gptQuery; + const handleMusicSelection = (selectedMusicItem: MusicItem | null) => { + console.log('선택된 음악 데이터 저장:', selectedMusicItem); + setSelectedMusic(selectedMusicItem); }; - const testFunction = async (mood: MoodDataType | null) => { - console.log('테스트 함수 실행됨'); - if (mood === null) { - console.log('감정 선택을 먼저 완료해주세요'); - } else { - const gptQuery = createGptQuery(mood); - const recommendations = await fetchGptRecommend(gptQuery); - setGptRecommendMusicList(recommendations); - } + const handleFetchRecommendations = async ( + emotionData: MoodDataType | null + ) => { + await fetchMusicRecommendation(emotionData, { + onSuccess: setRecommendedMusicList, + onError: () => addToast('음악 추천 요청에 실패했습니다.', 'error'), + onValidationError: () => + addToast('먼저 감정을 선택해주세요!', 'warning') + }); }; return (
- {/* */} + {/* */}
diff --git a/src/shared/api/mood.ts b/src/shared/api/mood.ts index 577c5ce..9f93f74 100644 --- a/src/shared/api/mood.ts +++ b/src/shared/api/mood.ts @@ -1,4 +1,4 @@ -import { WeeklyDataType, MonthlyDataType } from '../model/moodTypes'; +import { WeeklyDataType, MonthlyDataType } from '../model/conditionTypes'; import { moodQueryParamType } from '../model/moodQueryParamType'; import defaultApi from '@/shared/api/api'; diff --git a/src/shared/hooks/useGetMood.ts b/src/shared/hooks/useGetMood.ts index 830077a..af3e1c3 100644 --- a/src/shared/hooks/useGetMood.ts +++ b/src/shared/hooks/useGetMood.ts @@ -1,4 +1,4 @@ -import { WeeklyDataType, MonthlyDataType } from '../model/moodTypes'; +import { WeeklyDataType, MonthlyDataType } from '../model/conditionTypes'; import { useQuery } from '@tanstack/react-query'; import { moodQueryParamType } from '../model/moodQueryParamType'; import { getMoodApi } from '../api/mood'; diff --git a/src/shared/model/moodTypes.ts b/src/shared/model/conditionTypes.ts similarity index 51% rename from src/shared/model/moodTypes.ts rename to src/shared/model/conditionTypes.ts index ba687bb..3c659bb 100644 --- a/src/shared/model/moodTypes.ts +++ b/src/shared/model/conditionTypes.ts @@ -1,4 +1,4 @@ -export type EmotionType = +export type ConditionType = | '좋음' | '나쁨' | '보통' @@ -6,26 +6,26 @@ export type EmotionType = | '매우 나쁨' | null; -export interface DailyEmotionType { +export interface DailyConditionType { day: 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun'; - mood: EmotionType; + mood: ConditionType; } export interface WeeklyDataType { period: string; - mostFrequentEmotion: EmotionType; + mostFrequentEmotion: ConditionType; frequency: number | null; - allEmotions: DailyEmotionType[]; + allEmotions: DailyConditionType[]; } -export interface WeeklyEmotionSummaryType { +export interface WeeklyConditionSummaryType { week: number; - mostFrequentEmotion: EmotionType; + mostFrequentEmotion: ConditionType; } export interface MonthlyDataType { period: string; - weeklyResults: WeeklyEmotionSummaryType[]; + weeklyResults: WeeklyConditionSummaryType[]; frequency: number | null; - mostFrequentEmotion: EmotionType; + mostFrequentEmotion: ConditionType; } diff --git a/src/widgets/select-emotion/model/type.ts b/src/widgets/select-emotion/model/type.ts index f5a5877..32a53d8 100644 --- a/src/widgets/select-emotion/model/type.ts +++ b/src/widgets/select-emotion/model/type.ts @@ -1,5 +1,5 @@ import { MoodDataType } from '@/pages/DiaryWritePage/model/type'; -import { EmotionType } from '@/shared/model/moodTypes'; +import { ConditionType } from '@/shared/model/conditionTypes'; export interface SelectEmotionContainerProps { onMoodSelect: (mood: MoodDataType) => void; @@ -7,7 +7,7 @@ export interface SelectEmotionContainerProps { } export interface MoodState { - mood: EmotionType; + mood: ConditionType; emotion: string | null; subEmotion: (string | null)[]; } diff --git a/src/widgets/select-emotion/ui/SelectEmotionContainer.tsx b/src/widgets/select-emotion/ui/SelectEmotionContainer.tsx index ef6da0d..46a9d3c 100644 --- a/src/widgets/select-emotion/ui/SelectEmotionContainer.tsx +++ b/src/widgets/select-emotion/ui/SelectEmotionContainer.tsx @@ -7,14 +7,16 @@ import { import { ButtonContainer, Container } from './SelectEmotionContainer.styled'; import { EmotionButtonGroup } from '@/features/diary-write/emotion/ui/EmotionButtonGroup'; import { useState } from 'react'; -import { EmotionType } from '@/shared/model/moodTypes'; +import { ConditionType } from '@/shared/model/conditionTypes'; import Button from '@/shared/ui/Button/Button'; import { Emotions, getEmotionInfo } from '@/shared/model/EmotionEnum'; +import { useToastStore } from '@/features/Toast/hooks/useToastStore'; export const SelectEmotionContainer = ({ onMoodSelect, onNext }: SelectEmotionContainerProps) => { + const { addToast } = useToastStore(); const [moodState, setMoodState] = useState(INITIAL_MOOD_STATE); const isNextButtonActive = (): boolean => { @@ -25,7 +27,7 @@ export const SelectEmotionContainer = ({ ); }; - const handleConditionChange = (condition: EmotionType) => { + const handleConditionChange = (condition: ConditionType) => { setMoodState((prev) => ({ ...prev, mood: condition @@ -41,7 +43,7 @@ export const SelectEmotionContainer = ({ const info = getEmotionInfo(item); return info.description; } catch (error) { - console.error('감정 변환 에러:', error); + addToast('감정 변환 에러.', 'error'); return null; } }); @@ -56,6 +58,8 @@ export const SelectEmotionContainer = ({ if (isNextButtonActive()) { onMoodSelect(moodState); onNext?.(moodState); + } else { + addToast('데이터를 모두 입력해주세요!', 'warning'); } }; diff --git a/src/widgets/select-music/index.ts b/src/widgets/select-music/index.ts new file mode 100644 index 0000000..3b95f6d --- /dev/null +++ b/src/widgets/select-music/index.ts @@ -0,0 +1 @@ +export { SelectMusicContainer } from './ui/SelectMusicContainer'; diff --git a/src/widgets/select-music/ui/SelectMusicContainer.styled.tsx b/src/widgets/select-music/ui/SelectMusicContainer.styled.tsx index 92a12f6..aaf32a2 100644 --- a/src/widgets/select-music/ui/SelectMusicContainer.styled.tsx +++ b/src/widgets/select-music/ui/SelectMusicContainer.styled.tsx @@ -7,3 +7,11 @@ export const Container = styled.div` align-items: center; gap: 1rem; `; + +export const SearchInputWrapper = styled.div<{ isVisible: boolean }>` + width: 100%; + max-height: ${(props) => (props.isVisible ? '100px' : '0')}; + opacity: ${(props) => (props.isVisible ? '1' : '0')}; + overflow: hidden; + transition: all 0.2s ease-in-out; +`; diff --git a/src/widgets/select-music/ui/SelectMusicContainer.tsx b/src/widgets/select-music/ui/SelectMusicContainer.tsx index c3a9c12..927b297 100644 --- a/src/widgets/select-music/ui/SelectMusicContainer.tsx +++ b/src/widgets/select-music/ui/SelectMusicContainer.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { Container } from './SelectMusicContainer.styled'; +import { Container, SearchInputWrapper } from './SelectMusicContainer.styled'; import { MusicSearchInput, SearchModeButtonGroup, @@ -66,9 +66,9 @@ export const SelectMusicContainer = ({ selectedType={selectedType} onChange={handleTypeChange} /> - {selectedType === SEARCH_TYPE.USER && ( + - )} +