diff --git a/src/apis/videos.ts b/src/apis/videos.ts index 9fea94c..6cf2828 100644 --- a/src/apis/videos.ts +++ b/src/apis/videos.ts @@ -2,6 +2,7 @@ import { APIResponse } from '@/models/config/axios'; import { IVideo, VideoVersionType } from '@/models/video'; import axios from './config/instance'; +import axiosInstance from './config/instance'; const PREFIX = '/videos'; @@ -11,3 +12,10 @@ export const getVideoAPI = ( ) => { return axios.get>(PREFIX + `/${videoId}/${versionId}`); }; + +export const deleteVideos = async (videos: number[]) => { + const response = await axiosInstance.delete('/videos/selectDelete', { + data: { videos }, + }); + return response.data; +}; diff --git a/src/components/Home/InsightVideos.tsx b/src/components/Home/InsightVideos.tsx index 6075a38..254e0d1 100644 --- a/src/components/Home/InsightVideos.tsx +++ b/src/components/Home/InsightVideos.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { InsightVideosContainer } from '@/styles/HomepageStyle'; import Card from '../category/Card'; -import { cardDummy } from '../category/Card'; +import { IVideoProps } from '../category/Card'; interface InsightVideosProps { username: string; @@ -13,8 +13,8 @@ const InsightVideos: React.FC = ({ popularHashtags, }) => { const formattedHashtags = popularHashtags.map((tag) => '#' + tag); - const [categoryItems] = useState([]); - const [checkedItems, setCheckedItems] = useState([]); + const [categoryItems] = useState([]); + const [checkedItems, setCheckedItems] = useState([]); return ( diff --git a/src/components/category/Card.tsx b/src/components/category/Card.tsx index 7239bc4..4e97d49 100644 --- a/src/components/category/Card.tsx +++ b/src/components/category/Card.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useState } from 'react'; -import VideoTag from '../common/videoTag'; +import React, { useEffect } from 'react'; import * as CardStyles from '@/styles/category/Card.style'; +import VideoTag from '../common/videoTag'; -export interface cardDummy { +export interface IVideoProps { video_id: number; category_id: number; title: string; @@ -15,9 +15,9 @@ export interface cardDummy { } interface ICardProps { - videos: cardDummy[]; - checkedVideos: boolean[]; - setCheckedVideos: (value: boolean[]) => void; + videos: IVideoProps[]; + checkedVideos: number[]; + setCheckedVideos: (value: number[]) => void; } const Card: React.FC = ({ @@ -25,62 +25,35 @@ const Card: React.FC = ({ checkedVideos, setCheckedVideos, }) => { - const [isShadow, setIsShadow] = useState(new Array(6).fill(false)); + useEffect(() => {}, [checkedVideos]); - useEffect(() => { - if (checkedVideos.includes(true)) { - // 1개 이상 클릭 시 모든 hover event 활성화 - setIsShadow(isShadow.map(() => true)); - } else if (!isShadow.includes(false)) { - //모든 hover 활성화, 모든 체크 비활성화 시 모든 hover 활성화 제거 - setIsShadow(isShadow.map(() => false)); + const handleCheckBox = (videoId: number) => { + if (checkedVideos.includes(videoId)) { + setCheckedVideos(checkedVideos.filter((id) => id !== videoId)); + } else { + setCheckedVideos([...checkedVideos, videoId]); } - }, [checkedVideos]); - - const handleMouseEnter = (id: number) => { - const prev = checkedVideos.includes(true) - ? [...isShadow] - : new Array(isShadow.length).fill(false); - // 체크박스 미선택 이동 시 isshadow 중복 작동으로 인해 방식 변경 - prev[id] = true; - setIsShadow(prev); - }; - - const handleMouseLeave = (id: number) => { - if (!checkedVideos.includes(true)) { - // 선택되면 유지 - const prev = [...isShadow]; - prev[id] = false; - setIsShadow(prev); - } - }; - - const checkBoxHandler = (id: number) => { - const prev = [...checkedVideos]; - prev[id] = !prev[id]; - setCheckedVideos(prev); }; return ( - {videos.map((video, idx) => ( - handleMouseEnter(idx)} - onMouseLeave={() => handleMouseLeave(idx)} - > - - {isShadow[idx] && ( - checkBoxHandler(idx)} - /> - )} - + {videos.map((video) => ( + + + 0 ? 'activated' : ''} + > + handleCheckBox(video.video_id)} + /> + + + + {video.title} diff --git a/src/components/category/EmptyCard.tsx b/src/components/category/EmptyCard.tsx index 1278d61..5febd81 100644 --- a/src/components/category/EmptyCard.tsx +++ b/src/components/category/EmptyCard.tsx @@ -13,7 +13,7 @@ const EmptyCard = () => { 관련 영상들을 모아보세요 - 영상 정리해보기 + 영상 정리해보기 ); }; diff --git a/src/components/layout/header/profile/ProfileDetail.tsx b/src/components/layout/header/profile/ProfileDetail.tsx index 13b5c6f..44a54cd 100644 --- a/src/components/layout/header/profile/ProfileDetail.tsx +++ b/src/components/layout/header/profile/ProfileDetail.tsx @@ -27,6 +27,7 @@ const ProfileDetail = ({ onClose }: Props) => { const handleClickLogoutButton = () => { setUserToken(null); + navigate('/'); onClose(); }; diff --git a/src/components/layout/sideBar/UserMode.tsx b/src/components/layout/sideBar/UserMode.tsx index 9b5fe42..b2ed517 100644 --- a/src/components/layout/sideBar/UserMode.tsx +++ b/src/components/layout/sideBar/UserMode.tsx @@ -47,16 +47,21 @@ const UserMode = () => { }; const putCategoryFolder = async () => { + let response; if (grabedCategory.current?.topCategoryId === -1) { - subToTop(topId, grabedCategory, dropedCategory); + response = await subToTop(grabedCategory); } else if (grabedCategory.current?.topCategoryId === null) { - topToOtherTop(grabedCategory, dropedCategory); + response = await topToOtherTop(grabedCategory, dropedCategory); } else { - subToOtherTop(topId, grabedCategory); + response = await subToOtherTop(topId, grabedCategory); } // 잡은 카테고리, 놓은 카테고리 초기화 - grabedCategory.current = undefined; - dropedCategory.current = undefined; + if (response) { + grabedCategory.current = undefined; + dropedCategory.current = undefined; + } else { + alert('카테고리를 옮기는데 오류가 발생했습니다.'); + } }; return ( <> diff --git a/src/hooks/useMoveCategory.ts b/src/hooks/useMoveCategory.ts index 4902cc5..3be900b 100644 --- a/src/hooks/useMoveCategory.ts +++ b/src/hooks/useMoveCategory.ts @@ -3,23 +3,15 @@ import { putSubToTop, putTopToOtherTop, } from '@/apis/category'; -import { categoryState } from '@/stores/category'; -import handleCategory from '@/utils/handleCategory'; import { useNavigate } from 'react-router-dom'; -import { useRecoilState } from 'recoil'; import { ISubFolderProps } from 'types/category'; +import useUpdateCategories from './useUpdateCategories'; const useMoveCategory = () => { - const [categories, setCategories] = useRecoilState(categoryState); const navigate = useNavigate(); - const { - deleteSubCategory, - deleteTopCategory, - insertCategory, - insertSubToTopCategory, - } = handleCategory(); + const { updateCategories } = useUpdateCategories(); - const subToOtherTop = ( + const subToOtherTop = async ( topId: number, grabedCategory: React.MutableRefObject, ) => { @@ -29,26 +21,20 @@ const useMoveCategory = () => { } // 하위에 있는 폴더를 다른 상위 폴더로 이동하는 기능 // 카테고리 이동1 - const deleteResponse = deleteSubCategory( - categories, + const res = await putSubToOtherTop( + grabedCategory.current!.categoryId, topId, - grabedCategory.current?.categoryId, - ); - const insertResponse = insertCategory( - deleteResponse, - grabedCategory.current?.topCategoryId, - grabedCategory.current!, ); - putSubToOtherTop(grabedCategory.current!.categoryId, topId); - setCategories([...insertResponse]); - navigate(`/category/${grabedCategory.current?.topCategoryId}`); - console.log(grabedCategory.current?.name); + console.log(res); + if (res.isSuccess) { + await updateCategories(); + navigate(`/category/${grabedCategory.current?.topCategoryId}`); + } + return res.isSuccess; }; - const subToTop = ( - topId: number, + const subToTop = async ( grabedCategory: React.MutableRefObject, - dropedCategory: React.MutableRefObject, ) => { if (grabedCategory.current?.name === '기타') { alert(`'기타' 폴더는 이동할 수 없습니다.`); @@ -56,45 +42,28 @@ const useMoveCategory = () => { } // 하위에 있는 폴더를 상위로 올리는 기능 // 카테고리 이동2 - const deleteResponse = deleteSubCategory( - categories, - topId, - grabedCategory.current?.categoryId, - ); - const insertResponse = insertSubToTopCategory( - deleteResponse, - dropedCategory.current, - grabedCategory.current!, - ); - putSubToTop(grabedCategory.current!.categoryId); - setCategories([...insertResponse]); + const res = await putSubToTop(grabedCategory.current!.categoryId); + if (res.isSuccess) { + await updateCategories(); + } + return res.isSuccess; }; - const topToOtherTop = ( + const topToOtherTop = async ( grabedCategory: React.MutableRefObject, dropedCategory: React.MutableRefObject, ) => { // 상위에 있는 폴더를 다른 상위 폴더로 넣는 기능 // 카테고리 이동3 - const deleteResponse = deleteTopCategory( - categories, + const res = await putTopToOtherTop( grabedCategory.current!.categoryId, - ); - const insertResponse = insertCategory( - deleteResponse, dropedCategory.current!, - { - categoryId: grabedCategory.current!.categoryId, - name: grabedCategory.current!.name, - topCategoryId: dropedCategory.current!, - }, ); - putTopToOtherTop( - grabedCategory.current!.categoryId, - dropedCategory.current!, - ); - setCategories(insertResponse); - navigate(`/category/${dropedCategory.current}`); + if (res.isSuccess) { + await updateCategories(); + navigate(`/category/${dropedCategory.current}`); + } + return res.isSuccess; }; return { subToOtherTop, subToTop, topToOtherTop }; diff --git a/src/pages/CategoryPage.tsx b/src/pages/CategoryPage.tsx index eda9aa3..a11a17f 100644 --- a/src/pages/CategoryPage.tsx +++ b/src/pages/CategoryPage.tsx @@ -7,20 +7,21 @@ 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 Card, { IVideoProps } from '@/components/category/Card'; import axiosInstance from '@/apis/config/instance'; import { useRecoilValue } from 'recoil'; import { categoryState } from '@/stores/category'; import { ISubFolderProps } from 'types/category'; import EmptyCard from '@/components/category/EmptyCard'; +import { deleteVideos } from '@/apis/videos'; const CategoryPage = () => { const params = useParams(); const [name, setName] = useState(''); const [menus, setMenus] = useState([]); - const [videos, setVideos] = useState([]); + const [videos, setVideos] = useState([]); const [recentRegisterMode, setRecentRegisterMode] = useState(false); - const [checkedVideos, setCheckedVideos] = useState([]); + const [checkedVideos, setCheckedVideos] = useState([]); const categories = useRecoilValue(categoryState); const toggleRecentRegisterMode = () => @@ -46,42 +47,47 @@ const CategoryPage = () => { } }, [categories, params.top_folder]); - const allCheckBtnHandler = () => { - if (checkedVideos.includes(false)) { - setCheckedVideos(checkedVideos.map(() => true)); + const handleDeleteVideos = async () => { + const res = await deleteVideos(checkedVideos); + if (res.isSuccess) { + const existVideos = videos.filter( + (video) => !checkedVideos.includes(video.video_id), + ); + setVideos(existVideos); + setCheckedVideos([]); } else { - // 모두 삭제 + alert('비디오를 삭제하는데 실패했습니다.'); } }; - const dirMoveHanlder = () => { - checkedVideos.map((value, id) => { - if (value === true) { - console.log('이동해야할 index : ', id); - } - }); + const allCheckBtnHandler = async () => { + if (checkedVideos.length === videos.length) { + handleDeleteVideos(); + } else { + console.log('모두 선택'); + setCheckedVideos(videos.map((video) => video.video_id)); + } }; - const garbageHandler = () => { - checkedVideos.map((value, id) => { - if (value === true) { - console.log('삭제해야할 index : ', id); - } - }); + const dirMoveHanlder = () => { + console.log(checkedVideos); }; + console.log(videos); return ( - {checkedVideos.includes(true) ? ( + {checkedVideos.length > 0 ? ( <>
- {!checkedVideos.includes(false) ? '모두 삭제' : '모두 선택'} + {checkedVideos.length === videos.length + ? '모두 삭제' + : '모두 선택'} - {checkedVideos.filter((bool) => bool === true).length}개 선택 + {checkedVideos.length}개 선택
@@ -93,7 +99,9 @@ const CategoryPage = () => { - + @@ -101,7 +109,7 @@ const CategoryPage = () => { width={28} height={28} onClick={() => { - setCheckedVideos(checkedVideos.map(() => false)); + setCheckedVideos([]); }} /> diff --git a/src/pages/SignInPage.tsx b/src/pages/SignInPage.tsx index 5af90c3..116592f 100644 --- a/src/pages/SignInPage.tsx +++ b/src/pages/SignInPage.tsx @@ -22,6 +22,7 @@ import { LoginRequest } from '@/models/user'; import { userTokenState } from '@/stores/user'; import { BlurBackground } from '@/styles/modals/common.style'; +import useUpdateCategories from '@/hooks/useUpdateCategories'; const SignInPage: React.FC = () => { const navigate = useNavigate(); @@ -30,6 +31,7 @@ const SignInPage: React.FC = () => { const [isOpenErrorModal, setIsOpenErrorModal] = useState(false); const [isOpenSignUpModal, setIsOpenSignUpModal] = useState(false); const setUserToken = useSetRecoilState(userTokenState); + const { updateCategories } = useUpdateCategories(); const [loginInfo, setLoginInfo] = useState({ email: '', @@ -63,7 +65,7 @@ const SignInPage: React.FC = () => { const handleClickLoginButton = async () => { try { const { token } = (await loginAPI(loginInfo)).data.result; - + await updateCategories(); setUserToken(token); navigate('/'); } catch (error) { diff --git a/src/styles/category/Card.style.ts b/src/styles/category/Card.style.ts index e18abd1..1a0f975 100644 --- a/src/styles/category/Card.style.ts +++ b/src/styles/category/Card.style.ts @@ -2,7 +2,45 @@ import styled from 'styled-components'; import theme from '../theme'; import checkIcon from '@/assets/icons/check.svg'; import checkedIcon from '@/assets/icons/checked.svg'; +import { Link } from 'react-router-dom'; +export const CheckBoxWrap = styled.div` + background-color: rgba(0, 0, 0, 0.5); + display: none; + + flex-direction: row-reverse; + width: 100%; + height: 100%; + + &.activated { + display: flex; + } +`; + +export const CheckBox = styled.input` + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + + width: 24px; + height: 24px; + border-radius: 100%; + background-color: ${theme.color.gray300}; + background-image: url(${checkIcon}); + background-repeat: no-repeat; + background-position: center; + border: 1.5px solid ${theme.color.white}; + color: ${theme.color.white}; + margin: 12px; + + &:checked { + border: 1.5px solid ${theme.color.green300}; + background-color: ${theme.color.green300}; + background-image: url(${checkedIcon}); + background-repeat: no-repeat; + background-position: center; + } +`; export const Container = styled.div` display: grid; grid-template-columns: repeat(3, auto); @@ -17,9 +55,16 @@ export const Wrap = styled.div` border-radius: 16px; overflow: hidden; box-shadow: 0px 4px 40px 0px rgba(0, 0, 0, 0.05); + + &:hover { + ${CheckBoxWrap} { + display: flex; + } + } `; -export const Content = styled.div` +export const Content = styled(Link)` + text-decoration: none; display: flex; flex-direction: column; padding: 24px 20px; @@ -42,36 +87,9 @@ export const ChipWrap = styled.div` flex-wrap: wrap; `; -export const CheckBox = styled.input` - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - - width: 24px; - height: 24px; - border-radius: 50%; - background-color: ${theme.color.gray300}; - background-image: url(${checkIcon}); - background-repeat: no-repeat; - background-position: center; - border: 1.5px solid ${theme.color.white}; - color: ${theme.color.white}; - - position: absolute; - margin-left: 252px; - margin-top: 13px; - - &:checked { - border: 1.5px solid ${theme.color.green300}; - background-color: ${theme.color.green300}; - background-image: url(${checkedIcon}); - background-repeat: no-repeat; - background-position: center; - } -`; - -export const Image = styled.img` - &:hover { - filter: brightness(50%); - } +export const Image = styled.div<{ source: string }>` + background-image: url(${(props) => props.source}); + width: 290px; + height: 163px; + background-size: 100%; `; diff --git a/src/styles/category/EmptyCard.style.ts b/src/styles/category/EmptyCard.style.ts index 4d07b2d..547027d 100644 --- a/src/styles/category/EmptyCard.style.ts +++ b/src/styles/category/EmptyCard.style.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import theme from '../theme'; +import { Link } from 'react-router-dom'; export const Container = styled.div` display: flex; @@ -17,11 +18,11 @@ export const Content = styled.p` ${theme.typography.Subheader2} `; -export const Button = styled.button` - cursor: pointer; - border: 0; +export const Button = styled(Link)` background-color: ${theme.color.gray500}; color: ${theme.color.white}; + text-decoration: none; + text-align: center; border-radius: 100px; padding: 12px 32px; ${theme.typography.Subheader2} diff --git a/src/styles/layout/sideBar/index.ts b/src/styles/layout/sideBar/index.ts index be25c00..bba5b70 100644 --- a/src/styles/layout/sideBar/index.ts +++ b/src/styles/layout/sideBar/index.ts @@ -5,8 +5,9 @@ export const Container = styled.div` display: flex; flex-direction: column; margin: 60px 0px 0px 60px; + padding-right: 20px; width: 288px; - box-shadow: 4px 0px 40px 0px rgba(0, 0, 0, 0.05); + box-shadow: 4px 0px 10px rgba(0, 0, 0, 0.05); z-index: 0; `;