diff --git a/src/features/diary-write/condition/model/type.ts b/src/features/diary-write/condition/model/type.ts index 55c86b2..3fb2c6a 100644 --- a/src/features/diary-write/condition/model/type.ts +++ b/src/features/diary-write/condition/model/type.ts @@ -10,5 +10,5 @@ export type Condition = (typeof CONDITIONS)[number]; export interface ConditionButtonGroupProps { selectedCondition: Condition | null; - onChange: (condition: Condition) => void; + onConditionChange: (condition: Condition) => void; } diff --git a/src/features/diary-write/condition/ui/ConditionButtonGroup.stories.tsx b/src/features/diary-write/condition/ui/ConditionButtonGroup.stories.tsx index c008017..e02cb41 100644 --- a/src/features/diary-write/condition/ui/ConditionButtonGroup.stories.tsx +++ b/src/features/diary-write/condition/ui/ConditionButtonGroup.stories.tsx @@ -16,7 +16,7 @@ const meta = { options: [...CONDITIONS, null], description: '현재 선택되어있는 기분', }, - onChange: { + onConditionChange: { description: '기분 버튼 클릭시 호출 : 부모 컴포넌트로 선택된 기분 전달' } } @@ -29,7 +29,7 @@ type Story = StoryObj; export const Default: Story = { args: { selectedCondition: null, - onChange: (condition) => { + onConditionChange: (condition) => { console.log(condition); }, }, @@ -39,7 +39,7 @@ export const Default: Story = { export const Selected: Story = { args: { selectedCondition: '보통', - onChange: (condition) => { + onConditionChange: (condition) => { console.log(condition); }, }, @@ -48,7 +48,7 @@ export const Selected: Story = { export const Interactive: Story = { args: { selectedCondition: null, - onChange: (condition) => { + onConditionChange: (condition) => { alert(`${condition}`); }, }, diff --git a/src/features/diary-write/condition/ui/ConditionButtonGroup.tsx b/src/features/diary-write/condition/ui/ConditionButtonGroup.tsx index 8fc1ce9..7c75cd9 100644 --- a/src/features/diary-write/condition/ui/ConditionButtonGroup.tsx +++ b/src/features/diary-write/condition/ui/ConditionButtonGroup.tsx @@ -6,7 +6,7 @@ import { ConditionButton } from '../../../../entities/condition'; // condition으로 네이밍 변경 해야합니다 export const ConditionButtonGroup = ({ selectedCondition, // 현재 선택되어있는 기분 상태 - onChange // 부모 컴포넌트로 선택된 기분 전달 + onConditionChange // 부모 컴포넌트로 선택된 기분 전달 }: ConditionButtonGroupProps) => { return ( @@ -15,7 +15,7 @@ export const ConditionButtonGroup = ({ onChange(condition)} + onClick={() => onConditionChange(condition)} > {condition} diff --git a/src/features/diary-write/emotion/ui/EmotionButtonGroup.tsx b/src/features/diary-write/emotion/ui/EmotionButtonGroup.tsx index 6556370..667b863 100644 --- a/src/features/diary-write/emotion/ui/EmotionButtonGroup.tsx +++ b/src/features/diary-write/emotion/ui/EmotionButtonGroup.tsx @@ -11,6 +11,7 @@ import EmotionButtonList from '@/shared/EmotionButtonList/ui/EmotionButtonList'; import { KeywordButton } from '@/entities/KeywordButton/ui/KeywordButton'; import { Emotions } from '@/shared/model/EmotionEnum'; +// TODO - 초기값이 undefined로 들어오는데 null로 들어오도록 변경 export const EmotionButtonGroup: React.FC = ({ initialKeywords = [null, null, null, null, null], onKeywordsChange diff --git a/src/features/diary-write/musicList/model/type.ts b/src/features/diary-write/musicList/model/type.ts index 268c74a..86e1375 100644 --- a/src/features/diary-write/musicList/model/type.ts +++ b/src/features/diary-write/musicList/model/type.ts @@ -7,6 +7,8 @@ export interface MusicItem { export interface MusicCardListProps { type: string; // 리스트 타입 + responseMusicList: MusicItem[]; - // onChange: (music: MusicItem) => void; + selectedMusic: MusicItem | null; + onChange: (music: MusicItem | null) => void; } diff --git a/src/features/diary-write/musicList/ui/MusicCardList.stories.tsx b/src/features/diary-write/musicList/ui/MusicCardList.stories.tsx index 63189d7..105af49 100644 --- a/src/features/diary-write/musicList/ui/MusicCardList.stories.tsx +++ b/src/features/diary-write/musicList/ui/MusicCardList.stories.tsx @@ -28,26 +28,29 @@ const sampleMusicList: MusicItem[] = [ } ]; -export const GPTRecommend: Story = { +export const GptSearchMode: Story = { args: { - type: SEARCH_TYPE.GPT, + type: 'gptSearch', responseMusicList: sampleMusicList, - // onChange: (item: MusicItem) => console.log('Selected:', item) + selectedMusic: null, // 추가 + onChange: (music) => console.log('Selected music:', music) // 추가 } }; -export const UserSearch: Story = { +export const UserSearchMode: Story = { args: { - type: SEARCH_TYPE.USER, + type: 'userSearch', responseMusicList: sampleMusicList, - // onChange: (item: MusicItem) => console.log('Selected:', item) + selectedMusic: null, // 추가 + onChange: (music) => console.log('Selected music:', music) // 추가 } }; -export const Empty: Story = { +export const EmptyList: Story = { args: { - type: SEARCH_TYPE.USER, + type: 'userSearch', responseMusicList: [], - // onChange: (item: MusicItem) => console.log('Selected:', item) + selectedMusic: null, // 추가 + onChange: (music) => console.log('Selected music:', music) // 추가 } }; \ No newline at end of file diff --git a/src/features/diary-write/musicList/ui/MusicCardList.tsx b/src/features/diary-write/musicList/ui/MusicCardList.tsx index 5a31b9d..3767a3c 100644 --- a/src/features/diary-write/musicList/ui/MusicCardList.tsx +++ b/src/features/diary-write/musicList/ui/MusicCardList.tsx @@ -5,12 +5,11 @@ import { EmptyMusicCard, MusicCard } from '../../../../entities/music'; import useMusicStore from '@/entities/music/model/useMusicStore'; export const MusicCardList = ({ - responseMusicList - // onChange + responseMusicList, + selectedMusic, + onChange }: MusicCardListProps) => { const [nowPlaying, setNowPaying] = useState(null); - const { selectedMusic, setSelectedMusic, clearSelectedMusic } = - useMusicStore(); /** * iframe에 비디오 아이디를 셋팅합니다. @@ -27,14 +26,15 @@ export const MusicCardList = ({ }; /** - * 사용자가 선택한 음악 정보를 셋팅합니다. + * 사용자가 선택한 음악 정보를 리스트로 전달 * @param item :MusicItem 클릭한 카드 컴포넌트 음악 정보 */ const handleClick = (item: MusicItem) => { - if (item === selectedMusic) { - clearSelectedMusic(); + const isAlreadySelected = selectedMusic?.youtubeId === item.youtubeId; + if (isAlreadySelected) { + onChange(null); } else { - setSelectedMusic(item); + onChange(item); } }; diff --git a/src/pages/DiaryWritePage/model/type.ts b/src/pages/DiaryWritePage/model/type.ts new file mode 100644 index 0000000..3c806d6 --- /dev/null +++ b/src/pages/DiaryWritePage/model/type.ts @@ -0,0 +1,7 @@ +import { EmotionType } from '@/shared/model/moodTypes'; + +export interface MoodDataType { + mood: EmotionType; + emotion: string | null; + subEmotion: (string | null)[]; +} diff --git a/src/pages/DiaryWritePage/ui/DiaryWritePage.styled.tsx b/src/pages/DiaryWritePage/ui/DiaryWritePage.styled.tsx index c114613..320521c 100644 --- a/src/pages/DiaryWritePage/ui/DiaryWritePage.styled.tsx +++ b/src/pages/DiaryWritePage/ui/DiaryWritePage.styled.tsx @@ -3,11 +3,13 @@ import styled from 'styled-components'; export const Container = styled.div` display: flex; flex-direction: column; + align-items: center; `; export const Section = styled.div` display: flex; flex-direction: column; align-items: center; - padding: 0 200px 0 200px; + width: 960px; + gap: 200px; `; diff --git a/src/pages/DiaryWritePage/ui/DiaryWritePage.tsx b/src/pages/DiaryWritePage/ui/DiaryWritePage.tsx index bd3537a..dfd9ff0 100644 --- a/src/pages/DiaryWritePage/ui/DiaryWritePage.tsx +++ b/src/pages/DiaryWritePage/ui/DiaryWritePage.tsx @@ -5,21 +5,26 @@ import { Container, Section } from './DiaryWritePage.styled'; import { SelectEmotionContainer } from '@/widgets/select-emotion'; import { WriteDiaryContainer } from '@/widgets/write-diary'; import { fetchGptRecommend } from '@/entities/music/api/fetchGptRecommend'; -import { gptAnswerType, gptQueryParamsType } from '@/entities/music/model/type'; +import { + gptAnswerType, + gptQueryParamsType, + MusicItem +} from '@/entities/music/model/type'; +import { EmotionType } from '@/shared/model/moodTypes'; +import { Emotions } from '@/shared/model/EmotionEnum'; +import { MoodDataType } from '../model/type'; +// TODO - 리팩토링 (로직 분리) export const DiaryWritePage = () => { - // 테스트 데이터 - const testdiary: gptQueryParamsType = { - mood: '매우 나쁨', - emotion: '슬픔', - subemotion: ['슬픔'], + // 테스트 다이러리 + const diary = { title: '우울해', content: '너무 우울해서 빵샀어' }; - // const testdata = '뉴진스 supernatural'; - const [diaryData, setDiaryData] = useState(); - const [emotionData, setEmotionData] = useState(); + // const [diaryData, setDiaryData] = useState(); + const [emotionData, setEmotionData] = useState(null); + const [musicData, setMusicData] = useState(null); const [gptRecommendMusicList, setGptRecommendMusicList] = useState([]); @@ -29,31 +34,53 @@ export const DiaryWritePage = () => { // setDiaryData(diaryData); // }; - // // 감정 데이터가 넘어오면 셋팅 - // const handleEmotionSelect = (emotionData) => { - // setEmotionData(emotionData); - // }; + const handleMoodSelect = (moodState: MoodDataType) => { + console.log('감정 데이터 셋팅 : ', moodState); + setEmotionData(moodState); + }; - // const checkAndFetchRecommendations = async () => { - // const recommendations = await fetchGptRecommend(testdiary); - // setGptRecommendMusicList(recommendations); - // }; + const handleMusicSelect = (music: MusicItem | null) => { + console.log('음악 데이터 셋팅 : ', music); + setMusicData(music); + }; - const testFunction = async () => { - const recommendations = await fetchGptRecommend(testdiary); - setGptRecommendMusicList(recommendations); + // 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 testFunction = async (mood: MoodDataType | null) => { + console.log('테스트 함수 실행됨'); + if (mood === null) { + console.log('감정 선택을 먼저 완료해주세요'); + } else { + const gptQuery = createGptQuery(mood); + const recommendations = await fetchGptRecommend(gptQuery); + setGptRecommendMusicList(recommendations); + } }; return ( -
- {/* - */} - + {/* */} +
diff --git a/src/shared/EmotionButtonList/ui/EmotionButtonList.stories.tsx b/src/shared/EmotionButtonList/ui/EmotionButtonList.stories.tsx index affe1f1..8fdc409 100644 --- a/src/shared/EmotionButtonList/ui/EmotionButtonList.stories.tsx +++ b/src/shared/EmotionButtonList/ui/EmotionButtonList.stories.tsx @@ -1,39 +1,39 @@ -// import React from 'react'; -// import type { Meta, StoryObj } from '@storybook/react'; -// import EmotionButtonList from './EmotionButtonList'; -// import { Emotions } from '../../../shared/model/EmotionEnum'; +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import EmotionButtonList from './EmotionButtonList'; +import { Emotions } from '../../../shared/model/EmotionEnum'; -// const meta: Meta = { -// component: EmotionButtonList, -// title: 'Shared/EmotionButtonList', -// tags: ['autodocs'], -// argTypes: { -// isPrimary: { -// control: { type: 'boolean' }, -// description: '대표 감정 : true, 서브 감정 : false ', -// }, -// maxSelections: { -// control: { type: 'number' }, -// description: '사용자가 선택할 수 있는 최대 감정 버튼 개수', -// }, -// initialSelectedEmotions: { -// control: { -// type: 'array', -// options: Object.values(Emotions), -// }, -// description: '초기 선택된 감정 목록입니다.\n\n' + -// '배열의 크기가 maxSelections 수량보다 클 경우, 앞에서부터 maxSelections 수량만큼 설정됩니다.', -// }, -// onSelectionChange: { -// description: '선택된 감정 목록이 변경될 때 호출되는 콜백 함수', -// action: 'onSelectionChange', -// }, -// }, -// }; +const meta: Meta = { + component: EmotionButtonList, + title: 'Shared/EmotionButtonList', + tags: ['autodocs'], + argTypes: { + isPrimary: { + control: { type: 'boolean' }, + description: '대표 감정 : true, 서브 감정 : false ', + }, + maxSelections: { + control: { type: 'number' }, + description: '사용자가 선택할 수 있는 최대 감정 버튼 개수', + }, + initialSelectedEmotions: { + control: { + type: 'select', + options: Object.values(Emotions), + }, + description: '초기 선택된 감정 목록입니다.\n\n' + + '배열의 크기가 maxSelections 수량보다 클 경우, 앞에서부터 maxSelections 수량만큼 설정됩니다.', + }, + onSelectionChange: { + description: '선택된 감정 목록이 변경될 때 호출되는 콜백 함수', + action: 'onSelectionChange', + }, + }, +}; -// export default meta; +export default meta; -// type Story = StoryObj; +type Story = StoryObj; // export const Default: Story = { // args: { diff --git a/src/shared/EmotionButtonList/ui/EmotionButtonList.tsx b/src/shared/EmotionButtonList/ui/EmotionButtonList.tsx index 9df032d..61f1394 100644 --- a/src/shared/EmotionButtonList/ui/EmotionButtonList.tsx +++ b/src/shared/EmotionButtonList/ui/EmotionButtonList.tsx @@ -15,6 +15,12 @@ interface EmotionListProps { * 대표 감정 모드와 서브 감정 모드를 지원하며, 초기 선택된 감정을 설정하고 최대 선택 가능 수를 제한할 수 있습니다. */ +/* *********************************************************** +TODO - 해결 +SelectEmotionContainer.tsx:27 Warning: Cannot update a component (`SelectEmotionContainer`) while rendering a different component (`EmotionList`). To locate the bad setState() call inside `EmotionList`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render + at EmotionList + ********************************************************** */ + const EmotionList: React.FC = ({ isPrimary = true, maxSelections, diff --git a/src/shared/model/moodTypes.ts b/src/shared/model/moodTypes.ts index 3bfab20..ba687bb 100644 --- a/src/shared/model/moodTypes.ts +++ b/src/shared/model/moodTypes.ts @@ -1,4 +1,10 @@ -type EmotionType = '좋음' | '나쁨' | '보통' | '매우 좋음' | '매우 나쁨' | null; +export type EmotionType = + | '좋음' + | '나쁨' + | '보통' + | '매우 좋음' + | '매우 나쁨' + | null; export interface DailyEmotionType { day: 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun'; diff --git a/src/widgets/select-emotion/model/type.ts b/src/widgets/select-emotion/model/type.ts index edd07bb..f5a5877 100644 --- a/src/widgets/select-emotion/model/type.ts +++ b/src/widgets/select-emotion/model/type.ts @@ -1,3 +1,19 @@ +import { MoodDataType } from '@/pages/DiaryWritePage/model/type'; +import { EmotionType } from '@/shared/model/moodTypes'; + export interface SelectEmotionContainerProps { - onEmotionSelect: () => void; + onMoodSelect: (mood: MoodDataType) => void; + onNext?: (mood: MoodDataType) => void; } + +export interface MoodState { + mood: EmotionType; + emotion: string | null; + subEmotion: (string | null)[]; +} + +export const INITIAL_MOOD_STATE: MoodState = { + mood: null, + emotion: null, + subEmotion: Array(4).fill(null) +}; diff --git a/src/widgets/select-emotion/ui/SelectEmotionContainer.styled.tsx b/src/widgets/select-emotion/ui/SelectEmotionContainer.styled.tsx index e33c9ec..99613a6 100644 --- a/src/widgets/select-emotion/ui/SelectEmotionContainer.styled.tsx +++ b/src/widgets/select-emotion/ui/SelectEmotionContainer.styled.tsx @@ -2,10 +2,16 @@ import styled from 'styled-components'; export const Container = styled.div` display: flex; + flex-direction: column; + align-items: center; width: 100%; + gap: 2rem; +`; - // 임시 스타일 - height: 200px; - border-radius: 10px; - background-color: #eeeeee; +export const ButtonContainer = styled.div` + width: 100%; + display: flex; + gap: 0.5rem; + justify-content: center; + flex-wrap: wrap; `; diff --git a/src/widgets/select-emotion/ui/SelectEmotionContainer.tsx b/src/widgets/select-emotion/ui/SelectEmotionContainer.tsx index 7802a2c..ef6da0d 100644 --- a/src/widgets/select-emotion/ui/SelectEmotionContainer.tsx +++ b/src/widgets/select-emotion/ui/SelectEmotionContainer.tsx @@ -1,8 +1,99 @@ -import { SelectEmotionContainerProps } from '../model/type'; -import { Container } from './SelectEmotionContainer.styled'; +import { ConditionButtonGroup } from '@/features/diary-write'; +import { + INITIAL_MOOD_STATE, + MoodState, + SelectEmotionContainerProps +} from '../model/type'; +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 Button from '@/shared/ui/Button/Button'; +import { Emotions, getEmotionInfo } from '@/shared/model/EmotionEnum'; export const SelectEmotionContainer = ({ - onEmotionSelect + onMoodSelect, + onNext }: SelectEmotionContainerProps) => { - return 감정 선택 위젯; + const [moodState, setMoodState] = useState(INITIAL_MOOD_STATE); + + const isNextButtonActive = (): boolean => { + return !!( + moodState.mood && + (moodState.emotion || + moodState.subEmotion.some((emotion) => emotion !== null)) + ); + }; + + const handleConditionChange = (condition: EmotionType) => { + setMoodState((prev) => ({ + ...prev, + mood: condition + })); + }; + + const handleEmotionChange = (emotion: (Emotions | null)[]) => { + const korEmotion = emotion.map((item) => { + if (!item) { + return null; + } + try { + const info = getEmotionInfo(item); + return info.description; + } catch (error) { + console.error('감정 변환 에러:', error); + return null; + } + }); + setMoodState((prev) => ({ + ...prev, + emotion: korEmotion[0], // 첫 번째는 메인 감정 + subEmotions: korEmotion.slice(1) // 나머지는 서브 감정 + })); + }; + + const handleButtonClick = () => { + if (isNextButtonActive()) { + onMoodSelect(moodState); + onNext?.(moodState); + } + }; + + return ( + + + + + + + + + ); }; diff --git a/src/widgets/select-music/model/type.ts b/src/widgets/select-music/model/type.ts index a776abd..eb681f5 100644 --- a/src/widgets/select-music/model/type.ts +++ b/src/widgets/select-music/model/type.ts @@ -1,3 +1,6 @@ +import { MusicItem } from '@/entities/music/model/type'; + export interface SelectMusicContainerProps { + onMusicSelect: (music: MusicItem | null) => void; gptRecommendMusicList: string[]; } diff --git a/src/widgets/select-music/ui/SelectMusicContainer.tsx b/src/widgets/select-music/ui/SelectMusicContainer.tsx index 7457b02..c3a9c12 100644 --- a/src/widgets/select-music/ui/SelectMusicContainer.tsx +++ b/src/widgets/select-music/ui/SelectMusicContainer.tsx @@ -11,9 +11,11 @@ import { } from '@/features/diary-write/search-mode-selector/model/type'; import { useMusicSearch } from '@/entities/music'; import { SelectMusicContainerProps } from '../model/type'; +import { MusicItem } from '@/entities/music/model/type'; // TODO - 로딩스피너 추가 export const SelectMusicContainer = ({ + onMusicSelect, gptRecommendMusicList }: SelectMusicContainerProps) => { const [selectedType, setSelectedType] = useState( @@ -34,6 +36,13 @@ export const SelectMusicContainer = ({ return gptMusic && gptMusic.data ? [gptMusic.data] : []; }, [gptMusic]); + const [selectedMusic, setSelectedMusic] = useState(null); + + const handleMusicSelect = (music: MusicItem | null) => { + setSelectedMusic(music); + onMusicSelect?.(music); // 부모로 선택된 음악 전달 + }; + /** * 리스트 타입 세팅 * @param type @@ -62,11 +71,13 @@ export const SelectMusicContainer = ({ )} );