diff --git a/src/apis/category.ts b/src/apis/category.ts index be348b5..62479aa 100644 --- a/src/apis/category.ts +++ b/src/apis/category.ts @@ -8,6 +8,12 @@ export const getCategories = async () => { return response.data; }; +// 카테고리 별 태그 가져오는 API +export const getCategoryTags = async (categoryId: string) => { + const response = await axiosInstance.get(`/category/${categoryId}/`); + return response.data; +}; + // 카테고리 이동1 API export const putSubToOtherTop = async ( categoryId: number, diff --git a/src/apis/videos.ts b/src/apis/videos.ts index e6e81d7..83393b9 100644 --- a/src/apis/videos.ts +++ b/src/apis/videos.ts @@ -29,9 +29,9 @@ export const getRecentVideos = async (): Promise< }; export const getVideoById = async ( - videoId: number, + videoId: string, ): Promise>> => { - const response = await axiosInstance.get(`/videos/${videoId}`); + const response = await axiosInstance.get(`/videos/${videoId}/get`); return response.data; }; diff --git a/src/components/Home/RecentVideos.tsx b/src/components/Home/RecentVideos.tsx index 0e910b3..52a3967 100644 --- a/src/components/Home/RecentVideos.tsx +++ b/src/components/Home/RecentVideos.tsx @@ -19,31 +19,29 @@ const RecentVideos = ({ videos }: IRecentVideosProp) => { return (
-
- 최근 읽은 영상 - {videos.length >= 4 && ( - -
- -
- - )} +
+ 최근 읽은 영상 + {videos.length >= 4 && ( + +
+ +
+ + )}
- + {videos.length === 0 && ( - <> +
비어있는 비디오 이미지
-
- - 처음 방문하셨나요?
아직 정리해본 영상이 없어요! -
- -

영상 정리해보기

-
-
- + + 처음 방문하셨나요?
아직 정리해본 영상이 없어요! +
+ +

영상 정리해보기

+
+
)} {videos.length > 0 && ( diff --git a/src/components/category/Card.tsx b/src/components/category/Card.tsx index f4302f0..4b38c5a 100644 --- a/src/components/category/Card.tsx +++ b/src/components/category/Card.tsx @@ -7,6 +7,7 @@ import { CategorySelectBox } from '@/components/SummaryPage/SummaryDetailBox/Cat import { categoryState } from '@/stores/category'; import * as CardStyles from '@/styles/category/Card.style'; +import Chip from '../common/chip/Chip'; interface ICardProps { mode: 'default' | 'category' | 'recommend'; @@ -65,7 +66,7 @@ const Card: React.FC = ({ {video.description} {video.tag.map((tag) => ( - {`# ${tag.name}`} + ))} diff --git a/src/components/category/DefaultMenu.tsx b/src/components/category/DefaultMenu.tsx new file mode 100644 index 0000000..eb355d2 --- /dev/null +++ b/src/components/category/DefaultMenu.tsx @@ -0,0 +1,68 @@ +import * as CategoryPageStyles from '@/styles/category/index.style'; +import { ISubFolderProps, ITagProps } from 'types/category'; +import Chip from '../common/chip/Chip'; +import ChangeBottomSvg from '@/assets/icons/change-bottom.svg?react'; +import ChangeTopSvg from '@/assets/icons/change-top.svg?react'; + +interface IDefaultMenuProps { + menus: ISubFolderProps[] | ITagProps[]; + recentRegisterMode: boolean; + selectedTags: string[]; + setSelectedTags: React.Dispatch>; + toggleRecentRegisterMode: () => void; +} + +const DefaultMenu = ({ + menus, + recentRegisterMode, + selectedTags, + setSelectedTags, + toggleRecentRegisterMode, +}: IDefaultMenuProps) => { + const onSelectTag = (name: string) => { + if (selectedTags.includes(name)) { + setSelectedTags(selectedTags.filter((tag) => tag !== name)); + } else { + setSelectedTags([...selectedTags, name]); + } + }; + return ( + <> +
+ {menus.map((menu: ISubFolderProps | ITagProps) => ( +
+ {'tag_id' in menu && ( + + )} + {!('tag_id' in menu) && ( + + {menu.name} + + )} +
+ ))} +
+ + + {recentRegisterMode ? '최근등록순' : '최근영상순'} + + {recentRegisterMode ? ( + + ) : ( + + )} + + + ); +}; + +export default DefaultMenu; diff --git a/src/components/category/VideoSelectMenu.tsx b/src/components/category/VideoSelectMenu.tsx new file mode 100644 index 0000000..3685bb9 --- /dev/null +++ b/src/components/category/VideoSelectMenu.tsx @@ -0,0 +1,72 @@ +import * as CategoryPageStyles from '@/styles/category/index.style'; +import GarbageSvg from '@/assets/icons/garbage.svg?react'; +import CloseSvg from '@/assets/icons/close.svg?react'; +import { useState } from 'react'; +import { CategorySelectBox } from '../SummaryPage/SummaryDetailBox/CategorySelectBox'; +import { IFolderProps } from 'types/category'; + +interface IVideoSelectMenuProps { + categories: IFolderProps[]; + totalVideoCount: number; + checkedVideos: number[]; + setCheckedVideos: React.Dispatch>; + handleDeleteVideos: () => void; + allCheckBtnHandler: () => void; +} + +const VideoSelectMenu = ({ + categories, + totalVideoCount, + checkedVideos, + setCheckedVideos, + handleDeleteVideos, + allCheckBtnHandler, +}: IVideoSelectMenuProps) => { + const [selectedCategoryId, setSelectedCategoryId] = useState( + categories.length ? categories[0].categoryId : -1, + ); + + const handleSelectCategory = (categoryId: number) => { + setSelectedCategoryId(categoryId); + }; + + const onFileClick = (e: React.MouseEvent) => { + e.stopPropagation(); + // 비디오 이동 API 호출 후 모든 비디오 받아오는 API 재호출로 최신화하기 + }; + return ( + +
+ + {checkedVideos.length === totalVideoCount ? '모두 삭제' : '모두 선택'} + + + {checkedVideos.length}개 선택 + +
+ + + + + + + + + { + setCheckedVideos([]); + }} + /> + + +
+ ); +}; + +export default VideoSelectMenu; diff --git a/src/components/common/chip/Chip.style.ts b/src/components/common/chip/Chip.style.ts new file mode 100644 index 0000000..891e912 --- /dev/null +++ b/src/components/common/chip/Chip.style.ts @@ -0,0 +1,23 @@ +import theme from '@/styles/theme'; +import styled from 'styled-components'; + +export const ChipContainer = styled.div` + cursor: pointer; + margin-right: 18px; + margin-bottom: 18px; + padding: 3px 9.5px; + background-color: ${theme.color.gray100}; + border-radius: 8px; + color: ${theme.color.gray400}; + ${theme.typography.Caption1}; + + &.light { + border: 1px solid ${theme.color.gray200}; + background-color: ${theme.color.white}; + } + + &.selected { + border-color: ${theme.color.gray300}; + background-color: ${theme.color.gray100}; + } +`; diff --git a/src/components/common/chip/Chip.tsx b/src/components/common/chip/Chip.tsx new file mode 100644 index 0000000..02a224c --- /dev/null +++ b/src/components/common/chip/Chip.tsx @@ -0,0 +1,19 @@ +import { ChipContainer } from './Chip.style'; + +interface IChipProps { + name: string; + light?: boolean; + selected?: boolean; + onSelectTag?: (name: string) => void; +} + +const Chip = ({ name, light, selected, onSelectTag }: IChipProps) => { + return ( + onSelectTag && onSelectTag(name)} + >{`# ${name}`} + ); +}; + +export default Chip; diff --git a/src/hooks/useMoveCategory.ts b/src/hooks/useMoveCategory.ts index 3be900b..e1d61f2 100644 --- a/src/hooks/useMoveCategory.ts +++ b/src/hooks/useMoveCategory.ts @@ -25,7 +25,6 @@ const useMoveCategory = () => { grabedCategory.current!.categoryId, topId, ); - console.log(res); if (res.isSuccess) { await updateCategories(); navigate(`/category/${grabedCategory.current?.topCategoryId}`); diff --git a/src/pages/CategoryPage.tsx b/src/pages/CategoryPage.tsx index bbbfcc9..ac9eb6b 100644 --- a/src/pages/CategoryPage.tsx +++ b/src/pages/CategoryPage.tsx @@ -1,29 +1,28 @@ import CategoryTitle from '@/components/category/CategoryTitle'; import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import ChangeBottomSvg from '@/assets/icons/change-bottom.svg?react'; -import ChangeTopSvg from '@/assets/icons/change-top.svg?react'; -import GarbageSvg from '@/assets/icons/garbage.svg?react'; -import FolderSvg from '@/assets/icons/open-file.svg?react'; -import CloseSvg from '@/assets/icons/close.svg?react'; import * as CategoryPageStyles from '@/styles/category/index.style'; import Card from '@/components/category/Card'; import { useRecoilValue } from 'recoil'; import { categoryState } from '@/stores/category'; -import { ISubFolderProps } from 'types/category'; +import { ISubFolderProps, ITagProps } from 'types/category'; import EmptyCard from '@/components/category/EmptyCard'; -import { deleteVideos, getRecentVideos, getVideoById } from '@/apis/videos'; +import { deleteVideos, getRecentVideos } from '@/apis/videos'; import { IVideoProps } from 'types/videos'; import { sortVideos } from '@/utils/sortVideos'; import { CardContainer } from '@/styles/category/Card.style'; +import handleVideo from '@/utils/handleVideo'; +import VideoSelectMenu from '@/components/category/VideoSelectMenu'; +import DefaultMenu from '@/components/category/DefaultMenu'; const CategoryPage = () => { const params = useParams(); const [name, setName] = useState(''); - const [menus, setMenus] = useState([]); + const [menus, setMenus] = useState([]); const [videos, setVideos] = useState([]); const [recentRegisterMode, setRecentRegisterMode] = useState(false); const [checkedVideos, setCheckedVideos] = useState([]); + const [selectedTags, setSelectedTags] = useState([]); const categories = useRecoilValue(categoryState); const toggleRecentRegisterMode = () => @@ -37,19 +36,21 @@ const CategoryPage = () => { .then((res) => { setVideos(res.result.videos); setName('최근 읽은 영상'); + setMenus([]); }) .catch((err) => console.log(err)); } else { - getVideoById(Number(params.top_folder)).then((res) => { - const index = categories.findIndex( - (category) => category.categoryId === Number(params.top_folder), - ); - setVideos(res.result.videos); - setName(categories[index].name); - setMenus(categories[index].subFolders); - }); + handleVideo( + categories, + params.top_folder, + params.sub_folder!, + setMenus, + setName, + setVideos, + ); } - }, [categories, params.top_folder]); + setCheckedVideos([]); + }, [categories, params.sub_folder, params.top_folder]); const handleDeleteVideos = async () => { const res = await deleteVideos(checkedVideos); @@ -68,83 +69,31 @@ const CategoryPage = () => { if (checkedVideos.length === videos.length) { handleDeleteVideos(); } else { - console.log('모두 선택'); setCheckedVideos(videos.map((video) => video.video_id)); } }; - const dirMoveHanlder = () => { - console.log(checkedVideos); - }; - return ( {checkedVideos.length > 0 ? ( - <> -
- - {checkedVideos.length === sortedVideos.length - ? '모두 삭제' - : '모두 선택'} - - - {checkedVideos.length}개 선택 - -
- - - {menus.map((menu) => ( - - ))} - - - - - - - - - { - setCheckedVideos([]); - }} - /> - - - + ) : ( - <> -
- {menus.map((menu) => ( - - {menu.name} - - ))} -
- - - {recentRegisterMode ? '최근등록순' : '최근영상순'} - - {recentRegisterMode ? ( - - ) : ( - - )} - - + )}
@@ -153,15 +102,29 @@ const CategoryPage = () => { )} {sortedVideos.length > 0 && ( - {sortedVideos.map((video) => ( - - ))} + {sortedVideos.map((video) => { + // 하위 카테고리에 있을 때 태그 선택된 것에 따라 비디오 보여지게하는 로직 + const matchedTagCount = video.tag.reduce((acc, cur) => { + if (selectedTags.includes(cur.name)) return (acc += 1); + return acc; + }, 0); + if ( + params.sub_folder && + selectedTags.length && + matchedTagCount !== selectedTags.length + ) + return; + + return ( + + ); + })} )}
diff --git a/src/styles/HomepageStyle.ts b/src/styles/HomepageStyle.ts index f3bd6fc..a152ce2 100644 --- a/src/styles/HomepageStyle.ts +++ b/src/styles/HomepageStyle.ts @@ -142,10 +142,11 @@ export const RecentVideosContainer = styled.div` width: 910px; } - .empty-video{ + .empty-container { display: flex; - justify-content: center; - align-content: center; + flex-direction: column; + align-items: center; + text-align: center; } .empty-video img { @@ -192,7 +193,6 @@ export const VideosTitle = styled.h2` width: 910px; height: 45px; font-weight: bold; - `; export const VideosSubtitle = styled.h4` diff --git a/src/styles/category/Card.style.ts b/src/styles/category/Card.style.ts index 33760df..32a6263 100644 --- a/src/styles/category/Card.style.ts +++ b/src/styles/category/Card.style.ts @@ -143,13 +143,3 @@ export const DropdownWrap = styled.div` } } `; - -export const Chip = styled.div` - margin-right: 18px; - margin-bottom: 18px; - padding: 3px 9.5px; - background-color: ${theme.color.gray100}; - border-radius: 8px; - color: ${theme.color.gray400}; - ${theme.typography.Caption1}; -`; diff --git a/src/styles/category/index.style.ts b/src/styles/category/index.style.ts index f03b5b4..399d20f 100644 --- a/src/styles/category/index.style.ts +++ b/src/styles/category/index.style.ts @@ -2,6 +2,17 @@ import styled from 'styled-components'; import theme from '../theme'; import { Link } from 'react-router-dom'; +const CommonIconBackground = styled.div` + cursor: pointer; + width: 40px; + height: 40px; + border-radius: 8px; + + display: flex; + align-items: center; + justify-content: center; +`; + export const Container = styled.div` padding: 60px 60px 0px 120px; width: 100%; @@ -9,6 +20,7 @@ export const Container = styled.div` export const MenuWrap = styled.div` display: flex; + min-width: 'fit-content'; justify-content: space-between; align-items: center; margin-bottom: 40px; @@ -20,7 +32,7 @@ export const Menu = styled(Link)` color: ${theme.color.gray300}; margin-right: 20px; - &.activated { + &:hover { color: ${theme.color.gray500}; ${theme.typography.Subheader1}; } @@ -40,6 +52,13 @@ export const Mode = styled.span` color: ${theme.color.gray400}; `; +export const SelectModeWrap = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + align-items: center; +`; + export const CardManagement = styled.div` display: flex; flex-direction: row; @@ -55,29 +74,16 @@ export const SelectManagement = styled.select` color: ${theme.color.gray400}; `; -export const ManagementBoxGray = styled.div` - width: 36px; - height: 34px; - border-radius: 8px; - - display: flex; - align-items: center; - justify-content: center; +export const ManagementBoxGray = styled(CommonIconBackground)` background: ${theme.color.gray100}; `; -export const ManagementBox = styled.div` - width: 36px; - height: 34px; - border-radius: 8px; - - display: flex; - align-items: center; - justify-content: center; +export const ManagementBox = styled(CommonIconBackground)` background: ${theme.color.white}; `; export const AllSelectBtn = styled.button` + cursor: pointer; width: 90px; height: 28px; background: ${theme.color.gray500}; @@ -94,3 +100,31 @@ export const SelectedCount = styled.span` padding: 0px 10px; color: ${theme.color.gray400}; `; + +export const DropdownWrap = styled.div` + margin: 0; + display: flex; + flex-direction: column; + z-index: 10; + & div.select-box { + padding: 8px 16px; + width: 202px; + display: flex; + align-items: center; + justify-content: space-between; + height: 40px; + border-radius: 8px; + border: solid 1px ${theme.color.gray200}; + color: ${theme.color.gray400}; + ${theme.typography.Body3}; + cursor: pointer; + } + & span.icon-button { + padding: 5px 6px; + width: 40px; + height: 40px; + border-radius: 8px; + cursor: pointer; + background-color: ${theme.color.gray100}; + } +`; diff --git a/src/utils/handleVideo.ts b/src/utils/handleVideo.ts new file mode 100644 index 0000000..f6301a3 --- /dev/null +++ b/src/utils/handleVideo.ts @@ -0,0 +1,36 @@ +import { getCategoryTags } from '@/apis/category'; +import { getVideoById } from '@/apis/videos'; +import { IFolderProps, ISubFolderProps, ITagProps } from 'types/category'; +import { IVideoProps } from 'types/videos'; + +const handleVideo = async ( + categories: IFolderProps[], + topCategoryId: string, + subCategoryId: string, + setMenus: React.Dispatch< + React.SetStateAction + >, + setName: React.Dispatch>, + setVideos: React.Dispatch>, +) => { + await getVideoById(topCategoryId).then(async (res) => { + const topCategory = categories.find( + (category) => category.categoryId === Number(topCategoryId), + ); + if (subCategoryId) { + const subName = topCategory?.subFolders.find( + (subFolder) => subFolder.categoryId === Number(subCategoryId), + ); + setName(subName!.name); + await getCategoryTags(subCategoryId!).then((res) => { + if (res.isSuccess) setMenus(res.result.tags); + else setMenus([]); + }); + } else { + setName(topCategory!.name); + setMenus(topCategory!.subFolders); + } + setVideos(res.isSuccess ? res.result.videos : []); + }); +}; +export default handleVideo; diff --git a/src/utils/sortVideos.ts b/src/utils/sortVideos.ts index c711277..62af05b 100644 --- a/src/utils/sortVideos.ts +++ b/src/utils/sortVideos.ts @@ -9,13 +9,13 @@ export const sortVideos = ( prevVideo[isRecentRegisterMode ? 'created_at' : 'youtube_created_at'] > nextVideo[isRecentRegisterMode ? 'created_at' : 'youtube_created_at'] ) - return 1; + return -1; if ( prevVideo[isRecentRegisterMode ? 'created_at' : 'youtube_created_at'] === nextVideo[isRecentRegisterMode ? 'created_at' : 'youtube_created_at'] ) return 0; - return -1; + return 1; }); return sortedVideos; }; diff --git a/types/category.ts b/types/category.ts index 760296a..ac6a12b 100644 --- a/types/category.ts +++ b/types/category.ts @@ -15,3 +15,8 @@ export interface ISelectedCategoryProps { name: string; categoryId: number; } + +export interface ITagProps { + tag_id: number; + name: string; +}