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