From 4a42e1b49b957467aa0bf31caf46a2bdac4ce502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9C=A4?= <asdfasdf001234@ajou.ac.kr> Date: Sun, 8 Dec 2024 01:03:55 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=98=81=EC=83=81=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0+=EB=A3=A8=ED=8B=B4=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/{api.js => api/UserAPI.js} | 11 --- front/src/api/workoutAPI.js | 34 ++++++++- front/src/components/ExerciseBlock.jsx | 73 +++++++++++++------ front/src/components/Thumbnails.jsx | 4 +- front/src/components/common/Header.jsx | 2 +- front/src/components/index.css | 33 ++++++++- front/src/components/user/SignIn.jsx | 2 +- front/src/components/user/SignUp.jsx | 2 +- front/src/components/workout/VideoDetails.jsx | 54 ++++++++++++++ front/src/components/workout/VideoLists.jsx | 20 ++++- front/src/components/workout/secToTime.js | 12 +++ front/src/pages/MyPage.jsx | 2 +- front/src/pages/Sign.jsx | 1 - front/src/pages/Workout.jsx | 27 +------ front/src/pages/routine/progress.jsx | 2 +- 15 files changed, 210 insertions(+), 69 deletions(-) rename front/src/{api.js => api/UserAPI.js} (93%) create mode 100644 front/src/components/workout/VideoDetails.jsx create mode 100644 front/src/components/workout/secToTime.js diff --git a/front/src/api.js b/front/src/api/UserAPI.js similarity index 93% rename from front/src/api.js rename to front/src/api/UserAPI.js index 0c8204f..b82ca60 100644 --- a/front/src/api.js +++ b/front/src/api/UserAPI.js @@ -56,17 +56,6 @@ async function getUserData(){ } catch(err){ console.log(err.message); } - // const data = { - // user_id: 'idididididdd', - // user_password: 'klk', - // user_name: 'asdf', - // user_gender: 0, - // user_birthdate: '2024-11-11', - // user_email: 'asdf@asdf.com', - // user_created_at: new Date(), - // user_height: null, - // user_weight: null - // } }; async function changeUserData(userData) { diff --git a/front/src/api/workoutAPI.js b/front/src/api/workoutAPI.js index f3839c5..4fd8503 100644 --- a/front/src/api/workoutAPI.js +++ b/front/src/api/workoutAPI.js @@ -13,6 +13,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -23,6 +24,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -33,6 +35,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -43,6 +46,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -53,6 +57,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -63,6 +68,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -73,6 +79,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -83,6 +90,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -93,6 +101,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -103,6 +112,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, { @@ -113,6 +123,7 @@ async function getEntireVideos(last_id){ "video_tag": "가슴 홈트레이닝 | Chest Home Training", "video_length": 1071, "video_likes": 14, + "channel_title": "Men's Health UK", "__v": 0 }, ]}; @@ -306,4 +317,25 @@ async function searchVideos(filters, last_id){ } } -export { getEntireVideos, searchVideos }; +async function addRoutineVideo(data){ + //추후에 RoutineAPI로 옮기기 + + try{ + const uri = `/api/routine/add` + const response = await fetch(uri, { + method: "PUT", //POST가 아니고?? + headers: { + "Authorization": `Bearer ${localStorage.getItem('accessToken')}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + const responseData = await response.json(); + if(!responseData || !response.ok) + throw new Error(responseData.message || '루틴을 불러오는데 실패했습니다.'); + else return data; + } catch(err){ + console.log(err.message); + } +} +export { getEntireVideos, searchVideos, addRoutineVideo }; diff --git a/front/src/components/ExerciseBlock.jsx b/front/src/components/ExerciseBlock.jsx index 713ed66..8bbc538 100644 --- a/front/src/components/ExerciseBlock.jsx +++ b/front/src/components/ExerciseBlock.jsx @@ -1,25 +1,18 @@ -import react from 'react'; +import react, {useState} from 'react'; +import VideoDetails from './workout/VideoDetails'; +import Modal from '../components/common/Modal'; +import secToTime from './workout/secToTime'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faHeart } from "@fortawesome/free-regular-svg-icons"; import './index.css'; import Thumbnails from './Thumbnails'; -function secToTime(time){ - let mins = Math.floor(parseInt(time) / 60); - const secs = parseInt(time) % 60; - if (mins>60){ - const hrs = Math.floor(mins/60); - mins = mins%60; - return `${hrs}:${mins}:${secs}`; - } - return `${mins}:${secs}`; -} -function handleClick(){ - //모달창 띄워서 video의 data보여주기 - return 0; +function handleClick(e, item, onClick) { + console.log(e); // 클릭 이벤트 로그 + onClick(item); // 부모로 데이터 전달 } function Clickable({data, onClick}){ @@ -28,14 +21,16 @@ function Clickable({data, onClick}){ <> {data.map((item) => ( <div> - <div id="clickBlock" className="block"> - <Thumbnails video_id={item.video_id} /> + <div id="clickBlock" className="block" onClick={(e) => onClick(item)}> + <Thumbnails video_id={item.video_id} video_title={item.video_title} mode="mqdefault"/> <div className='space-between'> <h3 className="simplified">{item.video_title}</h3> + <span className='simplified'>{item.channel_title}</span> <span>{item.video_tag}<br/></span> - <span>{secToTime(item.video_length)}</span> - <span className='right-text'> <FontAwesomeIcon icon={faHeart} /> - {item.video_likes}<br/></span> + <span className='right-text'> + <span>{secToTime(item.video_length)}</span> + <span><FontAwesomeIcon icon={faHeart} /> {item.video_likes}<br/></span> + </span> </div> </div> </div> @@ -59,16 +54,48 @@ function NonClickable({data}){ } -function ExerciseBlock({data, mode}){ +function ExerciseBlock({data, mode, routines}){ + const [showModal, setShowModal] = useState(false); + const [selectedVideo, setSelectedVideo] = useState(null); + + const handleVideoClick = (video) => { + setSelectedVideo(video); + setShowModal(true); + }; + return ( - (mode === 'clickable')?( + <> + {(mode === 'clickable')?( <Clickable data={data} - onClick={handleClick} /> + onClick={handleVideoClick}/> ):( <NonClickable data={data} /> - ) + )} + + {showModal && + <Modal width="80vw" height="80vh"> + <div style={{ position: 'relative', padding: '20px' }}> + <button + onClick={() => setShowModal(false)} + style={{ + position: 'absolute', + right: '-30px', + top: '-80px', + background: 'none', + border: 'none', + fontSize: '20px', + cursor: 'pointer' + }} + > + ✕ + </button> + <VideoDetails video={selectedVideo} routines={routines}/> + </div> + </Modal> + } + </> ); } diff --git a/front/src/components/Thumbnails.jsx b/front/src/components/Thumbnails.jsx index b3a6195..0458d00 100644 --- a/front/src/components/Thumbnails.jsx +++ b/front/src/components/Thumbnails.jsx @@ -1,8 +1,8 @@ import react from 'react'; -function Thumbnails({video_id, video_title}){ +function Thumbnails({video_id, video_title, mode}){ return ( - <img className="thumbnails" src={`https://img.youtube.com/vi/${video_id}/mqdefault.jpg`} alt={video_title}/> + <img className={`thumbnails-${mode}`} src={`https://img.youtube.com/vi/${video_id}/${mode}.jpg`} alt={video_title}/> ) } diff --git a/front/src/components/common/Header.jsx b/front/src/components/common/Header.jsx index a68f116..ad49399 100644 --- a/front/src/components/common/Header.jsx +++ b/front/src/components/common/Header.jsx @@ -3,7 +3,7 @@ import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; import '../index.css'; import {ReactComponent as Logo} from '../../assets/logo.svg'; -import { userLogout } from '../../api'; +import { userLogout } from '../../api/UserAPI'; function Header(){ async function handleSignOut() { diff --git a/front/src/components/index.css b/front/src/components/index.css index 26537ff..9ea11d0 100644 --- a/front/src/components/index.css +++ b/front/src/components/index.css @@ -116,14 +116,22 @@ ul{ } .right-text{ + display: flex; text-align: right; + padding-bottom: 10px; + justify-content: space-between; } -.thumbnails{ +.thumbnails-mqdefault{ width: 10em; height: 5.6em; } +.thumbnails-maxresdefault{ + width: 32.4em; + height: 18em; +} + .more{ width: 100%; display: inline-flex; @@ -159,6 +167,8 @@ input[type='radio']:checked { #background{ position: absolute; + top: 0; + left: 0; background-color: #333333a5; width: 100vw; height: 100vh; @@ -175,4 +185,25 @@ input[type='radio']:checked { justify-content: center; text-align: center; align-items: center; +} + +.videoDetails{ + display: flex; + align-items: center; +} + +.left-align{ + padding: 20px; + display: flex; + flex-direction: column; + text-align: left; +} + +.details{ + font-size: large; + padding-bottom: 7px; +} + +.addbutton{ + align-self: center; } \ No newline at end of file diff --git a/front/src/components/user/SignIn.jsx b/front/src/components/user/SignIn.jsx index 386074d..c6d2dfa 100644 --- a/front/src/components/user/SignIn.jsx +++ b/front/src/components/user/SignIn.jsx @@ -1,6 +1,6 @@ import react, { useState } from 'react'; import '../index.css'; -import { userLogin } from '../../api'; +import { userLogin } from '../../api/UserAPI'; function SignIn(){ const [userId, setUserId] = useState(''); diff --git a/front/src/components/user/SignUp.jsx b/front/src/components/user/SignUp.jsx index f7d3233..5c20cdf 100644 --- a/front/src/components/user/SignUp.jsx +++ b/front/src/components/user/SignUp.jsx @@ -1,6 +1,6 @@ import react, { useState } from 'react'; import '../index.css'; -import { userSignUp } from '../../api'; +import { userSignUp } from '../../api/UserAPI'; function SignUp(){ const [userId, setUserId] = useState(''); diff --git a/front/src/components/workout/VideoDetails.jsx b/front/src/components/workout/VideoDetails.jsx new file mode 100644 index 0000000..5a61178 --- /dev/null +++ b/front/src/components/workout/VideoDetails.jsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import Thumbnails from '../Thumbnails'; +import secToTime from './secToTime'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faHeart } from "@fortawesome/free-regular-svg-icons"; + +function VideoDetails({video, routines}) { + async function addRoutine(e){ + const data = { + routine_name : e.target.value, + video_id : video.video_id + } + try{ + const response = await addRoutineVideo(data); + } catch(err) { + alert(err.message); + } + } + + return ( + <div className='videoDetails'> + <Thumbnails video_id={video.video_id} video_title={video.video_title} mode="maxresdefault" /> + {/*머있어야되지,, 일단 드롭다운 메뉴에서 루틴 선택하기 루틴에 추가하는 API부르기*/} + <div className='left-align'> + <h2>{video.video_title}</h2> + <span className='details'>{video.channel_title}</span> + <span className='details'>{video.video_tag}<br/></span> + <span className='details right-text'> + <span>{secToTime(video.video_length)}</span> + <span><FontAwesomeIcon icon={faHeart} /> {video.video_likes}<br/></span> + </span> + <select name="routines" id="routines"> + {routines.map((item) => ( + <> + <option value={item}>{item}</option> + <option value="javascript">JavaScript</option> + <option value="php">PHP</option> + <option value="java">Java</option> + <option value="golang">Golang</option> + <option value="python">Python</option> + <option value="c#">C#</option> + <option value="C++">C++</option> + <option value="erlang">Erlang</option> + </> + ))} + </select> + <button className="addbutton" onClick={addRoutine}>추가</button> + </div> + </div> + ); +} + +export default VideoDetails; \ No newline at end of file diff --git a/front/src/components/workout/VideoLists.jsx b/front/src/components/workout/VideoLists.jsx index 08c8129..bc28833 100644 --- a/front/src/components/workout/VideoLists.jsx +++ b/front/src/components/workout/VideoLists.jsx @@ -1,4 +1,4 @@ -import react, {useState} from 'react'; +import react, {useEffect, useState, useRef } from 'react'; import ExerciseBlock from '../ExerciseBlock'; import { searchVideos } from '../../api/workoutAPI'; @@ -11,7 +11,22 @@ function timeToSecond(time){ total += (min*60+sec); return total; } -function VideoLists({data, filter, onShowMore}){ +function VideoLists({data, filter, onShowMore, setShowModal}){ + const routines = useRef([]); + + useEffect(() => { + fetchRoutineNames(); + }, []) + + const fetchRoutineNames = async () => { + try{ + const data = await getUserRoutines(); + const routine = data.map(item => item.routine_name); + routines.current = routine; + } catch (error) { + console.error(error.message); + }} + function filteringVideos(data, filter){ return data.filter(video => { const filterTag = filter.video_tag.length?filter.video_tag.some(tag => { @@ -31,6 +46,7 @@ function VideoLists({data, filter, onShowMore}){ <ExerciseBlock data={filteredVideos} mode='clickable' + routines={routines.current} /> <div className='more' onClick={onShowMore}> <button>더보기</button> diff --git a/front/src/components/workout/secToTime.js b/front/src/components/workout/secToTime.js new file mode 100644 index 0000000..36a631a --- /dev/null +++ b/front/src/components/workout/secToTime.js @@ -0,0 +1,12 @@ +function secToTime(time){ + let mins = Math.floor(parseInt(time) / 60); + const secs = parseInt(time) % 60; + if (mins>60){ + const hrs = Math.floor(mins/60); + mins = mins%60; + return `${hrs}:${mins}:${secs}`; + } + return `${mins}:${secs}`; +} + +export default secToTime; \ No newline at end of file diff --git a/front/src/pages/MyPage.jsx b/front/src/pages/MyPage.jsx index a73b115..aa858db 100644 --- a/front/src/pages/MyPage.jsx +++ b/front/src/pages/MyPage.jsx @@ -1,6 +1,6 @@ import react, { useState, useEffect } from 'react'; -import {getUserData, changeUserData, userWithdraw} from '../api.js'; +import {getUserData, changeUserData, userWithdraw} from '../api/UserAPI.js'; import Modal from '../components/common/Modal.jsx'; function MyPage(){ diff --git a/front/src/pages/Sign.jsx b/front/src/pages/Sign.jsx index b59a7d7..4a71d29 100644 --- a/front/src/pages/Sign.jsx +++ b/front/src/pages/Sign.jsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react'; import './index.css'; import SignIn from '../components/user/SignIn'; import SignUp from '../components/user/SignUp'; -import { userLogin, userSignUp } from '../api'; function AsideContent({ isActive, onShow, title, label }){ let move = ''; diff --git a/front/src/pages/Workout.jsx b/front/src/pages/Workout.jsx index b7f8ac8..bc76798 100644 --- a/front/src/pages/Workout.jsx +++ b/front/src/pages/Workout.jsx @@ -1,10 +1,8 @@ -import React, { useState, useEffect, useReducer, useRef } from 'react'; +import React, { useState, useEffect, useReducer } from 'react'; -import Resizable from '../components/Resizable'; import VideoLists from '../components/workout/VideoLists'; import VideoSelection from '../components/workout/VideoSelection'; import Images from '../components/workout/Images'; -import ExerciseBlock from '../components/ExerciseBlock'; import { getEntireVideos, searchVideos} from '../api/workoutAPI'; @@ -32,7 +30,6 @@ function Workout(){ } const [videos, setVideos] = useState([]); - const [expose, setExpose] = useState(videos); const [selected, setSelected] = useState(); const [showMore, setShowMore] = useState(false); const [filter, dispatch] = useReducer(FilterReducer, initial); @@ -41,16 +38,10 @@ function Workout(){ fetchEntireVideos(); }, []) useEffect(() => { - if (videos.length >= 500){ //500개만 가지고있도록 관리 - //필터링 시 비디오 태그에 맞게 필터링하는 기능.. - //블록에 유튜버 이름도 보이게.. - //페이지 없앰.. - setVideos() + if (videos.length > 500){ //500개만 가지고있도록 관리 + setVideos(videos.slice(-500)); } - }, videos, filter) - useEffect(() => { - //필터가 바뀔때마다 필터링된 비디오 제공... - }) + }, videos) const fetchEntireVideos = async () => { try{ @@ -87,26 +78,16 @@ function Workout(){ } }; - //filter에 따라서 필터링된 data를 제공 function FilterReducer(filter, action){ switch (action.type){ case 'tag': { - // if(!action.tag.length){ - // delete filter.video_tag; - // console.log(filter); - // return filter; - // } - // else{ const newFilter = {...filter, video_tag: action.tag}; console.log(newFilter); return newFilter; - // } - //return videos.filter((t) => t.tags.indexOf(action.tag) !== -1); } case 'time': { const newFilter = {...filter, video_time_from: action.start, video_time_to: action.end}; return newFilter; - //return videos.filter((t) => t.time<action.end && t.time>action.start); } case 'level':{ //레벨 다시 눌렀을때 없어질 수 있어야함... diff --git a/front/src/pages/routine/progress.jsx b/front/src/pages/routine/progress.jsx index a85af96..6f52629 100644 --- a/front/src/pages/routine/progress.jsx +++ b/front/src/pages/routine/progress.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import "./Progress.css"; +import "./progress.css"; function Progress({ times, endSignal, onendClick }) { const [restTimes, setRestTimes] = useState([]); -- GitLab