diff --git a/src/api/music.ts b/src/api/music.ts index 04b83afb..a05d7aa3 100644 --- a/src/api/music.ts +++ b/src/api/music.ts @@ -43,3 +43,33 @@ export const deleteMusic = async (role: string, musicId: number) => { return false; } }; + +export const likeMusic = async ({ + role, + musicId, +}: { + role: string; + musicId: number; +}) => { + const response = await apiClient.patch( + SongController.likeMusic(role, musicId), + ); + return response.data; +}; + +export const getLikeMusic = async ({ + role, + date, +}: { + role: string; + date: string; +}) => { + try { + const { data } = await apiClient.get(SongController.getlikeMusic(role), { + params: { + date: date, + }, + }); + return data; + } catch (e: any) {} +}; diff --git a/src/assets/svg/ArrowDown.tsx b/src/assets/svg/ArrowDown.tsx new file mode 100644 index 00000000..a1455b23 --- /dev/null +++ b/src/assets/svg/ArrowDown.tsx @@ -0,0 +1,21 @@ +const ArrowDown = () => { + return ( + + + + ); +}; + +export default ArrowDown; diff --git a/src/assets/svg/HeartIcon.tsx b/src/assets/svg/HeartIcon.tsx new file mode 100644 index 00000000..7b30353e --- /dev/null +++ b/src/assets/svg/HeartIcon.tsx @@ -0,0 +1,31 @@ +import { Palette } from 'styles/globals'; + +interface HeartIconProps { + heartState: boolean; +} + +const HeartIcon = ({ heartState }: HeartIconProps) => { + const fillColor = heartState ? `${Palette.PRIMARY_P10}` : 'none'; + const strokeColor = heartState ? `${Palette.PRIMARY_P10}` : '#656B80'; + + return ( + + + + ); +}; + +export default HeartIcon; diff --git a/src/assets/svg/NewPageIcon.tsx b/src/assets/svg/NewPageIcon.tsx deleted file mode 100644 index e7b48022..00000000 --- a/src/assets/svg/NewPageIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -const NewPageIcon = () => { - return ( - - - - - - ); -}; - -export default NewPageIcon; diff --git a/src/assets/svg/index.ts b/src/assets/svg/index.ts index a475670f..6f934538 100644 --- a/src/assets/svg/index.ts +++ b/src/assets/svg/index.ts @@ -47,8 +47,9 @@ export { default as EyeIcon } from './EyeIcon'; export { default as EyeSelectedIcon } from './EyeSelectedIcon'; export { default as MusicalNoteIcon } from './MusicNoteIcon'; export { default as EllipsisVerticalIcon } from './EllipsisVerticalIcon'; -export { default as NewPageIcon } from './NewPageIcon'; export { default as TrashcanIcon } from './TrashcanIcon'; export { default as SettingIcon } from './SettingIcon'; export { default as CircleDefaultProfile } from './CircleDefaultProfile'; export { default as BookBenIcon } from './BookBenIcon'; +export { default as HeartIcon } from './HeartIcon'; +export { default as ArrowDown } from './ArrowDown'; diff --git a/src/components/Song/atoms/MusicItemThumbnail/index.tsx b/src/components/Song/atoms/MusicItemThumbnail/index.tsx index 35b7106e..047c3270 100644 --- a/src/components/Song/atoms/MusicItemThumbnail/index.tsx +++ b/src/components/Song/atoms/MusicItemThumbnail/index.tsx @@ -1,11 +1,52 @@ import Image from 'next/image'; +import { preventEvent } from 'utils/Libs/preventEvent'; +import { likeMusic } from 'api/music'; +import { HeartIcon } from 'assets/svg'; +import { toast } from 'react-toastify'; import * as S from './style'; -const MusicItemThumbnail = ({ thumbnail }: { thumbnail: string }) => { +interface MusicItemThumbnailProps { + thumbnail: string; + heartState: boolean; + setHeartState: React.Dispatch>; + musicId: number; + role: string; + setLikeCount: React.Dispatch>; +} + +const MusicItemThumbnail = ({ + thumbnail, + heartState, + setHeartState, + musicId, + role, + setLikeCount, +}: MusicItemThumbnailProps) => { + const handleHeart = async (e: React.MouseEvent) => { + preventEvent(e); + try { + const data = await likeMusic({ role, musicId }); + setLikeCount(data.likeCount); + setHeartState(!heartState); + } catch (error) { + console.error('Error liking music:', error); + toast.error('음악을 찾지 못했습니다'); + } + }; return ( - - thumbnail - +
+ + + + + thumbnail + +
); }; diff --git a/src/components/Song/atoms/MusicItemThumbnail/style.ts b/src/components/Song/atoms/MusicItemThumbnail/style.ts index 4c369fa4..62081549 100644 --- a/src/components/Song/atoms/MusicItemThumbnail/style.ts +++ b/src/components/Song/atoms/MusicItemThumbnail/style.ts @@ -1,5 +1,5 @@ -import styled from "@emotion/styled"; -import { Palette } from "styles/globals"; +import styled from '@emotion/styled'; +import { Palette } from 'styles/globals'; export const ImgBox = styled.div` position: relative; @@ -17,4 +17,15 @@ export const ImgBox = styled.div` span { min-height: 72px; } -`; \ No newline at end of file +`; + +export const HeartContainer = styled.div` + position: absolute; + top: 4px; + left: 4px; + z-index: 1; + + @media (min-width: 420px) { + display: none; + } +`; diff --git a/src/components/Song/atoms/MusicItemTitle/style.ts b/src/components/Song/atoms/MusicItemTitle/style.ts index f4ef229f..96aef12d 100644 --- a/src/components/Song/atoms/MusicItemTitle/style.ts +++ b/src/components/Song/atoms/MusicItemTitle/style.ts @@ -24,7 +24,7 @@ export const Title = styled.h4` export const Info = styled.div` display: none; - font-size: 1em; + font-size: 0.75em; color: ${Palette.NEUTRAL_N20}; @media (max-width: 800px) { diff --git a/src/components/Song/atoms/MusicListButton/index.tsx b/src/components/Song/atoms/MusicListButton/index.tsx index 1c1f0392..13fe9fcf 100644 --- a/src/components/Song/atoms/MusicListButton/index.tsx +++ b/src/components/Song/atoms/MusicListButton/index.tsx @@ -1,5 +1,7 @@ -import { NewPageIcon, TrashcanIcon } from 'assets/svg'; import { preventEvent } from 'utils/Libs/preventEvent'; +import { HeartIcon, TrashcanIcon } from 'assets/svg'; +import { likeMusic } from 'api/music'; +import { toast } from 'react-toastify'; import * as S from './style'; interface MusicListButtonProps { @@ -7,6 +9,11 @@ interface MusicListButtonProps { songStuNum: number; userStuNum?: string; setDeleteModal: React.Dispatch>; + heartState: boolean; + setHeartState: React.Dispatch>; + musicId: number; + likeCount: number; + setLikeCount: React.Dispatch>; } const MusicListButton = ({ @@ -14,22 +21,43 @@ const MusicListButton = ({ songStuNum, userStuNum, setDeleteModal, + heartState, + setHeartState, + musicId, + likeCount, + setLikeCount, }: MusicListButtonProps) => { + const handleHeart = async (e: React.MouseEvent) => { + preventEvent(e); + try { + const data = await likeMusic({ role, musicId }); + setLikeCount(data.likeCount); + setHeartState(!heartState); + } catch (error) { + console.error('Error liking music:', error); + toast.error('음악을 찾지 못했습니다'); + } + }; return ( - {(role !== 'member' || String(songStuNum) === userStuNum) && ( - - )} -
- -
+ + {(role !== 'member' || String(songStuNum) === userStuNum) && ( + { + preventEvent(e); + setDeleteModal(true); + }} + > + + + )} + + + + + {likeCount} + +
); }; diff --git a/src/components/Song/atoms/MusicListButton/style.ts b/src/components/Song/atoms/MusicListButton/style.ts index 594838f2..b2b06c53 100644 --- a/src/components/Song/atoms/MusicListButton/style.ts +++ b/src/components/Song/atoms/MusicListButton/style.ts @@ -1,34 +1,51 @@ -import styled from "@emotion/styled"; -import { Palette } from "styles/globals"; +import styled from '@emotion/styled'; +import { Palette } from 'styles/globals'; + +export const ButtonStyle = styled.div` + border: none; + border-radius: 0.5em; + background: ${Palette.BACKGROUND_BG}; + width: 40px; + height: 40px; + display: flex; + justify-content: center; + align-items: center; +`; export const ButtonContainer = styled.div` + display: flex; + height: 100%; + align-items: flex-end; +`; + +export const ButtonTestContainer = styled.div` display: flex; gap: 1em; justify-content: end; button, div { - border: none; - border-radius: 0.5em; - background: ${Palette.BACKGROUND_BG}; - width: 40px; - height: 40px; - display: flex; - justify-content: center; - align-items: center; - - @media (max-width: 800px) { - :last-child { - display: none; - } - } + ${ButtonStyle} } - svg path { + & > div:first-of-type svg path { stroke: ${Palette.NEUTRAL_N20}; } - @media (max-width: 420px) { display: none; } -`; \ No newline at end of file +`; + +export const LikeContainer = styled.div` + display: flex; + flex-direction: column; +`; + +export const LikeNum = styled.p` + font-family: SUIT; + font-size: 14px; + font-weight: 600; + line-height: 17.47px; + text-align: center; + color: ${Palette.NEUTRAL_N20}; +`; diff --git a/src/components/Song/atoms/MusicListSelect/index.tsx b/src/components/Song/atoms/MusicListSelect/index.tsx new file mode 100644 index 00000000..8f6d10a4 --- /dev/null +++ b/src/components/Song/atoms/MusicListSelect/index.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react'; +import { ArrowDown } from 'assets/svg'; +import { MusicListSelectProps, OptionType } from 'types/components/SongPage'; +import * as S from './style'; + +const MusicListSelect = ({ + options, + selectedOption, + setSelectedOption, +}: MusicListSelectProps) => { + const [isOpen, setIsOpen] = useState(false); + const handleOptionClick = (option: OptionType) => { + setSelectedOption(option); + setIsOpen(false); + }; + + return ( + + setIsOpen(!isOpen)}> + {selectedOption.label} + + + {isOpen && ( + + {options.map((option) => ( + handleOptionClick(option)} + > + {option.label} + + ))} + + )} + + ); +}; + +export default MusicListSelect; diff --git a/src/components/Song/atoms/MusicListSelect/style.ts b/src/components/Song/atoms/MusicListSelect/style.ts new file mode 100644 index 00000000..e250270a --- /dev/null +++ b/src/components/Song/atoms/MusicListSelect/style.ts @@ -0,0 +1,52 @@ +import styled from '@emotion/styled'; +import { Palette } from 'styles/globals'; + +export const SelectContainer = styled.div` + width: 80px; + height: 24px; +`; + +export const SelectBox = styled.div` + cursor: pointer; + font-family: SUIT; + font-size: 1rem; + font-weight: 500; + line-height: 1.5rem; + color: ${Palette.NEUTRAL_N20}; + + display: flex; + align-items: center; + gap: 0.5rem; +`; + +export const OptionsContainer = styled.div` + position: absolute; + right: 0px; + top: 2.25rem; + z-index: 3; + width: 240px; + height: 112px; + padding: 0.5rem 0px; + gap: 0.5rem; + border-radius: 1rem; + background-color: ${Palette.BACKGROUND_CARD}; + box-shadow: 0 2px 1.5rem rgba(0, 0, 0, 0.12); + + @media (max-width: 800px) { + width: 40%; + } +`; + +export const Option = styled.div` + width: 100%; + height: 48px; + padding: 0.5rem 1.25rem; + gap: 0.5rem; + display: flex; + align-items: center; + color: ${Palette.NEUTRAL_N10}; + + &:hover { + background-color: ${Palette.NEUTRAL_N50}; + } +`; diff --git a/src/components/Song/molecules/ResponsiveModal/index.tsx b/src/components/Song/molecules/ResponsiveModal/index.tsx index 50323322..b37fb07e 100644 --- a/src/components/Song/molecules/ResponsiveModal/index.tsx +++ b/src/components/Song/molecules/ResponsiveModal/index.tsx @@ -1,9 +1,11 @@ import { ResponseOverayWrapper } from 'components/Common/atoms/Wrappers/ModalOverayWrapper/style'; -import * as S from './style'; -import { NewPageIcon, TrashcanIcon } from 'assets/svg'; +import { HeartIcon, TrashcanIcon } from 'assets/svg'; import { SongResponsiveModalProps } from 'types'; import { preventEvent } from 'utils/Libs/preventEvent'; import { getRole } from 'utils/Libs/getRole'; +import { likeMusic } from 'api/music'; +import { toast } from 'react-toastify'; +import * as S from './style'; const ResponsiveModal = ({ modalState, @@ -11,25 +13,42 @@ const ResponsiveModal = ({ setDelModalState, songData, userData, + heartState, + setHeartState, + musicId, + setLikeCount, }: SongResponsiveModalProps) => { const role = getRole(); + + const handleHeart = async (e: React.MouseEvent) => { + preventEvent(e); + try { + const data = await likeMusic({ role, musicId }); + setLikeCount(data.likeCount); + setHeartState(!heartState); + } catch (error) { + console.error('Error liking music:', error); + toast.error('음악을 찾지 못했습니다'); + } + }; + + const handleDelete = (e: React.MouseEvent) => { + preventEvent(e); + setDelModalState(true); + setModalState(false); + }; + return ( <> -
- - 바로가기 +
+ + 좋아요
{(role !== 'member' || String(songData.stuNum) === userData.stuNum) && ( -
{ - preventEvent(e); - setDelModalState(true); - setModalState(false); - }} - > +
기상음악 삭제
diff --git a/src/components/Song/molecules/ResponsiveModal/style.ts b/src/components/Song/molecules/ResponsiveModal/style.ts index 842bfc6b..ac4fe238 100644 --- a/src/components/Song/molecules/ResponsiveModal/style.ts +++ b/src/components/Song/molecules/ResponsiveModal/style.ts @@ -33,7 +33,4 @@ export const BtnWrapper = styled.div` border-radius: 16px 16px 0 0; } } - svg path { - stroke: ${Palette.NEUTRAL_N20}; - } `; diff --git a/src/components/Song/molecules/SongItem/index.tsx b/src/components/Song/molecules/SongItem/index.tsx index d1c3a873..bc69d0b2 100644 --- a/src/components/Song/molecules/SongItem/index.tsx +++ b/src/components/Song/molecules/SongItem/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import useSWR, { mutate } from 'swr'; import Link from 'next/link'; import { deleteMusic, getMusic } from 'api/music'; @@ -27,6 +27,8 @@ const SongItem = ({ data: songData, selectedDate }: SongItemProps) => { const { data: userData } = useSWR(MemberController.myProfile); const [deleteModal, setDeleteModal] = useState(false); const [modalState, setModalState] = useState(false); + const [heartState, setHeartState] = useState(false); + const [likeCount, setLikeCount] = useState(0); const createdDate = new Date(songData.createdTime); const songDate = `${getDate(createdDate)[3]}시 ${getDate(createdDate)[4]}분`; @@ -34,6 +36,11 @@ const SongItem = ({ data: songData, selectedDate }: SongItemProps) => { getDate(selectedDate)[2] }`; + useEffect(() => { + setHeartState(songData.memberLikeCheck); + setLikeCount(songData.likeCount); + }, [setHeartState, setLikeCount]); + const onDelete = async (id: number) => { const isSuccess = await deleteMusic(role, id); if (isSuccess) @@ -44,7 +51,14 @@ const SongItem = ({ data: songData, selectedDate }: SongItemProps) => { - + { songStuNum={songData.stuNum} userStuNum={userData?.stuNum} setDeleteModal={setDeleteModal} + heartState={heartState} + setHeartState={setHeartState} + musicId={songData.id} + likeCount={likeCount} + setLikeCount={setLikeCount} /> { setDelModalState={setDeleteModal} songData={songData} userData={userData} + heartState={heartState} + setHeartState={setHeartState} + musicId={songData.id} + setLikeCount={setLikeCount} /> )} diff --git a/src/components/Song/organisms/SongList/index.tsx b/src/components/Song/organisms/SongList/index.tsx index a18690f2..f7f65d6f 100644 --- a/src/components/Song/organisms/SongList/index.tsx +++ b/src/components/Song/organisms/SongList/index.tsx @@ -1,12 +1,13 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import useSWR from 'swr'; -import { getMusic } from 'api/music'; -import { getRole } from 'utils/Libs/getRole'; -import { SongController } from 'utils/Libs/requestUrls'; import { getDate } from 'utils/getDate'; import EmptySongBox from 'components/Song/atoms/EmptySongBox'; import SongItem from 'components/Song/molecules/SongItem'; -import { SongListType } from 'types/components/SongPage'; +import MusicListSelect from 'components/Song/atoms/MusicListSelect'; +import { OptionType, SongListType } from 'types/components/SongPage'; +import { fetchMusic } from 'utils/fetchMusic'; +import { getRole } from 'utils/Libs/getRole'; +import { SongController } from 'utils/Libs/requestUrls'; import * as S from './style'; interface SongListProps { @@ -16,22 +17,47 @@ interface SongListProps { const SongList = ({ selectedDate }: SongListProps) => { const role = getRole(); const postDate = `${getDate(selectedDate)[0]}-${getDate(selectedDate)[1]}-${getDate(selectedDate)[2]}`; - const { data, mutate } = useSWR( - SongController.music(role), - () => getMusic(role, postDate), + + const options = useMemo( + () => [ + { value: '좋아요순', label: '좋아요순' }, + { value: '신청순', label: '신청순' }, + ], + [], + ); + + const defaultOption = useMemo( + () => options.find((option) => option.value === '좋아요순') || options[0], + [options], + ); + + const [selectedOption, setSelectedOption] = + useState(defaultOption); + + const swrKey = SongController.music(role); + + const { data, mutate } = useSWR(swrKey, () => + fetchMusic(role, postDate, selectedOption.value), ); useEffect(() => { mutate(); - }, [selectedDate]); + }, [selectedDate, selectedOption, mutate]); return ( -

신청음악

-

- {data?.content?.length ?? 0} 개 -

+ +

신청음악

+

+ {data?.content?.length ?? 0} 개 +

+
+
{data && data.content?.length > 0 ? ( diff --git a/src/components/Song/organisms/SongList/style.ts b/src/components/Song/organisms/SongList/style.ts index 0eabbff2..6fdc03c8 100644 --- a/src/components/Song/organisms/SongList/style.ts +++ b/src/components/Song/organisms/SongList/style.ts @@ -21,11 +21,17 @@ export const Layer = styled.div` export const ListHeader = styled.div` width: 100%; background: ${Palette.BACKGROUND_CARD}; - opacity: 0.9; backdrop-filter: blur(4px); display: flex; gap: 0.5rem; align-items: center; + justify-content: space-between; +`; + +export const MusicDataHeader = styled.div` + gap: 0.5rem; + display: flex; + align-items: center; h3 { color: ${Palette.NEUTRAL_N10}; @@ -58,7 +64,6 @@ export const ListContainer = styled.div` height: 72px; transition: 0.2s; border-radius: 0.5em; - padding: 0.5em 0.5em 0.5em 0; :hover { background: ${Palette.NEUTRAL_N50}; @@ -71,4 +76,4 @@ export const ListContainer = styled.div` & > a:-webkit-any-link { text-decoration: none; } -`; \ No newline at end of file +`; diff --git a/src/components/StuInfo/organisms/StuInfoForm/style.ts b/src/components/StuInfo/organisms/StuInfoForm/style.ts index 55ab5d8e..3c263543 100644 --- a/src/components/StuInfo/organisms/StuInfoForm/style.ts +++ b/src/components/StuInfo/organisms/StuInfoForm/style.ts @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; export const Positioner = styled.div` - width: 30%; + width: fit-content; `; diff --git a/src/components/StuInfo/organisms/StuInfoList/style.ts b/src/components/StuInfo/organisms/StuInfoList/style.ts index 21a82485..96210213 100644 --- a/src/components/StuInfo/organisms/StuInfoList/style.ts +++ b/src/components/StuInfo/organisms/StuInfoList/style.ts @@ -2,12 +2,12 @@ import styled from '@emotion/styled'; import { Palette } from 'styles/globals'; export const Layer = styled.div` - width: 70%; height: 100%; background: ${Palette.BACKGROUND_CARD}; border-radius: 1rem; padding: 1.5rem 1rem; display: flex; + flex: 1; flex-direction: column; gap: 0.25em; overflow-y: auto; diff --git a/src/pages/song.tsx b/src/pages/song.tsx index 7784e973..12edd4cc 100644 --- a/src/pages/song.tsx +++ b/src/pages/song.tsx @@ -44,7 +44,10 @@ const SongPage: NextPage<{ setSelectedDate={setSelectedDate} setNoticeModal={setNoticeModal} /> - + diff --git a/src/types/Modals.ts b/src/types/Modals.ts index 5d9c974d..c3dca698 100644 --- a/src/types/Modals.ts +++ b/src/types/Modals.ts @@ -61,7 +61,7 @@ export interface PenaltyRecordModalProps { handleDelete: ( state: string[], setState: (state: string[]) => void, - select: string + select: string, ) => void; } @@ -69,4 +69,8 @@ export interface SongResponsiveModalProps extends ModalProps { setDelModalState: (state: boolean) => void; songData: SongType; userData: myProfileType; + heartState: boolean; + musicId: number; + setHeartState: React.Dispatch>; + setLikeCount: React.Dispatch>; } diff --git a/src/types/components/SongPage.ts b/src/types/components/SongPage.ts index 4c208040..6c8a0672 100644 --- a/src/types/components/SongPage.ts +++ b/src/types/components/SongPage.ts @@ -7,8 +7,21 @@ export interface SongType { stuNum: number; thumbnail: string; title: string; + likeCount: number; + memberLikeCheck: boolean; } export interface SongListType { content: SongType[]; } + +export interface OptionType { + value: string; + label: string; +} + +export interface MusicListSelectProps { + options: OptionType[]; + selectedOption: OptionType; + setSelectedOption: React.Dispatch>; // string에서 OptionType으로 수정 +} diff --git a/src/utils/Libs/requestUrls.ts b/src/utils/Libs/requestUrls.ts index 146aa802..5ad1948a 100644 --- a/src/utils/Libs/requestUrls.ts +++ b/src/utils/Libs/requestUrls.ts @@ -7,7 +7,7 @@ export const MemberController = { emailPasswordCheck: '/email/password', changePasswd: '/members/password', myProfile: '/home', - profileImage: '/members/profileImage' + profileImage: '/members/profileImage', }; export const NoticeController = { @@ -80,6 +80,12 @@ export const SongController = { deleteMusic(role: string, musicId: number) { return `/${role}/music/${musicId}`; }, + likeMusic(role: string, musicId: number) { + return `/${role}/music/${musicId}/like`; + }, + getlikeMusic(role: string) { + return `/${role}/music/like`; + }, }; export const StuInfoController = { diff --git a/src/utils/fetchMusic.ts b/src/utils/fetchMusic.ts new file mode 100644 index 00000000..62cbcb98 --- /dev/null +++ b/src/utils/fetchMusic.ts @@ -0,0 +1,7 @@ +import { getLikeMusic, getMusic } from 'api/music'; + +export const fetchMusic = (role: string, date: string, option: string) => { + return option === '좋아요순' + ? getLikeMusic({ role, date }) + : getMusic(role, date); +};