diff --git a/front/src/App.jsx b/front/src/App.jsx index 36198867147608ba27c94bf110ff3371058b72d0..c4c6494be7b9caea69eda6c40cd7211114019b82 100644 --- a/front/src/App.jsx +++ b/front/src/App.jsx @@ -6,7 +6,7 @@ import Footer from './components/common/Footer' import Sign from './pages/Sign'; import MyPage from './pages/MyPage'; import Workout from './pages/Workout'; -import Routine from './pages/routine/Routine'; +import Routine from './pages/Routine'; import './App.css'; diff --git a/front/src/pages/routine/api.js b/front/src/api/routineAPI.js similarity index 61% rename from front/src/pages/routine/api.js rename to front/src/api/routineAPI.js index 75b640f91ca465f969913bd9bfba12dc0e5c1fd8..61726101177e72030cd1597cfcabecc78e4e997b 100644 --- a/front/src/pages/routine/api.js +++ b/front/src/api/routineAPI.js @@ -1,52 +1,75 @@ -async function fetchWithOptions(url, options) { - try { - const response = await fetch(url, options); - if (!response.ok) { - throw new Error(`HTTP 오류! 상태 코드: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error("API 요청 중 에러 발생:", error.message); - throw error; - } -} - -async function addExerciseRecord(record) { - return await fetchWithOptions('/routine/records', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(record), - }); -} - -async function getUserRoutines() { - return await fetchWithOptions('/routine', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); -} - -async function getRoutineVideos(routineName) { - return await fetchWithOptions(`/routine/videos?routine_name=${routineName}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); -} - -async function deleteRoutine(routineName) { - return await fetchWithOptions('/routine', { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ routine_name: routineName }), - }); -} - -export { addExerciseRecord, getUserRoutines, getRoutineVideos, deleteRoutine }; +async function fetchWithOptions(url, options) { + try { + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`HTTP 오류! 상태 코드: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("API 요청 중 에러 발생:", error.message); + throw error; + } +} + +async function addExerciseRecord(record) { + return await fetchWithOptions('/routine/records', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(record), + }); +} + +async function getUserRoutines() { + return await fetchWithOptions('/routine', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +async function getRoutineVideos(routineName) { + return await fetchWithOptions(`/routine/videos?routine_name=${routineName}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +async function deleteRoutine(routineName) { + return await fetchWithOptions('/routine', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ routine_name: routineName }), + }); +} + +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 { addRoutineVideo, addExerciseRecord, getUserRoutines, getRoutineVideos, deleteRoutine }; diff --git a/front/src/api/workoutAPI.js b/front/src/api/workoutAPI.js index 4fd8503f664865277fcd5a623a6cae8c795ded1c..7cb85588fe535b8f1eefe36b251c33cee6ae7c08 100644 --- a/front/src/api/workoutAPI.js +++ b/front/src/api/workoutAPI.js @@ -317,25 +317,4 @@ async function searchVideos(filters, last_id){ } } -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 }; +export { getEntireVideos, searchVideos }; diff --git a/front/src/assets/explains.json b/front/src/assets/explains.json index 1138d9e6d3d6f22befb796f059e79f15d727df66..492e8f0185db201ffde2694588a78d7304fc698c 100644 --- a/front/src/assets/explains.json +++ b/front/src/assets/explains.json @@ -1,5 +1,6 @@ [ { + "id" : "calf", "name" : "가자미근 (Soleus muscle)", "explains" : "정강이 뒤에 있는 하퇴삼두근을 구성하고 있는 가자미모양의 근육입니다. 종아리 근육인 비복근과 함께 발을 아래로 굽히는 동작을 도와주고 서 있을 때는 자세를 유지하는데 쓰이며, 걷는 동안 몸을 안정적으로 지탱해줍니다. 굽이 높은 신발을 신는 경우 해당 부위의 근육이 더 많이 사용됩니다. 가자미근이 피로해지면 통증이 주로 가자미근의 안쪽 아래 부분, 발뒤꿈치, 그리고 아킬레스건에 나타날 수 있습니다. 발끝을 잡아 발목을 몸 쪽으로 당기는 스트레칭이나, 계단 끝에 발을 올려 뒤꿈치를 내리는 동작을 통해 가자미근의 피로를 풀어줄 수 있습니다.", "source" : "[네이버 지식백과] [넙치근](https://terms.naver.com/entry.naver?docId=1076186) [soleus muscle] (두산백과 두피디아, 두산백과) " @@ -12,6 +13,7 @@ "source" : " [네이버 지식백과] [대퇴사두근](https://terms.naver.com/entry.naver?docId=1081589) [musculus quadriceps femoris, 大腿四頭筋] (두산백과 두피디아, 두산백과)" }, { + "id" : "back-calf", "name" : "비복근 (Gastrocnemius muscle)", "explains" : "종아리 뒤쪽에 위치한 두 갈래로 나뉜 근육을 비복근이라 하며 허벅지뼈(넓적다리뼈)에서 시작해 발뒤꿈치까지 이어집니다. 비복근은 발뒤꿈치를 들거나 무릎을 굽히는 역할을 합니다. 이로 인해 걷기, 달리기, 점프 같은 다양한 움직임을 가능하게 해줍니다. 특히 단거리 달리기와 점프처럼 순간적으로 강한 힘이 필요한 동작에서 사용됩니다. 뛰다가 갑작스럽게 방향을 바꾸거나 과도한 움직임을 할 경우, 비복근이 손상되거나 파열될 위험이 있습니다. 평상시에도 벽을 향해 서서 한쪽 다리를 뒤로 뻗고 앞다리를 살짝 구부린 채 뒷다리의 종아리가 늘어나도록 하는 등의 스트레칭을 통해 충분히 풀어주어야 합니다.", "source" : "[네이버 지식백과] [비복근](https://terms.naver.com/entry.naver?docId=1106015) [gastrocnemius, 腓腹筋] (두산백과 두피디아, 두산백과)" diff --git a/front/src/components/Footer.jsx b/front/src/components/Footer.jsx deleted file mode 100644 index a5ee5826f48484c3e5227e4123bd542a844cb8f3..0000000000000000000000000000000000000000 --- a/front/src/components/Footer.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import react from 'react'; -import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; - -import './index.css'; - -function Footer() { - return ( - <div id="footer"> - <ul> - <li> 웹시스템설계 </li> - <li> 9조 </li> - </ul> - <ul> - <li> 백엔드 </li> - <li> 문경호 </li> - <li> 김다인 </li> - </ul> - <ul> - <li> 프론트엔드 </li> - <li> 박태현 </li> - <li> 장지윤 </li> - </ul> - <ul> - <li>DOCUMENT</li> - <li><a href='https://git.ajou.ac.kr/wss9/fiturring' class="link" >GitLab</a></li> - <li><a href='https://www.notion.so/2024-2-130669572a77805db7e8e4f991ad455e?pvs=4' class="link">Notion</a></li> - </ul> - </div> - ) -} -export default Footer; \ No newline at end of file diff --git a/front/src/components/Header.jsx b/front/src/components/Header.jsx deleted file mode 100644 index 7414da56c90e88b2fadd7d16e3c28b02a05bcce0..0000000000000000000000000000000000000000 --- a/front/src/components/Header.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import react from 'react'; -import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; - -import './index.css'; -import {ReactComponent as Logo} from '../assets/logo.svg'; - -function MyPage(props){ - if (props.userId){ - return ( - <span> - <Link class="link large" to='/mypage'>My Page</Link> - <Link class="link large" to='/sign'>Sign Out</Link> - </span> - ); - } - else{ - return ( - <Link class="link large" to='/sign'>Sign in</Link> - ); - } -} - -function Header(){ - - return ( - <div id="header"> - <Link class="logo link large" to='/'> - <Logo width='25pt' height='30pt' fill='#0072CE'/> - <span>Fiturring</span> - </Link> - <Link class="link large" to='/workout'>Workout</Link> - <Link class="link large" to='/habitTracker'>Habit Tracker</Link> - <Link class="link large" to='/routine'>Routine</Link> - <Link class="link large" to='diet'>Diet</Link> - <MyPage /> - </div> - ); -} - -export default Header; \ No newline at end of file diff --git a/front/src/components/Resizable.jsx b/front/src/components/Resizable.jsx deleted file mode 100644 index 63db821f12fab8ff03f2d7b07df5ac9bb05b1bb4..0000000000000000000000000000000000000000 --- a/front/src/components/Resizable.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import react, { useState } from 'react'; - -function Resizable({defaultSize, children}){ - const [leftSize, setLeftSize] = useState(defaultSize); - const [rightSize, setRightSize] = useState(defaultSize); - const [isResizing, setIsResizing] = useState(false); - - function startResize(e){ - console.log(e.target.getBoundingClientRect()); - - } - - function handleResize(e){ - setIsResizing(true); - } - - function finishResize(e){ - // const left = e.target.getBoundingClientRect().left; - // const right = e.target.getBoundingClientRect().right; - // const newWidth = right - left; - // //마우스업이 발생한 지점의 clientX가 그 컴포넌트의 너비가 됨... - // //console.log(e.clientX); - // e.target.style.width=newWidth; - // console.log(e.clientX); - e.target.getBoundingClientRect().left = e.clientX; - - } - - //onMouseUp={handle} - return( - <div onMouseDown={startResize} onMouseMove={handleResize} onMouseUp={finishResize} - style={{backgroundColor:"#999999"}}> - <div style={{backgroundColor:"#555555"}}></div> - <h2>Resizable</h2> - {children} - </div> - ); -} -export default Resizable; \ No newline at end of file diff --git a/front/src/components/ExerciseBlock.jsx b/front/src/components/common/ExerciseBlock.jsx similarity index 93% rename from front/src/components/ExerciseBlock.jsx rename to front/src/components/common/ExerciseBlock.jsx index 8bbc53832eb85a7aff3ea4c3ba89be7cf4b98816..e4d699507c945ef6d1f4359328d8d285d904293c 100644 --- a/front/src/components/ExerciseBlock.jsx +++ b/front/src/components/common/ExerciseBlock.jsx @@ -1,14 +1,13 @@ import react, {useState} from 'react'; -import VideoDetails from './workout/VideoDetails'; -import Modal from '../components/common/Modal'; -import secToTime from './workout/secToTime'; +import VideoDetails from '../workout/VideoDetails'; +import Modal from './Modal'; +import secToTime from '../workout/secToTime'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faHeart } from "@fortawesome/free-regular-svg-icons"; +import Thumbnails from '../common/Thumbnails'; - -import './index.css'; -import Thumbnails from './Thumbnails'; +import '../index.css'; function handleClick(e, item, onClick) { console.log(e); // 클릭 이벤트 로그 diff --git a/front/src/components/Thumbnails.jsx b/front/src/components/common/Thumbnails.jsx similarity index 100% rename from front/src/components/Thumbnails.jsx rename to front/src/components/common/Thumbnails.jsx diff --git a/front/src/pages/routine/List.css b/front/src/components/routine/List.css similarity index 93% rename from front/src/pages/routine/List.css rename to front/src/components/routine/List.css index 02c4b626bb7df4c766bb90b65c39ed9ea6c2c21e..5a240d9d1881f586d689e5966823266bc8659f13 100644 --- a/front/src/pages/routine/List.css +++ b/front/src/components/routine/List.css @@ -1,142 +1,142 @@ -#list-container { - padding: 20px 20px; - width: 255px; - height: 495px; -} - -.list-head { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} - -.list-head span { - font-weight: 600; - font-size: 20px; - line-height: 38px; - - text-align: center; - - color: #000000; -} - -.division-line { - width: 180px; - - border: 1px solid #DFDFE4; -} - -#list-content { - height: 400px; - overflow-y: auto; - -ms-overflow-style: none; - box-sizing: border-box; - - .pick { - background-color: #8C7DFF; - } - - .pick:hover { - background-color: #CCC5FF; - } -} - -#list-content::-webkit-scrollbar { - display: none; -} - -#list-content ul { - margin-top: 5px; - display: flex; - flex-direction: column; - align-items: center; - text-align: left; - padding: 0px; - margin-left: 5px; - gap: 5px; -} - - -.list-head li { - width: 230px; - height: 45px; - - cursor: pointer; - - border-radius: 10px; - - display: flex; - align-items: center; - - position: relative; -} - -#list-content li span { - width: 300px; - height: 45px; - - - cursor: pointer; - - font-weight: 600; - font-size: 17px; - border-radius: 10px; - - display: flex; - align-items: center; - - color: #000000; - position: relative; -} - -#list-content li:hover { - background-color: #D5D5DC; -} - -#list-content li::after { - content: ""; - position: absolute; - bottom: 0; - left: 7%; - width: 83%; - height: 1.2px; - background-color: #DFDFE4; -} - -#list-content li button { - font-weight: 600; - - width: 25px; - height: 25px; - - background: #E26F6F; - border-radius: 5px; -} - -#list-content li button:hover { - background: #FFA9A9; -} - -#list-content button { - font-weight: 600; - background: #CFFF5E; - border-radius: 10px; - margin: 0; - margin-left: 10px; - margin-right: 10px; - cursor: pointer; -} - -#list-content button:hover { - background: #EAFFB8; -} - -.list-foot { - display: flex; - align-items: center; - flex-direction: row; - gap: 0; - margin: 0; - width: 255px; +#list-container { + padding: 20px 20px; + width: 255px; + height: 495px; +} + +.list-head { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.list-head span { + font-weight: 600; + font-size: 20px; + line-height: 38px; + + text-align: center; + + color: #000000; +} + +.division-line { + width: 180px; + + border: 1px solid #DFDFE4; +} + +#list-content { + height: 400px; + overflow-y: auto; + -ms-overflow-style: none; + box-sizing: border-box; + + .pick { + background-color: #8C7DFF; + } + + .pick:hover { + background-color: #CCC5FF; + } +} + +#list-content::-webkit-scrollbar { + display: none; +} + +#list-content ul { + margin-top: 5px; + display: flex; + flex-direction: column; + align-items: center; + text-align: left; + padding: 0px; + margin-left: 5px; + gap: 5px; +} + + +.list-head li { + width: 230px; + height: 45px; + + cursor: pointer; + + border-radius: 10px; + + display: flex; + align-items: center; + + position: relative; +} + +#list-content li span { + width: 300px; + height: 45px; + + + cursor: pointer; + + font-weight: 600; + font-size: 17px; + border-radius: 10px; + + display: flex; + align-items: center; + + color: #000000; + position: relative; +} + +#list-content li:hover { + background-color: #D5D5DC; +} + +#list-content li::after { + content: ""; + position: absolute; + bottom: 0; + left: 7%; + width: 83%; + height: 1.2px; + background-color: #DFDFE4; +} + +#list-content li button { + font-weight: 600; + + width: 25px; + height: 25px; + + background: #E26F6F; + border-radius: 5px; +} + +#list-content li button:hover { + background: #FFA9A9; +} + +#list-content button { + font-weight: 600; + background: #CFFF5E; + border-radius: 10px; + margin: 0; + margin-left: 10px; + margin-right: 10px; + cursor: pointer; +} + +#list-content button:hover { + background: #EAFFB8; +} + +.list-foot { + display: flex; + align-items: center; + flex-direction: row; + gap: 0; + margin: 0; + width: 255px; } \ No newline at end of file diff --git a/front/src/pages/routine/List.jsx b/front/src/components/routine/List.jsx similarity index 98% rename from front/src/pages/routine/List.jsx rename to front/src/components/routine/List.jsx index aad379cf87c75e35280c86756c8aa789085e629b..ed16b331f0ee322e95f96aeea477601f58a0a980 100644 --- a/front/src/pages/routine/List.jsx +++ b/front/src/components/routine/List.jsx @@ -1,309 +1,309 @@ -import React, { useState, useEffect } from "react"; -import "./List.css"; -import truncateText from './truncateText'; -import { getUserRoutines, getRoutineVideos, deleteRoutine } from './api'; - -const initialRoutines = [ - { - id: 1, - name: "전신 루틴", - exercises: [ - { title: "전신 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example1", duration: 300, canceled: false }, - { title: "전신 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example2", duration: 420, canceled: false }, - { title: "전신 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example3", duration: 350, canceled: false }, - ], - }, - { - id: 2, - name: "상체 루틴", - exercises: [ - { title: "상체 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example11", duration: 360, canceled: false }, - { title: "상체 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example12", duration: 400, canceled: false }, - { title: "상체 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example13", duration: 330, canceled: false }, - { title: "상체 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example14", duration: 470, canceled: false }, - { title: "상체 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example15", duration: 380, canceled: false }, - { title: "상체 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example16", duration: 390, canceled: false }, - { title: "상체 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example17", duration: 420, canceled: false }, - { title: "상체 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example18", duration: 440, canceled: false }, - { title: "상체 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example19", duration: 350, canceled: false }, - { title: "상체 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example20", duration: 500, canceled: false }, - ], - }, - { - id: 3, - name: "하체 루틴", - exercises: [ - { title: "하체 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example21", duration: 350, canceled: false }, - { title: "하체 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example22", duration: 450, canceled: false }, - { title: "하체 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example23", duration: 400, canceled: false }, - { title: "하체 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example24", duration: 500, canceled: false }, - { title: "하체 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example25", duration: 480, canceled: false }, - { title: "하체 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example26", duration: 420, canceled: false }, - { title: "하체 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example27", duration: 430, canceled: false }, - { title: "하체 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example28", duration: 360, canceled: false }, - { title: "하체 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example29", duration: 490, canceled: false }, - { title: "하체 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example30", duration: 460, canceled: false }, - ], - }, - { - id: 4, - name: "복부 루틴", - exercises: [ - { title: "복부 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example31", duration: 400, canceled: false }, - { title: "복부 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example32", duration: 300, canceled: false }, - { title: "복부 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example33", duration: 350, canceled: false }, - { title: "복부 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example34", duration: 380, canceled: false }, - { title: "복부 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example35", duration: 320, canceled: false }, - { title: "복부 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example36", duration: 370, canceled: false }, - { title: "복부 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example37", duration: 390, canceled: false }, - { title: "복부 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example38", duration: 430, canceled: false }, - { title: "복부 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example39", duration: 480, canceled: false }, - { title: "복부 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example40", duration: 500, canceled: false }, - ], - }, - { - id: 5, - name: "유산소 루틴", - exercises: [ - { title: "유산소 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example41", duration: 600, canceled: false }, - { title: "유산소 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example42", duration: 500, canceled: false }, - { title: "유산소 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example43", duration: 550, canceled: false }, - { title: "유산소 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example44", duration: 510, canceled: false }, - { title: "유산소 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example45", duration: 560, canceled: false }, - { title: "유산소 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example46", duration: 530, canceled: false }, - { title: "유산소 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example47", duration: 480, canceled: false }, - { title: "유산소 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example48", duration: 540, canceled: false }, - { title: "유산소 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example49", duration: 490, canceled: false }, - { title: "유산소 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example50", duration: 520, canceled: false }, - ], - }, - { - id: 6, - name: "스트레칭 루틴", - exercises: [ - { title: "스트레칭 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example51", duration: 300, canceled: false }, - { title: "스트레칭 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example52", duration: 400, canceled: false }, - { title: "스트레칭 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example53", duration: 350, canceled: false }, - { title: "스트레칭 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example54", duration: 300, canceled: false }, - { title: "스트레칭 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example55", duration: 380, canceled: false }, - { title: "스트레칭 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example56", duration: 420, canceled: false }, - { title: "스트레칭 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example57", duration: 440, canceled: false }, - { title: "스트레칭 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example58", duration: 450, canceled: false }, - { title: "스트레칭 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example59", duration: 500, canceled: false }, - { title: "스트레칭 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example60", duration: 480, canceled: false }, - ], - }, - { - id: 7, - name: "등 루틴", - exercises: [ - { title: "업그레이드를 위한 새로운 등운동 [ BACK DAY ]", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=f7wFbp9BnFs", duration: 300, canceled: false }, - { title: "뚫고 나오는 등 만들고 싶으면 이거 봐", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=8SBBp65Sv3c", duration: 400, canceled: false }, - { title: "Try This Back Exercise | Back & Hamstrings Workout", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=nRzAV-CYndA", duration: 350, canceled: false }, - { title: "My Title Winning Back Training", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=5dp2FUN3mRQ", duration: 300, canceled: false }, - { title: "Back and Delts Workout", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=DjfnFj-50b4", duration: 380, canceled: false }, - { title: "INTENSE Back Workout | Mr. Olympia Derek Lunsford", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=HYngFKG5YbY&t=477s", duration: 420, canceled: false }, - { title: "Mr. Olympia BACK WORKOUT", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=HqZOWPRyck8&t=134s", duration: 440, canceled: false }, - { title: "등 운동 후 몽둥이질 당한 느낌이 나게 하는 방법들", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=OD1JMTLJp-A", duration: 450, canceled: false }, - { title: "등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) ", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=naxGvgl9pKg&t=1102s", duration: 500, canceled: false }, - { title: "요즘 유행하는 등 운동 루틴", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=XLCtwqECMrs&t=279s", duration: 480, canceled: false }, - ], - }, -]; - -function List({ onRoutineSelect, isActive }) { - - const [routines, setRoutines] = useState(initialRoutines); // 루틴 목록 - const [selectedRoutine, setSelectedRoutine] = useState(null); // 선택한 루틴 - const [modify, setModify] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); - - const modalStyle = { - position: 'fixed', - top: 0, - left: 0, - width: '100%', - height: '100%', - color: '#000000', - background: 'rgba(0, 0, 0, 0.1)', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - zIndex: 9999, - }; - - const modalContentStyle = { - background: 'white', - padding: '40px', - fontSize: '25px', - borderRadius: '10px', - textAlign: 'center', - boxShadow: '0 4px 10px rgba(0, 0, 0, 0.3)', - }; - - const buttonStyle = { - height: '60px', - width: '140px', - margin: '30px', - fontSize: '0.7em', - cursor: 'pointer', - }; - -const fetchRoutines = async () => { - try { - const data = await getUserRoutines(); - fetchExercises(data); - } catch (err) { - alert(err); - } -}; - -const fetchExercises = async (routines) => { - try { - const updatedRoutines = await Promise.all( - routines.map(async (rt) => { - const data = await getRoutineVideos(rt.name); - const updatedExercises = data.map((exercise) => ({ - ...exercise, - canceled: false, - })); - return { ...rt, exercises: updatedExercises }; - }) - ); - setRoutines(updatedRoutines); - } catch (err) { - alert(err); - } -}; - -useEffect(() => { - fetchRoutines(); -}, []); - -/* - 루틴 목록에서 원하는 루틴 클릭 시 해당 루틴을 selectedRoutine으로 설정하여 - 해당 루틴 운동 목록화면으로 전환 -*/ -const handleRoutineClick = (routine) => { - if (!modify) setSelectedRoutine(routine); - -}; - -/* - 뒤로가기 클릭 시 selectedRoutine 값을 null로 설정하여 루틴 목록화면으로 전환 -*/ -const handleBackClick = () => { - setSelectedRoutine(null); -}; - -const handledelete = async (name) => { - try { - await deleteRoutine(name); - setIsModalOpen(false); - } catch (err) { - alert(err); - } -}; - -/* - 토글 클릭 시 해당 운동의 루틴 제외 여부 설정 -*/ -const toggleCancelExercise = (exercise) => { - const updatedExercises = selectedRoutine.exercises.map((ex) => - ex.title === exercise.title ? { ...ex, canceled: !ex.canceled } : ex - ); - setSelectedRoutine({ ...selectedRoutine, exercises: updatedExercises }); -}; - -return ( - <div id="list-container"> - {!selectedRoutine ? ( - <div className="list-head"> - {/*selectedRoutine이 null로 루틴 목록 화면 표시*/} - <div> - <span>routine</span> - <div className="division-line"></div> - </div> - <div id="list-content"> - <ul> - {routines.map((routine) => ( - <li key={routine.id} onClick={() => handleRoutineClick(routine)}> - <span>{truncateText(routine.name, 10)}</span> - {modify && ( - <button onClick={(e) => { - e.stopPropagation(); // 이벤트 전파 중단 - setIsModalOpen(true); - }}>X</button> - - )} - {isModalOpen && ( - <div style={modalStyle}> - <div style={modalContentStyle}> - <div> - 루틴을 삭제하겠습니까? - <div> - <button style={buttonStyle} onClick={() => handledelete(routine.title)}>삭제</button> - <button style={buttonStyle} onClick={() => setIsModalOpen(false)}>취소</button> - </div> - </div> - </div> - </div> - )} - </li> - ))} - </ul> - {!modify ? (<button className='back' onClick={() => setModify(true)}>수정</button> - ) : (<button className='back' onClick={() => setModify(false)}>뒤로가기</button>)} - </div> - </div> - ) : ( - <div className="list-head"> - {/*selectedRoutine이 설정 되어 운동 목록 화면 표시*/} - <div> - <span>{selectedRoutine.name}</span> - <div className="division-line"></div> - </div> - <div id="list-content"> - <ul> - {selectedRoutine.exercises.map((exercise, index) => ( - <li key={index}> - <span - style={{ - textDecoration: exercise.canceled ? "line-through" : "none", - }} - > - {truncateText(exercise.title, 11)} - </span> - <button onClick={() => toggleCancelExercise(exercise)}> - {exercise.canceled ? "X" : "O"} - </button> - </li> - ))} - </ul> - <div className="list-foot"> - <button - className="pick" - onClick={() => { - if (!isActive) { - onRoutineSelect({ - ...selectedRoutine, - exercises: selectedRoutine.exercises.filter( - (exercise) => !exercise.canceled - ), - }); - } - }} - > - 선택 - </button> - <button className='back' onClick={handleBackClick}>뒤로가기</button> - </div> - </div> - </div> - ) - } - </div > -); -} - -export default List; +import React, { useState, useEffect } from "react"; +import "./List.css"; +import truncateText from './truncateText'; +import { getUserRoutines, getRoutineVideos, deleteRoutine } from '../../api/routineAPI'; + +const initialRoutines = [ + { + id: 1, + name: "전신 루틴", + exercises: [ + { title: "전신 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example1", duration: 300, canceled: false }, + { title: "전신 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example2", duration: 420, canceled: false }, + { title: "전신 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example3", duration: 350, canceled: false }, + ], + }, + { + id: 2, + name: "상체 루틴", + exercises: [ + { title: "상체 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example11", duration: 360, canceled: false }, + { title: "상체 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example12", duration: 400, canceled: false }, + { title: "상체 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example13", duration: 330, canceled: false }, + { title: "상체 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example14", duration: 470, canceled: false }, + { title: "상체 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example15", duration: 380, canceled: false }, + { title: "상체 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example16", duration: 390, canceled: false }, + { title: "상체 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example17", duration: 420, canceled: false }, + { title: "상체 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example18", duration: 440, canceled: false }, + { title: "상체 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example19", duration: 350, canceled: false }, + { title: "상체 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example20", duration: 500, canceled: false }, + ], + }, + { + id: 3, + name: "하체 루틴", + exercises: [ + { title: "하체 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example21", duration: 350, canceled: false }, + { title: "하체 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example22", duration: 450, canceled: false }, + { title: "하체 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example23", duration: 400, canceled: false }, + { title: "하체 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example24", duration: 500, canceled: false }, + { title: "하체 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example25", duration: 480, canceled: false }, + { title: "하체 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example26", duration: 420, canceled: false }, + { title: "하체 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example27", duration: 430, canceled: false }, + { title: "하체 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example28", duration: 360, canceled: false }, + { title: "하체 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example29", duration: 490, canceled: false }, + { title: "하체 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example30", duration: 460, canceled: false }, + ], + }, + { + id: 4, + name: "복부 루틴", + exercises: [ + { title: "복부 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example31", duration: 400, canceled: false }, + { title: "복부 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example32", duration: 300, canceled: false }, + { title: "복부 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example33", duration: 350, canceled: false }, + { title: "복부 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example34", duration: 380, canceled: false }, + { title: "복부 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example35", duration: 320, canceled: false }, + { title: "복부 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example36", duration: 370, canceled: false }, + { title: "복부 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example37", duration: 390, canceled: false }, + { title: "복부 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example38", duration: 430, canceled: false }, + { title: "복부 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example39", duration: 480, canceled: false }, + { title: "복부 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example40", duration: 500, canceled: false }, + ], + }, + { + id: 5, + name: "유산소 루틴", + exercises: [ + { title: "유산소 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example41", duration: 600, canceled: false }, + { title: "유산소 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example42", duration: 500, canceled: false }, + { title: "유산소 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example43", duration: 550, canceled: false }, + { title: "유산소 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example44", duration: 510, canceled: false }, + { title: "유산소 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example45", duration: 560, canceled: false }, + { title: "유산소 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example46", duration: 530, canceled: false }, + { title: "유산소 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example47", duration: 480, canceled: false }, + { title: "유산소 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example48", duration: 540, canceled: false }, + { title: "유산소 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example49", duration: 490, canceled: false }, + { title: "유산소 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example50", duration: 520, canceled: false }, + ], + }, + { + id: 6, + name: "스트레칭 루틴", + exercises: [ + { title: "스트레칭 운동 1", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=example51", duration: 300, canceled: false }, + { title: "스트레칭 운동 2", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=example52", duration: 400, canceled: false }, + { title: "스트레칭 운동 3", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=example53", duration: 350, canceled: false }, + { title: "스트레칭 운동 4", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=example54", duration: 300, canceled: false }, + { title: "스트레칭 운동 5", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=example55", duration: 380, canceled: false }, + { title: "스트레칭 운동 6", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=example56", duration: 420, canceled: false }, + { title: "스트레칭 운동 7", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=example57", duration: 440, canceled: false }, + { title: "스트레칭 운동 8", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=example58", duration: 450, canceled: false }, + { title: "스트레칭 운동 9", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=example59", duration: 500, canceled: false }, + { title: "스트레칭 운동 10", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=example60", duration: 480, canceled: false }, + ], + }, + { + id: 7, + name: "등 루틴", + exercises: [ + { title: "업그레이드를 위한 새로운 등운동 [ BACK DAY ]", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail1", link: "https://www.youtube.com/watch?v=f7wFbp9BnFs", duration: 300, canceled: false }, + { title: "뚫고 나오는 등 만들고 싶으면 이거 봐", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail2", link: "https://www.youtube.com/watch?v=8SBBp65Sv3c", duration: 400, canceled: false }, + { title: "Try This Back Exercise | Back & Hamstrings Workout", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail3", link: "https://www.youtube.com/watch?v=nRzAV-CYndA", duration: 350, canceled: false }, + { title: "My Title Winning Back Training", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail4", link: "https://www.youtube.com/watch?v=5dp2FUN3mRQ", duration: 300, canceled: false }, + { title: "Back and Delts Workout", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail5", link: "https://www.youtube.com/watch?v=DjfnFj-50b4", duration: 380, canceled: false }, + { title: "INTENSE Back Workout | Mr. Olympia Derek Lunsford", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail6", link: "https://www.youtube.com/watch?v=HYngFKG5YbY&t=477s", duration: 420, canceled: false }, + { title: "Mr. Olympia BACK WORKOUT", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail7", link: "https://www.youtube.com/watch?v=HqZOWPRyck8&t=134s", duration: 440, canceled: false }, + { title: "등 운동 후 몽둥이질 당한 느낌이 나게 하는 방법들", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail8", link: "https://www.youtube.com/watch?v=OD1JMTLJp-A", duration: 450, canceled: false }, + { title: "등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) 등 운동 하는 날 반드시 시청해야 할 영상 ( feat.등 운동 루틴 풀버전 ) ", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail9", link: "https://www.youtube.com/watch?v=naxGvgl9pKg&t=1102s", duration: 500, canceled: false }, + { title: "요즘 유행하는 등 운동 루틴", thumbnail: "https://via.placeholder.com/150x100.png?text=Thumbnail10", link: "https://www.youtube.com/watch?v=XLCtwqECMrs&t=279s", duration: 480, canceled: false }, + ], + }, +]; + +function List({ onRoutineSelect, isActive }) { + + const [routines, setRoutines] = useState(initialRoutines); // 루틴 목록 + const [selectedRoutine, setSelectedRoutine] = useState(null); // 선택한 루틴 + const [modify, setModify] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + + const modalStyle = { + position: 'fixed', + top: 0, + left: 0, + width: '100%', + height: '100%', + color: '#000000', + background: 'rgba(0, 0, 0, 0.1)', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: 9999, + }; + + const modalContentStyle = { + background: 'white', + padding: '40px', + fontSize: '25px', + borderRadius: '10px', + textAlign: 'center', + boxShadow: '0 4px 10px rgba(0, 0, 0, 0.3)', + }; + + const buttonStyle = { + height: '60px', + width: '140px', + margin: '30px', + fontSize: '0.7em', + cursor: 'pointer', + }; + +const fetchRoutines = async () => { + try { + const data = await getUserRoutines(); + fetchExercises(data); + } catch (err) { + alert(err); + } +}; + +const fetchExercises = async (routines) => { + try { + const updatedRoutines = await Promise.all( + routines.map(async (rt) => { + const data = await getRoutineVideos(rt.name); + const updatedExercises = data.map((exercise) => ({ + ...exercise, + canceled: false, + })); + return { ...rt, exercises: updatedExercises }; + }) + ); + setRoutines(updatedRoutines); + } catch (err) { + alert(err); + } +}; + +useEffect(() => { + fetchRoutines(); +}, []); + +/* + 루틴 목록에서 원하는 루틴 클릭 시 해당 루틴을 selectedRoutine으로 설정하여 + 해당 루틴 운동 목록화면으로 전환 +*/ +const handleRoutineClick = (routine) => { + if (!modify) setSelectedRoutine(routine); + +}; + +/* + 뒤로가기 클릭 시 selectedRoutine 값을 null로 설정하여 루틴 목록화면으로 전환 +*/ +const handleBackClick = () => { + setSelectedRoutine(null); +}; + +const handledelete = async (name) => { + try { + await deleteRoutine(name); + setIsModalOpen(false); + } catch (err) { + alert(err); + } +}; + +/* + 토글 클릭 시 해당 운동의 루틴 제외 여부 설정 +*/ +const toggleCancelExercise = (exercise) => { + const updatedExercises = selectedRoutine.exercises.map((ex) => + ex.title === exercise.title ? { ...ex, canceled: !ex.canceled } : ex + ); + setSelectedRoutine({ ...selectedRoutine, exercises: updatedExercises }); +}; + +return ( + <div id="list-container"> + {!selectedRoutine ? ( + <div className="list-head"> + {/*selectedRoutine이 null로 루틴 목록 화면 표시*/} + <div> + <span>routine</span> + <div className="division-line"></div> + </div> + <div id="list-content"> + <ul> + {routines.map((routine) => ( + <li key={routine.id} onClick={() => handleRoutineClick(routine)}> + <span>{truncateText(routine.name, 10)}</span> + {modify && ( + <button onClick={(e) => { + e.stopPropagation(); // 이벤트 전파 중단 + setIsModalOpen(true); + }}>X</button> + + )} + {isModalOpen && ( + <div style={modalStyle}> + <div style={modalContentStyle}> + <div> + 루틴을 삭제하겠습니까? + <div> + <button style={buttonStyle} onClick={() => handledelete(routine.title)}>삭제</button> + <button style={buttonStyle} onClick={() => setIsModalOpen(false)}>취소</button> + </div> + </div> + </div> + </div> + )} + </li> + ))} + </ul> + {!modify ? (<button className='back' onClick={() => setModify(true)}>수정</button> + ) : (<button className='back' onClick={() => setModify(false)}>뒤로가기</button>)} + </div> + </div> + ) : ( + <div className="list-head"> + {/*selectedRoutine이 설정 되어 운동 목록 화면 표시*/} + <div> + <span>{selectedRoutine.name}</span> + <div className="division-line"></div> + </div> + <div id="list-content"> + <ul> + {selectedRoutine.exercises.map((exercise, index) => ( + <li key={index}> + <span + style={{ + textDecoration: exercise.canceled ? "line-through" : "none", + }} + > + {truncateText(exercise.title, 11)} + </span> + <button onClick={() => toggleCancelExercise(exercise)}> + {exercise.canceled ? "X" : "O"} + </button> + </li> + ))} + </ul> + <div className="list-foot"> + <button + className="pick" + onClick={() => { + if (!isActive) { + onRoutineSelect({ + ...selectedRoutine, + exercises: selectedRoutine.exercises.filter( + (exercise) => !exercise.canceled + ), + }); + } + }} + > + 선택 + </button> + <button className='back' onClick={handleBackClick}>뒤로가기</button> + </div> + </div> + </div> + ) + } + </div > +); +} + +export default List; diff --git a/front/src/pages/routine/Now.css b/front/src/components/routine/Now.css similarity index 94% rename from front/src/pages/routine/Now.css rename to front/src/components/routine/Now.css index 2514ef7accaa8c4c623d5f33de914750b00bd76c..477d7ec5b1b0ffc5b55f32eca96ce0464549b351 100644 --- a/front/src/pages/routine/Now.css +++ b/front/src/components/routine/Now.css @@ -1,59 +1,59 @@ -.now-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 0px; - gap: 2px; - - width: 412.48px; - height: 191.92px; - animation: 0.2s ease-in-out loadEffect1; -} - -.now-thumbnail { - display: flex; - flex-direction: row; - align-items: center; - padding: 0px; - gap: 9.59px; - - width: 412.48px; - height: 146.83px; -} - -.now-thumbnail img { - width: 150px; - height: 100px; - border-radius: 16px; - /* 둥근 모서리 적용 */ - object-fit: cover; - /* 이미지 비율 유지하며 크기 조정 */ - box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); - /* 선택 사항: 그림자 추가 */ -} - -.underbar { - display: flex; - align-items: center; - justify-items: center; - width: 420px; - height: 60px; -} - -.underbar button { - width: 110px; - height: 45px; - margin-top: 0px; - border-radius: 7px; - cursor: pointer; -} - -.now-content { - display: flex; - align-items: center; - font-size: 22px; - font-weight: 600; - width: 420px; - height: 160px; +.now-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + gap: 2px; + + width: 412.48px; + height: 191.92px; + animation: 0.2s ease-in-out loadEffect1; +} + +.now-thumbnail { + display: flex; + flex-direction: row; + align-items: center; + padding: 0px; + gap: 9.59px; + + width: 412.48px; + height: 146.83px; +} + +.now-thumbnail img { + width: 150px; + height: 100px; + border-radius: 16px; + /* 둥근 모서리 적용 */ + object-fit: cover; + /* 이미지 비율 유지하며 크기 조정 */ + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + /* 선택 사항: 그림자 추가 */ +} + +.underbar { + display: flex; + align-items: center; + justify-items: center; + width: 420px; + height: 60px; +} + +.underbar button { + width: 110px; + height: 45px; + margin-top: 0px; + border-radius: 7px; + cursor: pointer; +} + +.now-content { + display: flex; + align-items: center; + font-size: 22px; + font-weight: 600; + width: 420px; + height: 160px; } \ No newline at end of file diff --git a/front/src/pages/routine/Now.jsx b/front/src/components/routine/Now.jsx similarity index 97% rename from front/src/pages/routine/Now.jsx rename to front/src/components/routine/Now.jsx index e11ddf1e8cf227b2c9bb4c98e0dc777000fe4602..23c8c9c723c8ed133b353617db3e0892c532f046 100644 --- a/front/src/pages/routine/Now.jsx +++ b/front/src/components/routine/Now.jsx @@ -1,205 +1,205 @@ -import React, { useState, useEffect } from 'react'; -import "./Now.css"; -import truncateText from './truncateText'; -import formatTime from "./formatTime" - -function Now({ - currentVideo, - onNext, - isRest, - restSeconds, - isActive, - onPause, - isPaused, - onAddTime, - endSignal -}) { - const [exerciseTime, setExerciseTime] = useState(0); // 현재 운동 타이머 - const [restTime, setRestTime] = useState(0); // 운동 중 휴식 타이머 - const [restTimeLeft, setRestTimeLeft] = useState(restSeconds); // 현재 휴식 타이머 - const [isTakingBreak, setIsTakingBreak] = useState(false); // 운동 중 휴식 상태 - const [isModalOpen, setIsModalOpen] = useState(false); // 모달 여부 - const [isFlag, setFlag] = useState(true); - const [oneRest, setoneRest] = useState(false); - - /* - 운동 시작 전 설정한 휴식 시간으로 타이머 시간 설정 - */ - useEffect(() => { - if (!isActive) setRestTimeLeft(restSeconds); - }, [restTimeLeft]); - - /* - 마지막 운동에 대한 시간 전달 - */ - useEffect(() => { - if (endSignal) { - onAddTime(`Exercise: ${exerciseTime}`); - } - }, [exerciseTime,endSignal]); - - /* - 운동 시간 타이머 - */ - useEffect(() => { - if (isRest || isPaused || !isActive || isTakingBreak) return; - - const timer = setInterval(() => { - setExerciseTime((prev) => prev + 1); - }, 1000); - - return () => clearInterval(timer); - }, [isRest, isPaused, isActive, isTakingBreak]); - - /* - 운동 중 쉬는 시간 타이머 - */ - useEffect(() => { - if (!isTakingBreak) return; - - const timer = setInterval(() => { - setRestTime((prev) => prev + 1); - }, 1000); - - return () => clearInterval(timer); - }, [isTakingBreak]); - - /* - 쉬는 시간 타이머 - 4초 남을 시 모달 오픈 1초 남을 시 다음 운동으로 넘어감 - */ - useEffect(() => { - if (!isRest || isPaused || !isActive || isTakingBreak) return; - - const timer = setInterval(() => { - setRestTimeLeft((prev) => { - if (prev <= 1) { - onNext(); - if (!isRest && !isFlag && isActive) { - onAddTime(`Rest: ${restSeconds}`); - } else if (!isRest && isFlag && isActive) setFlag(false); - setoneRest(false); - } - if (prev <= 4) { - setIsModalOpen(true); - } - return prev - 1; - }); - }, 1000); - - return () => clearInterval(timer); - }, [isRest, isPaused, isActive, isTakingBreak]); - - /* - 다음 운동으로 넘어 갈 시 운동 시간 전달 - 휴식 종료시 쉬는 시간 전달 - 마지막 조건 문은 휴식 시간에 Next 선택을 위한 조건처리 - */ - useEffect(() => { - if (currentVideo && isRest && isActive) { - setIsModalOpen(false); - onAddTime(`Exercise: ${exerciseTime}`); - setExerciseTime(0); - setRestTimeLeft(restSeconds); - } - }, [currentVideo, isRest, isActive]); - - const handleRestStart = () => { - if (!isPaused && isActive && !oneRest) { - setIsTakingBreak(true); - setoneRest(true); - } - }; - - const handleRestStop = () => { - if (isTakingBreak) { - setIsTakingBreak(false); - onAddTime(`Rest: ${restTime}`); - setRestTime(0); - } - }; - - /* - 휴식 때 Next 클릭시 쉬는 시간 전달 및 쉬는 시간 4초로 변경 - 운동 시작 전이면 그냥 다음 영상으로 넘김 - */ - const handleRestNext = () => { - if (isActive) { - onAddTime(`Rest: ${restSeconds - restTimeLeft}`); - setFlag(true); - setRestTimeLeft(4); - } else onNext(); - }; - - return ( - <div className='now-container'> - {isTakingBreak ? ( - <div> - <div className='now-content'> - <span>Taking a Break / </span> - <span> / {formatTime(restTime)}</span> - </div> - <div className="underbar"> - <button onClick={handleRestStop}> - Go back - </button> - </div> - </div> - ) : isRest ? ( - <div> - <div className='now-content'> - <span>Rest Period / </span> - {!isModalOpen ? ( - <span>/ {formatTime(restTimeLeft)}</span> - ) : ( - <span>/ 다음 운동으로</span> - )} - </div> - <div className="underbar"> - <button className="next-button" onClick={handleRestNext}> - Next - </button> - </div> - {isModalOpen && ( - <div className="modal-backdrop"> - <div className="modal"> - <div id="modal-content"> - <span>{restTimeLeft}</span> - </div> - </div> - </div> - )} - </div> - ) : ( - <div> - <div className='now-content'> - {currentVideo?.thumbnail && ( - <div className="now-thumbnail"> - <img - src={currentVideo.thumbnail} - alt={currentVideo.title} - className="now-thumbnail" - /> - <p className="now-title">{truncateText(currentVideo.title, 20)}</p> - </div> - )} - <p>Timer: {formatTime(exerciseTime)}</p> - </div> - <div className="underbar"> - <button onClick={onNext}> - Next - </button> - <button onClick={onPause}> - {isPaused ? "Start" : "Pause"} - </button> - <button onClick={handleRestStart}> - Rest - </button> - </div> - </div> - )} - </div> - ); -} - -export default Now; +import React, { useState, useEffect } from 'react'; +import "./Now.css"; +import truncateText from './truncateText'; +import formatTime from "./formatTime" + +function Now({ + currentVideo, + onNext, + isRest, + restSeconds, + isActive, + onPause, + isPaused, + onAddTime, + endSignal +}) { + const [exerciseTime, setExerciseTime] = useState(0); // 현재 운동 타이머 + const [restTime, setRestTime] = useState(0); // 운동 중 휴식 타이머 + const [restTimeLeft, setRestTimeLeft] = useState(restSeconds); // 현재 휴식 타이머 + const [isTakingBreak, setIsTakingBreak] = useState(false); // 운동 중 휴식 상태 + const [isModalOpen, setIsModalOpen] = useState(false); // 모달 여부 + const [isFlag, setFlag] = useState(true); + const [oneRest, setoneRest] = useState(false); + + /* + 운동 시작 전 설정한 휴식 시간으로 타이머 시간 설정 + */ + useEffect(() => { + if (!isActive) setRestTimeLeft(restSeconds); + }, [restTimeLeft]); + + /* + 마지막 운동에 대한 시간 전달 + */ + useEffect(() => { + if (endSignal) { + onAddTime(`Exercise: ${exerciseTime}`); + } + }, [exerciseTime,endSignal]); + + /* + 운동 시간 타이머 + */ + useEffect(() => { + if (isRest || isPaused || !isActive || isTakingBreak) return; + + const timer = setInterval(() => { + setExerciseTime((prev) => prev + 1); + }, 1000); + + return () => clearInterval(timer); + }, [isRest, isPaused, isActive, isTakingBreak]); + + /* + 운동 중 쉬는 시간 타이머 + */ + useEffect(() => { + if (!isTakingBreak) return; + + const timer = setInterval(() => { + setRestTime((prev) => prev + 1); + }, 1000); + + return () => clearInterval(timer); + }, [isTakingBreak]); + + /* + 쉬는 시간 타이머 + 4초 남을 시 모달 오픈 1초 남을 시 다음 운동으로 넘어감 + */ + useEffect(() => { + if (!isRest || isPaused || !isActive || isTakingBreak) return; + + const timer = setInterval(() => { + setRestTimeLeft((prev) => { + if (prev <= 1) { + onNext(); + if (!isRest && !isFlag && isActive) { + onAddTime(`Rest: ${restSeconds}`); + } else if (!isRest && isFlag && isActive) setFlag(false); + setoneRest(false); + } + if (prev <= 4) { + setIsModalOpen(true); + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(timer); + }, [isRest, isPaused, isActive, isTakingBreak]); + + /* + 다음 운동으로 넘어 갈 시 운동 시간 전달 + 휴식 종료시 쉬는 시간 전달 + 마지막 조건 문은 휴식 시간에 Next 선택을 위한 조건처리 + */ + useEffect(() => { + if (currentVideo && isRest && isActive) { + setIsModalOpen(false); + onAddTime(`Exercise: ${exerciseTime}`); + setExerciseTime(0); + setRestTimeLeft(restSeconds); + } + }, [currentVideo, isRest, isActive]); + + const handleRestStart = () => { + if (!isPaused && isActive && !oneRest) { + setIsTakingBreak(true); + setoneRest(true); + } + }; + + const handleRestStop = () => { + if (isTakingBreak) { + setIsTakingBreak(false); + onAddTime(`Rest: ${restTime}`); + setRestTime(0); + } + }; + + /* + 휴식 때 Next 클릭시 쉬는 시간 전달 및 쉬는 시간 4초로 변경 + 운동 시작 전이면 그냥 다음 영상으로 넘김 + */ + const handleRestNext = () => { + if (isActive) { + onAddTime(`Rest: ${restSeconds - restTimeLeft}`); + setFlag(true); + setRestTimeLeft(4); + } else onNext(); + }; + + return ( + <div className='now-container'> + {isTakingBreak ? ( + <div> + <div className='now-content'> + <span>Taking a Break / </span> + <span> / {formatTime(restTime)}</span> + </div> + <div className="underbar"> + <button onClick={handleRestStop}> + Go back + </button> + </div> + </div> + ) : isRest ? ( + <div> + <div className='now-content'> + <span>Rest Period / </span> + {!isModalOpen ? ( + <span>/ {formatTime(restTimeLeft)}</span> + ) : ( + <span>/ 다음 운동으로</span> + )} + </div> + <div className="underbar"> + <button className="next-button" onClick={handleRestNext}> + Next + </button> + </div> + {isModalOpen && ( + <div className="modal-backdrop"> + <div className="modal"> + <div id="modal-content"> + <span>{restTimeLeft}</span> + </div> + </div> + </div> + )} + </div> + ) : ( + <div> + <div className='now-content'> + {currentVideo?.thumbnail && ( + <div className="now-thumbnail"> + <img + src={currentVideo.thumbnail} + alt={currentVideo.title} + className="now-thumbnail" + /> + <p className="now-title">{truncateText(currentVideo.title, 20)}</p> + </div> + )} + <p>Timer: {formatTime(exerciseTime)}</p> + </div> + <div className="underbar"> + <button onClick={onNext}> + Next + </button> + <button onClick={onPause}> + {isPaused ? "Start" : "Pause"} + </button> + <button onClick={handleRestStart}> + Rest + </button> + </div> + </div> + )} + </div> + ); +} + +export default Now; diff --git a/front/src/pages/routine/progress.jsx b/front/src/components/routine/Progress.jsx similarity index 95% rename from front/src/pages/routine/progress.jsx rename to front/src/components/routine/Progress.jsx index 0ead796075beefa4088d2265e5ed7e4d9e39495c..6a4eac3b732cf766001fe466f1851804769d6370 100644 --- a/front/src/pages/routine/progress.jsx +++ b/front/src/components/routine/Progress.jsx @@ -1,94 +1,94 @@ -import React, { useState, useEffect } from "react"; -import "./Progress.css"; -import { addExerciseRecord } from './api'; - -function Progress({ currentVideo, times, endSignal, onendClick }) { - const [restTimes, setRestTimes] = useState([]); - const [exerciseTimes, setExerciseTimes] = useState([]); - const [allTimes, setAllTimes] = useState([]); - const [isModalOpen, setIsModalOpen] = useState(false); - - const colors = ["#242834", "#CFFF5E", "#B87EED", "#8C7DFF", "#FFFFFF"]; - const getRandomColor = () => colors[Math.floor(Math.random() * colors.length)]; - - // 누적 시간 상태 - const [totals, setTotals] = useState({ restTotal: 0, exerciseTotal: 0, allTotal: 0 }); - - useEffect(() => { - if (typeof times === "string") { - const timeValue = parseInt(times.split(":")[1].trim(), 10); - const randomColor = getRandomColor(); - if (times.startsWith("Rest:")) { - setRestTimes((prev) => [...prev, timeValue]); - setAllTimes((prev) => [...prev, { type: "Rest", value: timeValue, color: randomColor, id: Date.now() }]); - } else if (times.startsWith("Exercise:")) { - const tmp = new Date(); - const { canceled, ...recordvideo } = { - ...currentVideo, - date: tmp.toISOString().split("T")[0], - }; - addExerciseRecord(recordvideo) - .then(() => { - setExerciseTimes((prev) => [...prev, timeValue]); - setAllTimes((prev) => [...prev, { type: "Exercise", value: timeValue, color: randomColor, id: Date.now() }]); - }) - .catch((err) => { - alert(err); - }); - } - } - }, [times]); - - // 총합 계산 - useEffect(() => { - const restTotal = restTimes.reduce((sum, time) => sum + time, 0); - const exerciseTotal = exerciseTimes.reduce((sum, time) => sum + time, 0); - const allTotal = restTotal + exerciseTotal; - - setTotals({ restTotal, exerciseTotal, allTotal }); - }, [restTimes, exerciseTimes]); - - // endSignal에 따라 모달 오픈 - useEffect(() => { - if (endSignal) setIsModalOpen(true); - }, [endSignal]); - - const calculateHeight = (value) => 2 * ((value + 60) / 60 + 1); - - return ( - <div className="progress-container"> - {[...allTimes].reverse().map((item, index) => { - const height = calculateHeight(item.value); - const isLastBlock = index === 0; - - return ( - <div - key={item.id} - className={`progress-block ${isLastBlock ? "new-block" : ""}`} - style={{ - height: `${height}px`, - backgroundColor: item.color, - }} - /> - ); - })} - {(isModalOpen && endSignal) && ( - <div className="modal-backdrop"> - <div className="modal"> - <div className="modal-content"> - <p> - 오늘은 총{" "} - {`${Math.floor(totals.allTotal / 60)}분 ${totals.allTotal % 60}초`}{" "} - 동안 운동하셨고, 그 중 {`${Math.round((totals.exerciseTotal / totals.allTotal) * 100)}%`} 동안 - 운동하셨습니다! - </p> - <button onClick={() => { setIsModalOpen(false); onendClick(); }}>확인</button> - </div> - </div> - </div> - )} - </div> - ); -} - -export default Progress; +import React, { useState, useEffect } from "react"; +import "./progress.css"; +import { addExerciseRecord } from '../../api/routineAPI'; + +function Progress({ currentVideo, times, endSignal, onendClick }) { + const [restTimes, setRestTimes] = useState([]); + const [exerciseTimes, setExerciseTimes] = useState([]); + const [allTimes, setAllTimes] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + + const colors = ["#242834", "#CFFF5E", "#B87EED", "#8C7DFF", "#FFFFFF"]; + const getRandomColor = () => colors[Math.floor(Math.random() * colors.length)]; + + // 누적 시간 상태 + const [totals, setTotals] = useState({ restTotal: 0, exerciseTotal: 0, allTotal: 0 }); + + useEffect(() => { + if (typeof times === "string") { + const timeValue = parseInt(times.split(":")[1].trim(), 10); + const randomColor = getRandomColor(); + if (times.startsWith("Rest:")) { + setRestTimes((prev) => [...prev, timeValue]); + setAllTimes((prev) => [...prev, { type: "Rest", value: timeValue, color: randomColor, id: Date.now() }]); + } else if (times.startsWith("Exercise:")) { + const tmp = new Date(); + const { canceled, ...recordvideo } = { + ...currentVideo, + date: tmp.toISOString().split("T")[0], + }; + addExerciseRecord(recordvideo) + .then(() => { + setExerciseTimes((prev) => [...prev, timeValue]); + setAllTimes((prev) => [...prev, { type: "Exercise", value: timeValue, color: randomColor, id: Date.now() }]); + }) + .catch((err) => { + alert(err); + }); + } + } + }, [times]); + + // 총합 계산 + useEffect(() => { + const restTotal = restTimes.reduce((sum, time) => sum + time, 0); + const exerciseTotal = exerciseTimes.reduce((sum, time) => sum + time, 0); + const allTotal = restTotal + exerciseTotal; + + setTotals({ restTotal, exerciseTotal, allTotal }); + }, [restTimes, exerciseTimes]); + + // endSignal에 따라 모달 오픈 + useEffect(() => { + if (endSignal) setIsModalOpen(true); + }, [endSignal]); + + const calculateHeight = (value) => 2 * ((value + 60) / 60 + 1); + + return ( + <div className="progress-container"> + {[...allTimes].reverse().map((item, index) => { + const height = calculateHeight(item.value); + const isLastBlock = index === 0; + + return ( + <div + key={item.id} + className={`progress-block ${isLastBlock ? "new-block" : ""}`} + style={{ + height: `${height}px`, + backgroundColor: item.color, + }} + /> + ); + })} + {(isModalOpen && endSignal) && ( + <div className="modal-backdrop"> + <div className="modal"> + <div className="modal-content"> + <p> + 오늘은 총{" "} + {`${Math.floor(totals.allTotal / 60)}분 ${totals.allTotal % 60}초`}{" "} + 동안 운동하셨고, 그 중 {`${Math.round((totals.exerciseTotal / totals.allTotal) * 100)}%`} 동안 + 운동하셨습니다! + </p> + <button onClick={() => { setIsModalOpen(false); onendClick(); }}>확인</button> + </div> + </div> + </div> + )} + </div> + ); +} + +export default Progress; diff --git a/front/src/pages/routine/Rest.css b/front/src/components/routine/Rest.css similarity index 93% rename from front/src/pages/routine/Rest.css rename to front/src/components/routine/Rest.css index 8ca6b2a58999d2c34947d16f04763960b3bcddc6..958fbdf7cdbe9756dc245d2f38812593074c37b4 100644 --- a/front/src/pages/routine/Rest.css +++ b/front/src/components/routine/Rest.css @@ -1,50 +1,50 @@ -#rest-container { - display: flex; - flex-direction: column; - align-items: center; -} - -.comment { - width: 240px; - height: 39px; - font-weight: 600; - font-size: 30px; - line-height: 38px; - - text-align: center; - margin: 0; - color: #000000; -} - -#rest-container div { - display: flex; - flex-direction: column; - align-items: center; - - width: 178px; - height: 80px; -} - -#rest-container div input { - width: 178px; - height: 40px; - background: #DFDFE4; - border-radius: 15px; - text-align: center; - font-size: 20px; - - flex: none; -} - -#rest-container div span { - width: 178px; - height: 35px; - - font-weight: 600; - font-size: 25px; - line-height: 38px; - - text-align: center; - - color: #000000; +#rest-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.comment { + width: 240px; + height: 39px; + font-weight: 600; + font-size: 30px; + line-height: 38px; + + text-align: center; + margin: 0; + color: #000000; +} + +#rest-container div { + display: flex; + flex-direction: column; + align-items: center; + + width: 178px; + height: 80px; +} + +#rest-container div input { + width: 178px; + height: 40px; + background: #DFDFE4; + border-radius: 15px; + text-align: center; + font-size: 20px; + + flex: none; +} + +#rest-container div span { + width: 178px; + height: 35px; + + font-weight: 600; + font-size: 25px; + line-height: 38px; + + text-align: center; + + color: #000000; } \ No newline at end of file diff --git a/front/src/pages/routine/Rest.jsx b/front/src/components/routine/Rest.jsx similarity index 97% rename from front/src/pages/routine/Rest.jsx rename to front/src/components/routine/Rest.jsx index 907c76005a43cbcde70e5b353334ea868df018a9..bd95ff730ed1fc9fa2e86b83aaec8de4309c8df0 100644 --- a/front/src/pages/routine/Rest.jsx +++ b/front/src/components/routine/Rest.jsx @@ -1,52 +1,52 @@ -import React, { useState } from "react"; -import './Rest.css'; - -function Rest({ onRestChange, isActive }) { - const [seconds, setSeconds] = useState(""); // 운동 간 쉬는 시간 - const [warning, setWaring] = useState(false); // 쉬는 시간이 300 초과인지 확인 여부 - - /* - 설정 시간에 따른 출력 텍스트 설정 - */ - const getDisplayText = (seconds) => { - if (isActive) return "파이팅 해야지"; - if (warning) return "덜 쉬어봐요"; - if (seconds <= 0) return ""; - if (seconds >= 0 && seconds <= 60) return "파이팅 넘치네요"; - if (seconds > 60 && seconds <= 120) return "오늘도 아자아자"; - if (seconds > 120 && seconds <= 180) return "오늘은 느긋하게"; - if (seconds > 180) return "쉬려고 왔나요?"; - }; - - /* - 운동 시작 전 쉬는 시간 설정 및 타이머 시간 설정을 위한 값 전달 - */ - const handleInputChange = (e) => { - const value = e.target.value; - if (/^\d*$/.test(value)&&!isActive) { - const restValue = value === 0 ? "" : value; - if (restValue <= 300 || value === "") { - setWaring(false); - setSeconds(value); // 값이 300 이하일 때만 업데이트 - onRestChange(restValue || 0); // 부모에 변경 사항 전달 - } else setWaring(true); - } - }; - - return ( - <div id="rest-container"> - <h1 className="comment">{getDisplayText(seconds)}</h1> - <div> - <input - type="number" - value={seconds} - onChange={handleInputChange} - placeholder="rest period" - /> - <span>rest period</span> - </div> - </div> - ); -}; - -export default Rest; +import React, { useState } from "react"; +import './Rest.css'; + +function Rest({ onRestChange, isActive }) { + const [seconds, setSeconds] = useState(""); // 운동 간 쉬는 시간 + const [warning, setWaring] = useState(false); // 쉬는 시간이 300 초과인지 확인 여부 + + /* + 설정 시간에 따른 출력 텍스트 설정 + */ + const getDisplayText = (seconds) => { + if (isActive) return "파이팅 해야지"; + if (warning) return "덜 쉬어봐요"; + if (seconds <= 0) return ""; + if (seconds >= 0 && seconds <= 60) return "파이팅 넘치네요"; + if (seconds > 60 && seconds <= 120) return "오늘도 아자아자"; + if (seconds > 120 && seconds <= 180) return "오늘은 느긋하게"; + if (seconds > 180) return "쉬려고 왔나요?"; + }; + + /* + 운동 시작 전 쉬는 시간 설정 및 타이머 시간 설정을 위한 값 전달 + */ + const handleInputChange = (e) => { + const value = e.target.value; + if (/^\d*$/.test(value)&&!isActive) { + const restValue = value === 0 ? "" : value; + if (restValue <= 300 || value === "") { + setWaring(false); + setSeconds(value); // 값이 300 이하일 때만 업데이트 + onRestChange(restValue || 0); // 부모에 변경 사항 전달 + } else setWaring(true); + } + }; + + return ( + <div id="rest-container"> + <h1 className="comment">{getDisplayText(seconds)}</h1> + <div> + <input + type="number" + value={seconds} + onChange={handleInputChange} + placeholder="rest period" + /> + <span>rest period</span> + </div> + </div> + ); +}; + +export default Rest; diff --git a/front/src/pages/routine/Start.css b/front/src/components/routine/Start.css similarity index 94% rename from front/src/pages/routine/Start.css rename to front/src/components/routine/Start.css index 0cc6378eb2862b921c9b3345d4e97d25e656cf4e..068e234c09f243ae6eb1890f664b47be0e1551ed 100644 --- a/front/src/pages/routine/Start.css +++ b/front/src/components/routine/Start.css @@ -1,130 +1,130 @@ -#start-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - width: 205px; - height: 135px; -} - -.today { - width: 210px; - height: 39px; - - font-weight: 600; - font-size: 32px; - line-height: 50px; - - text-align: center; - - color: #181818; - align-self: stretch; -} - -#start-container button { - width: 177px; - height: 76px; - - border-radius: 22px; - cursor: pointer; - - flex: none; -} - -.stop-button { - background: #B87EED; -} - -.start-button { - background: #242834; -} - -.start-button:hover { - background-color: #616368; -} - -.stop-button:hover { - background-color: #d9b5fc; -} - -#start-container span { - width: 92px; - height: 39px; - - font-family: 'Roboto'; - font-style: normal; - font-weight: 600; - font-size: 35px; - - color: #DFDFE4; -} - -.modal-backdrop { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - color: #000000; - background: rgba(0, 0, 0, 0.6); - /* 반투명 어두운 배경 */ - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; -} - -.modal { - background: white; - padding: 40px; - border-radius: 10px; - text-align: center; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); -} - -#modal-contents { - height: 200px; - width: 400px; - color: #000000;; - font-size: 30px; - font-weight: 600; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -#modal-content { - display: flex; - justify-content: center; - align-items: center; -} - -#modal-content p { - font-size: 20px; - text-align: center; - margin-bottom: 20px; -} - -#modal-contents button { - height: 60px; - width: 140px; - margin: 30px; - font-size: 0.7em; - cursor: pointer; -} - -#modal-content span { - font-weight: 600; - font-size: 60px; -} - - -#modal-contents span { - width: 500px; - color: #000000; - font-weight: 600; - font-size: 40px; - font-stretch: expanded; +#start-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: 205px; + height: 135px; +} + +.today { + width: 210px; + height: 39px; + + font-weight: 600; + font-size: 32px; + line-height: 50px; + + text-align: center; + + color: #181818; + align-self: stretch; +} + +#start-container button { + width: 177px; + height: 76px; + + border-radius: 22px; + cursor: pointer; + + flex: none; +} + +.stop-button { + background: #B87EED; +} + +.start-button { + background: #242834; +} + +.start-button:hover { + background-color: #616368; +} + +.stop-button:hover { + background-color: #d9b5fc; +} + +#start-container span { + width: 92px; + height: 39px; + + font-family: 'Roboto'; + font-style: normal; + font-weight: 600; + font-size: 35px; + + color: #DFDFE4; +} + +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: #000000; + background: rgba(0, 0, 0, 0.6); + /* 반투명 어두운 배경 */ + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.modal { + background: white; + padding: 40px; + border-radius: 10px; + text-align: center; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +#modal-contents { + height: 200px; + width: 400px; + color: #000000;; + font-size: 30px; + font-weight: 600; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +#modal-content { + display: flex; + justify-content: center; + align-items: center; +} + +#modal-content p { + font-size: 20px; + text-align: center; + margin-bottom: 20px; +} + +#modal-contents button { + height: 60px; + width: 140px; + margin: 30px; + font-size: 0.7em; + cursor: pointer; +} + +#modal-content span { + font-weight: 600; + font-size: 60px; +} + + +#modal-contents span { + width: 500px; + color: #000000; + font-weight: 600; + font-size: 40px; + font-stretch: expanded; } \ No newline at end of file diff --git a/front/src/pages/routine/Start.jsx b/front/src/components/routine/Start.jsx similarity index 96% rename from front/src/pages/routine/Start.jsx rename to front/src/components/routine/Start.jsx index 8eb4fc6ea2e5596a2e5be00cce2aea254e0eb778..d89ff750d7c72cb4326fd617bb4870b20c6c0b1b 100644 --- a/front/src/pages/routine/Start.jsx +++ b/front/src/components/routine/Start.jsx @@ -1,100 +1,100 @@ -import React, { useState } from 'react'; -import './Start.css'; - -function Start({ ButtonClick, isActive, hasRoutine, endSignal }) { - const [isModalOpen, setIsModalOpen] = useState(false); // 모달 여부 - const [countdown, setCountdown] = useState(null); // 카운트다운 상태 - - const message = ["운동을 시작하겠습니다", "1", "2", "3"]; - - /* - 금일 날짜 설정 - */ - const getTodayDate = () => { - const today = new Date(); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); - const day = String(today.getDate()).padStart(2, '0'); - return `${year}/${month}/${day}`; - }; - - const handleConfirm = () => { - if (!hasRoutine) { - setIsModalOpen(false); // 운동 선택 요청 모달 닫기 - } else { - startCountdown(); // 카운트다운 시작 - } - }; - - - /* - 운동 시작 전이면 모달 표시 - */ - const handleStartClick = () => { - if (!isActive&&!endSignal) setIsModalOpen(true); - }; - - /* - 운동 시작 전 카운트 다운 - 카운트 다운 완료시 모달 종료 후 운동 시작 여부 전달 - */ - const startCountdown = () => { - let count = 3; - setCountdown(count); - - const interval = setInterval(() => { - count -= 1; - setCountdown(count); - if (count === -1) { - clearInterval(interval); - setIsModalOpen(false); - ButtonClick(); - } - }, 1000); - }; - - /* - 취소 클릭 시 모달 종료 - */ - const handleCancel = () => { - setIsModalOpen(false); - }; - - return ( - <div id="start-container"> - <div className="today">{getTodayDate()}</div> - <button - className={`${isActive ? 'stop-button' : 'start-button'}`} - onClick={handleStartClick} - > - <span>{isActive ? 'STOP' : 'START'} </span> - </button> - - {isModalOpen && ( - <div className="modal-backdrop"> - <div className="modal"> - <div id="modal-contents"> - {!hasRoutine ? ( - <p>운동을 선택해 주세요!</p> - ) : countdown !== null ? ( - <span>{message[countdown]}</span> - ) : ( - <p>운동을 시작하겠습니까?</p> - )} - <div> - {countdown === null && ( - <> - <button onClick={handleConfirm}>확인</button> - {hasRoutine && <button onClick={handleCancel}>취소</button>} - </> - )} - </div> - </div> - </div> - </div> - )} - </div> - ); -} - -export default Start; +import React, { useState } from 'react'; +import './Start.css'; + +function Start({ ButtonClick, isActive, hasRoutine, endSignal }) { + const [isModalOpen, setIsModalOpen] = useState(false); // 모달 여부 + const [countdown, setCountdown] = useState(null); // 카운트다운 상태 + + const message = ["운동을 시작하겠습니다", "1", "2", "3"]; + + /* + 금일 날짜 설정 + */ + const getTodayDate = () => { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + return `${year}/${month}/${day}`; + }; + + const handleConfirm = () => { + if (!hasRoutine) { + setIsModalOpen(false); // 운동 선택 요청 모달 닫기 + } else { + startCountdown(); // 카운트다운 시작 + } + }; + + + /* + 운동 시작 전이면 모달 표시 + */ + const handleStartClick = () => { + if (!isActive&&!endSignal) setIsModalOpen(true); + }; + + /* + 운동 시작 전 카운트 다운 + 카운트 다운 완료시 모달 종료 후 운동 시작 여부 전달 + */ + const startCountdown = () => { + let count = 3; + setCountdown(count); + + const interval = setInterval(() => { + count -= 1; + setCountdown(count); + if (count === -1) { + clearInterval(interval); + setIsModalOpen(false); + ButtonClick(); + } + }, 1000); + }; + + /* + 취소 클릭 시 모달 종료 + */ + const handleCancel = () => { + setIsModalOpen(false); + }; + + return ( + <div id="start-container"> + <div className="today">{getTodayDate()}</div> + <button + className={`${isActive ? 'stop-button' : 'start-button'}`} + onClick={handleStartClick} + > + <span>{isActive ? 'STOP' : 'START'} </span> + </button> + + {isModalOpen && ( + <div className="modal-backdrop"> + <div className="modal"> + <div id="modal-contents"> + {!hasRoutine ? ( + <p>운동을 선택해 주세요!</p> + ) : countdown !== null ? ( + <span>{message[countdown]}</span> + ) : ( + <p>운동을 시작하겠습니까?</p> + )} + <div> + {countdown === null && ( + <> + <button onClick={handleConfirm}>확인</button> + {hasRoutine && <button onClick={handleCancel}>취소</button>} + </> + )} + </div> + </div> + </div> + </div> + )} + </div> + ); +} + +export default Start; diff --git a/front/src/pages/routine/Timer.css b/front/src/components/routine/Timer.css similarity index 93% rename from front/src/pages/routine/Timer.css rename to front/src/components/routine/Timer.css index 2bc3fc6e1b8a4084cbd6bbaa6a3c97c908815af1..e5a7433159b8648d279dc2e71510ac396fadcd50 100644 --- a/front/src/pages/routine/Timer.css +++ b/front/src/components/routine/Timer.css @@ -1,56 +1,56 @@ -#timer-container { - display: flex; - flex-direction: column; - align-items: center; - - width: 241.73px; - height: 218.71px; -} - -.time-text { - width: 200px; - height: 55px; - text-align: center; - - font-weight: 300; - font-size: 40px; - line-height: 55px; - - letter-spacing: 0.01em; - font-weight: 600; -} - -.circle { - display: flex; - padding: 0px; - width: 200px; - height: 200px; -} - -.inside-circle { - cx: 100; - cy: 80; - r: 60; - stroke: #2E2E2E; - stroke-width: 20; - fill: none; -} - -.outside-circle { - cx: 60; - cy: 100; - r: 60; - fill: none; - stroke-dasharray: 376.99; - stroke-linecap: round; -} - -.time-over { - color: #B87EED; - stroke: #B87EED; -} - -.time-off { - color: #8C7DFF; - stroke: #6C63FF; +#timer-container { + display: flex; + flex-direction: column; + align-items: center; + + width: 241.73px; + height: 218.71px; +} + +.time-text { + width: 200px; + height: 55px; + text-align: center; + + font-weight: 300; + font-size: 40px; + line-height: 55px; + + letter-spacing: 0.01em; + font-weight: 600; +} + +.circle { + display: flex; + padding: 0px; + width: 200px; + height: 200px; +} + +.inside-circle { + cx: 100; + cy: 80; + r: 60; + stroke: #2E2E2E; + stroke-width: 20; + fill: none; +} + +.outside-circle { + cx: 60; + cy: 100; + r: 60; + fill: none; + stroke-dasharray: 376.99; + stroke-linecap: round; +} + +.time-over { + color: #B87EED; + stroke: #B87EED; +} + +.time-off { + color: #8C7DFF; + stroke: #6C63FF; } \ No newline at end of file diff --git a/front/src/pages/routine/Timer.jsx b/front/src/components/routine/Timer.jsx similarity index 97% rename from front/src/pages/routine/Timer.jsx rename to front/src/components/routine/Timer.jsx index 2ce7cd313fb126ec9b4471b3a74a251e9ab93b28..3a767d526640d5ff49f86cbb981b7a42b847eb2e 100644 --- a/front/src/pages/routine/Timer.jsx +++ b/front/src/components/routine/Timer.jsx @@ -1,53 +1,53 @@ -import React, { useState, useEffect } from "react"; -import "./Timer.css"; -import formatTime from "./formatTime" - -function Timer({ duration, isActive }) { - const [timeLeft, setTimeLeft] = useState(duration); // 남은 시간 - const [progress, setProgress] = useState(0); // 타이머 진행도 - const [timeover, setTimeover] = useState(0); // 타이머 종료 후 타이머 크기 증가를 위한 가중치 - - /* - 운동 시작 시 타이머 시간 감소 - */ - useEffect(() => { - if (!isActive) return; - const timerInterval = setInterval(() => { - setTimeLeft((prev) => { - if (prev <= 0) setTimeover((prevTimeover) => prevTimeover + 0.2); - return prev - 1; - }); - }, 1000); - return () => clearInterval(timerInterval); - }, [isActive]); - - /* - 초기 타이머 시간 설정 - */ - useEffect(() => { - setTimeLeft(duration); - }, [duration]); - - /* - 타이머 진행도 업데이트 - */ - useEffect(() => { - setProgress(((duration - timeLeft) / duration) * 100); - }, [timeLeft, duration]); - - return ( - <div id="timer-container"> - <svg className="circle"> - <circle className="inside-circle" /> - <circle className={`outside-circle ${timeLeft <= 0 ? 'time-over' : 'time-off'}`} - strokeDashoffset={376.99 - (progress / 100) * 376.99} - transform="rotate(-90 70 70)" - strokeWidth={20 + timeover} - /> - </svg> - <div className={`time-text ${timeLeft <= 0 ? 'time-over' : 'time-off'}`}>{formatTime(timeLeft)}</div> - </div> - ); -}; - -export default Timer; +import React, { useState, useEffect } from "react"; +import "./Timer.css"; +import formatTime from "./formatTime" + +function Timer({ duration, isActive }) { + const [timeLeft, setTimeLeft] = useState(duration); // 남은 시간 + const [progress, setProgress] = useState(0); // 타이머 진행도 + const [timeover, setTimeover] = useState(0); // 타이머 종료 후 타이머 크기 증가를 위한 가중치 + + /* + 운동 시작 시 타이머 시간 감소 + */ + useEffect(() => { + if (!isActive) return; + const timerInterval = setInterval(() => { + setTimeLeft((prev) => { + if (prev <= 0) setTimeover((prevTimeover) => prevTimeover + 0.2); + return prev - 1; + }); + }, 1000); + return () => clearInterval(timerInterval); + }, [isActive]); + + /* + 초기 타이머 시간 설정 + */ + useEffect(() => { + setTimeLeft(duration); + }, [duration]); + + /* + 타이머 진행도 업데이트 + */ + useEffect(() => { + setProgress(((duration - timeLeft) / duration) * 100); + }, [timeLeft, duration]); + + return ( + <div id="timer-container"> + <svg className="circle"> + <circle className="inside-circle" /> + <circle className={`outside-circle ${timeLeft <= 0 ? 'time-over' : 'time-off'}`} + strokeDashoffset={376.99 - (progress / 100) * 376.99} + transform="rotate(-90 70 70)" + strokeWidth={20 + timeover} + /> + </svg> + <div className={`time-text ${timeLeft <= 0 ? 'time-over' : 'time-off'}`}>{formatTime(timeLeft)}</div> + </div> + ); +}; + +export default Timer; diff --git a/front/src/pages/routine/Video.css b/front/src/components/routine/Video.css similarity index 94% rename from front/src/pages/routine/Video.css rename to front/src/components/routine/Video.css index 8381711ae05da17903ef3e37fa2229474a84e18d..f21a59c390f1b3adcc660af29f06cad534e403da 100644 --- a/front/src/pages/routine/Video.css +++ b/front/src/components/routine/Video.css @@ -1,99 +1,99 @@ -#video-container { - display: flex; - flex-direction: column; - align-items: center; - padding: 0px; - gap: 19.19px; - - width: 800.71px; - height: 90%; - overflow-y: auto; - -ms-overflow-style: none; - box-sizing: border-box; - animation: 0.2s ease-in-out loadEffect1; - -} - -#video-container::-webkit-scrollbar { - display: none; -} - - -.video-info { - display: flex; - align-items: center; - padding: 20px; - gap: 8.67px; - - cursor: pointer; - color: #000000; - width: 600.71px; - height: 126.62px; - - background: #DFDFE4; - border-radius: 33.5742px; - - cursor: grab; - transition: transform 0.2s ease, background 0.2s ease; - -} - -.video-info:active { - cursor: grabbing; - background: #e0e0e0; -} - -.video-info.dragging { - opacity: 0.6; - background: #e0f7fa; - transform: scale(1.05); -} - -.video-info img { - width: 120px; - height: 80px; - border-radius: 16px; - object-fit: cover; - box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); -} - - -.rest-period { - display: flex; - justify-content: center; - gap: 9.59px; - padding: 10px; - width: 600.71px; - height: 25.04px; - - background: #8C7DFF; - border: 2px solid #B87EED; - border-radius: 14.389px; -} - -.highlighted { - display: flex; - align-items: center; - padding: 20px; - gap: 8.67px; - - cursor: pointer; - color: #000000; - width: 550.71px; - height: 126.62px; - - background: #FFFFFF; - border-radius: 33.5742px; - transition: transform 0.2s ease, background 0.2s ease; - background-color: #f0f8ff; - transform: scale(1.15); - border-color: #007acc; -} - -.highlighted-rest { - background: #ffe5b4; - border: 2px solid #ff9800; - font-weight: bold; - transform: scale(1.1); - transition: all 0.2s ease; +#video-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 0px; + gap: 19.19px; + + width: 800.71px; + height: 90%; + overflow-y: auto; + -ms-overflow-style: none; + box-sizing: border-box; + animation: 0.2s ease-in-out loadEffect1; + +} + +#video-container::-webkit-scrollbar { + display: none; +} + + +.video-info { + display: flex; + align-items: center; + padding: 20px; + gap: 8.67px; + + cursor: pointer; + color: #000000; + width: 600.71px; + height: 126.62px; + + background: #DFDFE4; + border-radius: 33.5742px; + + cursor: grab; + transition: transform 0.2s ease, background 0.2s ease; + +} + +.video-info:active { + cursor: grabbing; + background: #e0e0e0; +} + +.video-info.dragging { + opacity: 0.6; + background: #e0f7fa; + transform: scale(1.05); +} + +.video-info img { + width: 120px; + height: 80px; + border-radius: 16px; + object-fit: cover; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); +} + + +.rest-period { + display: flex; + justify-content: center; + gap: 9.59px; + padding: 10px; + width: 600.71px; + height: 25.04px; + + background: #8C7DFF; + border: 2px solid #B87EED; + border-radius: 14.389px; +} + +.highlighted { + display: flex; + align-items: center; + padding: 20px; + gap: 8.67px; + + cursor: pointer; + color: #000000; + width: 550.71px; + height: 126.62px; + + background: #FFFFFF; + border-radius: 33.5742px; + transition: transform 0.2s ease, background 0.2s ease; + background-color: #f0f8ff; + transform: scale(1.15); + border-color: #007acc; +} + +.highlighted-rest { + background: #ffe5b4; + border: 2px solid #ff9800; + font-weight: bold; + transform: scale(1.1); + transition: all 0.2s ease; } \ No newline at end of file diff --git a/front/src/pages/routine/video.jsx b/front/src/components/routine/Video.jsx similarity index 97% rename from front/src/pages/routine/video.jsx rename to front/src/components/routine/Video.jsx index f818b285fbf9ff398f4003996b20dd8e8793196e..e3299ec1d21286e325f6610bc72a6d07a01ea2b1 100644 --- a/front/src/pages/routine/video.jsx +++ b/front/src/components/routine/Video.jsx @@ -1,125 +1,125 @@ -import React, { useState, useEffect, useRef } from "react"; -import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; -import truncateText from './truncateText'; -import "./Video.css"; - -function Video({ - routine, - onRoutineChange, - onVideoClick, - isActive, - currentIndex, - isRest, - restSeconds, -}) { - const [exercises, setExercises] = useState(routine.exercises); // 루틴 운동 목록 - const [isDragging, setIsDragging] = useState(false); // 드래그 여부 - const videoRefs = useRef([]); - - useEffect(() => { - setExercises(routine.exercises); - }, [routine]); - - /* - 운동 변경시 해당 운동 화면 중앙으로 위치 - */ - useEffect(() => { - const targetIndex = isRest ? currentIndex - 1 : currentIndex; - if (videoRefs.current[targetIndex]) { - videoRefs.current[targetIndex].scrollIntoView({ - behavior: "smooth", - block: "center", - }); - } - - }, [currentIndex, isRest]); - - const handleDragStart = () => { - setIsDragging(true); - }; - - /* - 드래그 종료시 드래그 한 블록을 놓은 위치로 순서 변경 - 선택한 블록을 updateExercises에서 제거하여 movedItem에 저장 - moveItem을 놓은 위치에 삽입하여 Exercises로 최종 업데이트 - */ - const handleDragEnd = (result) => { - setIsDragging(false); - - if (!result.destination) return; - - const updatedExercises = Array.from(exercises); - const [movedItem] = updatedExercises.splice(result.source.index, 1); - updatedExercises.splice(result.destination.index, 0, movedItem); - setExercises(updatedExercises); - onRoutineChange(updatedExercises); - }; - - /* - 운동 클릭시 해당 영상 링크 오픈 - */ - const handleVideoClick = (exercise) => { - window.open(exercise.link, "_blank", "noopener,noreferrer"); - if (!isActive) onVideoClick(exercise); - }; - - return ( - <DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}> - <Droppable droppableId="video-list"> - {(provided) => ( - <div - id="video-container" - ref={provided.innerRef} - {...provided.droppableProps} - > - {exercises.map((exercise, index) => ( - <React.Fragment key={exercise.title}> - <Draggable - draggableId={exercise.title} - index={index} - isDragDisabled={isActive} - > - {(provided) => ( - <div - ref={(el) => { - provided.innerRef(el); - videoRefs.current[index] = el; - }} - {...provided.draggableProps} - {...provided.dragHandleProps} - onClick={() => handleVideoClick(exercise)} - className={`video-info ${currentIndex === index && !isRest - ? "highlighted" - : "" - }`} - > - <img src={exercise.thumbnail} alt={exercise.title} /> - <div className="video-title">{truncateText(exercise.title, 80)}</div> - </div> - )} - </Draggable> - {index < exercises.length - 1 && ( - <div - className={`rest-period ${currentIndex - 1 === index && isRest - ? "highlighted-rest" - : "" - }`} - style={{ - visibility: isDragging ? "hidden" : "visible", - }} - - > - {restSeconds}초 - Rest Period - </div> - )} - </React.Fragment> - ))} - {provided.placeholder} - </div> - )} - </Droppable> - </DragDropContext> - ); -} - -export default Video; +import React, { useState, useEffect, useRef } from "react"; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import truncateText from './truncateText'; +import "./Video.css"; + +function Video({ + routine, + onRoutineChange, + onVideoClick, + isActive, + currentIndex, + isRest, + restSeconds, +}) { + const [exercises, setExercises] = useState(routine.exercises); // 루틴 운동 목록 + const [isDragging, setIsDragging] = useState(false); // 드래그 여부 + const videoRefs = useRef([]); + + useEffect(() => { + setExercises(routine.exercises); + }, [routine]); + + /* + 운동 변경시 해당 운동 화면 중앙으로 위치 + */ + useEffect(() => { + const targetIndex = isRest ? currentIndex - 1 : currentIndex; + if (videoRefs.current[targetIndex]) { + videoRefs.current[targetIndex].scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } + + }, [currentIndex, isRest]); + + const handleDragStart = () => { + setIsDragging(true); + }; + + /* + 드래그 종료시 드래그 한 블록을 놓은 위치로 순서 변경 + 선택한 블록을 updateExercises에서 제거하여 movedItem에 저장 + moveItem을 놓은 위치에 삽입하여 Exercises로 최종 업데이트 + */ + const handleDragEnd = (result) => { + setIsDragging(false); + + if (!result.destination) return; + + const updatedExercises = Array.from(exercises); + const [movedItem] = updatedExercises.splice(result.source.index, 1); + updatedExercises.splice(result.destination.index, 0, movedItem); + setExercises(updatedExercises); + onRoutineChange(updatedExercises); + }; + + /* + 운동 클릭시 해당 영상 링크 오픈 + */ + const handleVideoClick = (exercise) => { + window.open(exercise.link, "_blank", "noopener,noreferrer"); + if (!isActive) onVideoClick(exercise); + }; + + return ( + <DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}> + <Droppable droppableId="video-list"> + {(provided) => ( + <div + id="video-container" + ref={provided.innerRef} + {...provided.droppableProps} + > + {exercises.map((exercise, index) => ( + <React.Fragment key={exercise.title}> + <Draggable + draggableId={exercise.title} + index={index} + isDragDisabled={isActive} + > + {(provided) => ( + <div + ref={(el) => { + provided.innerRef(el); + videoRefs.current[index] = el; + }} + {...provided.draggableProps} + {...provided.dragHandleProps} + onClick={() => handleVideoClick(exercise)} + className={`video-info ${currentIndex === index && !isRest + ? "highlighted" + : "" + }`} + > + <img src={exercise.thumbnail} alt={exercise.title} /> + <div className="video-title">{truncateText(exercise.title, 80)}</div> + </div> + )} + </Draggable> + {index < exercises.length - 1 && ( + <div + className={`rest-period ${currentIndex - 1 === index && isRest + ? "highlighted-rest" + : "" + }`} + style={{ + visibility: isDragging ? "hidden" : "visible", + }} + + > + {restSeconds}초 - Rest Period + </div> + )} + </React.Fragment> + ))} + {provided.placeholder} + </div> + )} + </Droppable> + </DragDropContext> + ); +} + +export default Video; diff --git a/front/src/pages/routine/formatTime.js b/front/src/components/routine/formatTime.js similarity index 97% rename from front/src/pages/routine/formatTime.js rename to front/src/components/routine/formatTime.js index f19adbf19ac04ca249f1dc050e66b8d211358858..169dc1e9cfdeb48534e0faf045b243371a68273b 100644 --- a/front/src/pages/routine/formatTime.js +++ b/front/src/components/routine/formatTime.js @@ -1,8 +1,8 @@ -const formatTime = (seconds) => { - seconds = Math.abs(seconds); - const minutes = Math.floor(seconds / 60); - const secs = seconds % 60; - return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; -}; - +const formatTime = (seconds) => { + seconds = Math.abs(seconds); + const minutes = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; +}; + export default formatTime; \ No newline at end of file diff --git a/front/src/pages/routine/index.css b/front/src/components/routine/index.css similarity index 94% rename from front/src/pages/routine/index.css rename to front/src/components/routine/index.css index 370220987ee7f1490e93efdbba06683c7aca2545..3302e33e8dd386b184aacfda59093f1b80884ce8 100644 --- a/front/src/pages/routine/index.css +++ b/front/src/components/routine/index.css @@ -1,169 +1,169 @@ -#box { - display: flex; - flex-direction: row; - align-items: center; - padding: 9.59264px; - gap: 9.59px; - - width: 1253.76px; - height: 730px; - - background: rgba(24, 24, 24, 0.0941176); - animation: 0.2s ease-in-out loadEffect1; -} - -#left { - display: flex; - flex-direction: column; - gap: 9.59px; - - width: 969.82px; - height: 710.81px; -} - -#right { - display: flex; - flex-direction: column; - gap: 9.59px; - - width: 255.16px; - height: 710.81px; -} - -#up { - display: flex; - flex-direction: row; - - gap: 9.59px; - - width: 969.82px; - height: 218.71px; -} - -#now { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - width: 467.16px; - height: 218.71px; - - background: #8C7DFF; - border-radius: 33.5742px; -} - -#rest { - display: flex; - flex-direction: column; - justify-content: center; - - width: 241.73px; - height: 218.71px; - - background: #B87EED; - border-radius: 33.5742px; -} - -#timer { - display: flex; - justify-content: center; - align-items: center; - - width: 241.73px; - height: 218.71px; - - background: #242834; - border-radius: 33.5742px; -} - -#down { - display: flex; - flex-direction: row; - align-items: flex-start; - gap: 9.59px; - - width: 969.82px; - height: 482.51px; -} - -#progress { - display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: center; - padding: 15px 0px; - - position: relative; - width: 235.02px; - height: 453.75px; - - overflow: hidden; - - background: #DFDFE4; - border-radius: 33.5742px; -} - -#video { - display: flex; - flex-direction: column; - justify-content: center; - - align-items: center; - - width: 719.45px; - height: 482.51px; - - background: #CFFF5E; - border-radius: 33.5742px; - -} - -#list { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - width: 255.16px; - height: 496.9px; - - background: #B87EED; - border-radius: 33.5742px; -} - -#start { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 0px; - - width: 255.16px; - height: 204.32px; - - background: #8C7DFF; - border-radius: 33.5742px; -} - -input[type="number"] { - -moz-appearance: textfield; - -webkit-appearance: none; - appearance: none; -} - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - -@keyframes loadEffect1 { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } +#box { + display: flex; + flex-direction: row; + align-items: center; + padding: 9.59264px; + gap: 9.59px; + + width: 1253.76px; + height: 730px; + + background: rgba(24, 24, 24, 0.0941176); + animation: 0.2s ease-in-out loadEffect1; +} + +#left { + display: flex; + flex-direction: column; + gap: 9.59px; + + width: 969.82px; + height: 710.81px; +} + +#right { + display: flex; + flex-direction: column; + gap: 9.59px; + + width: 255.16px; + height: 710.81px; +} + +#up { + display: flex; + flex-direction: row; + + gap: 9.59px; + + width: 969.82px; + height: 218.71px; +} + +#now { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: 467.16px; + height: 218.71px; + + background: #8C7DFF; + border-radius: 33.5742px; +} + +#rest { + display: flex; + flex-direction: column; + justify-content: center; + + width: 241.73px; + height: 218.71px; + + background: #B87EED; + border-radius: 33.5742px; +} + +#timer { + display: flex; + justify-content: center; + align-items: center; + + width: 241.73px; + height: 218.71px; + + background: #242834; + border-radius: 33.5742px; +} + +#down { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 9.59px; + + width: 969.82px; + height: 482.51px; +} + +#progress { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + padding: 15px 0px; + + position: relative; + width: 235.02px; + height: 453.75px; + + overflow: hidden; + + background: #DFDFE4; + border-radius: 33.5742px; +} + +#video { + display: flex; + flex-direction: column; + justify-content: center; + + align-items: center; + + width: 719.45px; + height: 482.51px; + + background: #CFFF5E; + border-radius: 33.5742px; + +} + +#list { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: 255.16px; + height: 496.9px; + + background: #B87EED; + border-radius: 33.5742px; +} + +#start { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + + width: 255.16px; + height: 204.32px; + + background: #8C7DFF; + border-radius: 33.5742px; +} + +input[type="number"] { + -moz-appearance: textfield; + -webkit-appearance: none; + appearance: none; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +@keyframes loadEffect1 { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } } \ No newline at end of file diff --git a/front/src/pages/routine/progress.css b/front/src/components/routine/progress.css similarity index 95% rename from front/src/pages/routine/progress.css rename to front/src/components/routine/progress.css index 20c782fa023390dd595ae947bb9ba8dc8190f8d3..9270468840542c26ea0b7189c6aea7898b5d4724 100644 --- a/front/src/pages/routine/progress.css +++ b/front/src/components/routine/progress.css @@ -1,28 +1,28 @@ -.progress-block { - margin: 2px 0; - width: 200px; - border-radius: 5px; - text-align: center; - line-height: 1.5; - font-size: 10px; - color: white; - font-weight: bold; -} - -.new-block { - animation: dropAnimation 0.8s ease forwards; /* 드롭 애니메이션 */ -} - -@keyframes dropAnimation { - 0% { - transform: translateY(-200px); /* 위쪽에서 시작 */ - opacity: 0; /* 투명하게 시작 */ - } - 50% { - opacity: 0.5; /* 중간에서 반투명 */ - } - 100% { - transform: translateY(0); /* 원래 위치 */ - opacity: 1; /* 완전히 보임 */ - } -} +.progress-block { + margin: 2px 0; + width: 200px; + border-radius: 5px; + text-align: center; + line-height: 1.5; + font-size: 10px; + color: white; + font-weight: bold; +} + +.new-block { + animation: dropAnimation 0.8s ease forwards; /* 드롭 애니메이션 */ +} + +@keyframes dropAnimation { + 0% { + transform: translateY(-200px); /* 위쪽에서 시작 */ + opacity: 0; /* 투명하게 시작 */ + } + 50% { + opacity: 0.5; /* 중간에서 반투명 */ + } + 100% { + transform: translateY(0); /* 원래 위치 */ + opacity: 1; /* 완전히 보임 */ + } +} diff --git a/front/src/pages/routine/truncateText.js b/front/src/components/routine/truncateText.js similarity index 97% rename from front/src/pages/routine/truncateText.js rename to front/src/components/routine/truncateText.js index 06d99f47a4ff55e04d3ed57c3ad78131cb584350..142dc316627785fa49170743ca953f6781a1febc 100644 --- a/front/src/pages/routine/truncateText.js +++ b/front/src/components/routine/truncateText.js @@ -1,5 +1,5 @@ -const truncateText = (text, maxLength) => { - return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text; -}; - +const truncateText = (text, maxLength) => { + return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text; +}; + export default truncateText; \ No newline at end of file diff --git a/front/src/components/workout/VideoDetails.jsx b/front/src/components/workout/VideoDetails.jsx index 5a611786cd1bc317565e8cdec2df053fe67b9295..e34aaadb81bd73f3bf39d5777119c2145fb7ed5d 100644 --- a/front/src/components/workout/VideoDetails.jsx +++ b/front/src/components/workout/VideoDetails.jsx @@ -1,9 +1,10 @@ import React from 'react'; -import Thumbnails from '../Thumbnails'; +import Thumbnails from '../common/Thumbnails'; import secToTime from './secToTime'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faHeart } from "@fortawesome/free-regular-svg-icons"; +import {addRoutineVideo} from '../../api/routineAPI'; function VideoDetails({video, routines}) { async function addRoutine(e){ diff --git a/front/src/components/workout/VideoLists.jsx b/front/src/components/workout/VideoLists.jsx index bc288336450e04d3c6fade7a9f1baf90cefed580..9ff94daeb270121357ce532272e59daf4eb6dbd3 100644 --- a/front/src/components/workout/VideoLists.jsx +++ b/front/src/components/workout/VideoLists.jsx @@ -1,7 +1,6 @@ import react, {useEffect, useState, useRef } from 'react'; -import ExerciseBlock from '../ExerciseBlock'; - -import { searchVideos } from '../../api/workoutAPI'; +import ExerciseBlock from '../common/ExerciseBlock'; +import {getUserRoutines} from '../../api/routineAPI'; function timeToSecond(time){ console.log(time); @@ -28,6 +27,7 @@ function VideoLists({data, filter, onShowMore, setShowModal}){ }} function filteringVideos(data, filter){ + console.log(data); return data.filter(video => { const filterTag = filter.video_tag.length?filter.video_tag.some(tag => { return (video.video_tag.toUpperCase().indexOf(tag.toUpperCase()) !== -1 ?true:false); diff --git a/front/src/pages/routine/Routine.css b/front/src/pages/Routine.css similarity index 94% rename from front/src/pages/routine/Routine.css rename to front/src/pages/Routine.css index 370220987ee7f1490e93efdbba06683c7aca2545..3302e33e8dd386b184aacfda59093f1b80884ce8 100644 --- a/front/src/pages/routine/Routine.css +++ b/front/src/pages/Routine.css @@ -1,169 +1,169 @@ -#box { - display: flex; - flex-direction: row; - align-items: center; - padding: 9.59264px; - gap: 9.59px; - - width: 1253.76px; - height: 730px; - - background: rgba(24, 24, 24, 0.0941176); - animation: 0.2s ease-in-out loadEffect1; -} - -#left { - display: flex; - flex-direction: column; - gap: 9.59px; - - width: 969.82px; - height: 710.81px; -} - -#right { - display: flex; - flex-direction: column; - gap: 9.59px; - - width: 255.16px; - height: 710.81px; -} - -#up { - display: flex; - flex-direction: row; - - gap: 9.59px; - - width: 969.82px; - height: 218.71px; -} - -#now { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - width: 467.16px; - height: 218.71px; - - background: #8C7DFF; - border-radius: 33.5742px; -} - -#rest { - display: flex; - flex-direction: column; - justify-content: center; - - width: 241.73px; - height: 218.71px; - - background: #B87EED; - border-radius: 33.5742px; -} - -#timer { - display: flex; - justify-content: center; - align-items: center; - - width: 241.73px; - height: 218.71px; - - background: #242834; - border-radius: 33.5742px; -} - -#down { - display: flex; - flex-direction: row; - align-items: flex-start; - gap: 9.59px; - - width: 969.82px; - height: 482.51px; -} - -#progress { - display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: center; - padding: 15px 0px; - - position: relative; - width: 235.02px; - height: 453.75px; - - overflow: hidden; - - background: #DFDFE4; - border-radius: 33.5742px; -} - -#video { - display: flex; - flex-direction: column; - justify-content: center; - - align-items: center; - - width: 719.45px; - height: 482.51px; - - background: #CFFF5E; - border-radius: 33.5742px; - -} - -#list { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - width: 255.16px; - height: 496.9px; - - background: #B87EED; - border-radius: 33.5742px; -} - -#start { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 0px; - - width: 255.16px; - height: 204.32px; - - background: #8C7DFF; - border-radius: 33.5742px; -} - -input[type="number"] { - -moz-appearance: textfield; - -webkit-appearance: none; - appearance: none; -} - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - -@keyframes loadEffect1 { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } +#box { + display: flex; + flex-direction: row; + align-items: center; + padding: 9.59264px; + gap: 9.59px; + + width: 1253.76px; + height: 730px; + + background: rgba(24, 24, 24, 0.0941176); + animation: 0.2s ease-in-out loadEffect1; +} + +#left { + display: flex; + flex-direction: column; + gap: 9.59px; + + width: 969.82px; + height: 710.81px; +} + +#right { + display: flex; + flex-direction: column; + gap: 9.59px; + + width: 255.16px; + height: 710.81px; +} + +#up { + display: flex; + flex-direction: row; + + gap: 9.59px; + + width: 969.82px; + height: 218.71px; +} + +#now { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: 467.16px; + height: 218.71px; + + background: #8C7DFF; + border-radius: 33.5742px; +} + +#rest { + display: flex; + flex-direction: column; + justify-content: center; + + width: 241.73px; + height: 218.71px; + + background: #B87EED; + border-radius: 33.5742px; +} + +#timer { + display: flex; + justify-content: center; + align-items: center; + + width: 241.73px; + height: 218.71px; + + background: #242834; + border-radius: 33.5742px; +} + +#down { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 9.59px; + + width: 969.82px; + height: 482.51px; +} + +#progress { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + padding: 15px 0px; + + position: relative; + width: 235.02px; + height: 453.75px; + + overflow: hidden; + + background: #DFDFE4; + border-radius: 33.5742px; +} + +#video { + display: flex; + flex-direction: column; + justify-content: center; + + align-items: center; + + width: 719.45px; + height: 482.51px; + + background: #CFFF5E; + border-radius: 33.5742px; + +} + +#list { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: 255.16px; + height: 496.9px; + + background: #B87EED; + border-radius: 33.5742px; +} + +#start { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + + width: 255.16px; + height: 204.32px; + + background: #8C7DFF; + border-radius: 33.5742px; +} + +input[type="number"] { + -moz-appearance: textfield; + -webkit-appearance: none; + appearance: none; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +@keyframes loadEffect1 { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } } \ No newline at end of file diff --git a/front/src/pages/routine/Routine.jsx b/front/src/pages/Routine.jsx similarity index 94% rename from front/src/pages/routine/Routine.jsx rename to front/src/pages/Routine.jsx index eff847811d674aac6dfae54ede40fac751b20250..756a153054d6962c564996d4ded499762c8d61fd 100644 --- a/front/src/pages/routine/Routine.jsx +++ b/front/src/pages/Routine.jsx @@ -1,183 +1,183 @@ -import React, { useState, useEffect } from "react"; -import "./Routine.css"; -import Start from "./Start"; -import Timer from "./Timer"; -import Rest from "./Rest"; -import List from "./List"; -import Video from "./Video"; -import Now from "./Now"; -import Progress from "./Progress"; - -function Routine() { - const [isActive, setIsActive] = useState(false); // 운동 시작 여부 - const [isPaused, setIsPaused] = useState(false); // 운동 중 정지 여부 - const [currentIndex, setCurrentIndex] = useState(0); // 현재 운동 - const [selectedRoutine, setSelectedRoutine] = useState(null); // 선택된 루틴 - const [totalDuration, setTotalDuration] = useState(0); // 총 타이머 시간 - const [restSeconds, setRestSeconds] = useState(60); // 쉬는 시간 - const [isRest, setIsRest] = useState(false); // 휴식 상태 여부 - const [progressTimes, setProgressTimes] = useState(null); - const [endSignal, setEndSignal] = useState(false); // 종료 여부 - - /* - START 버튼 클릭 시 - */ - const ButtonClick = () => { - setIsActive((prev) => !prev); - setIsPaused(false); // 시작 시 정지 상태 해제 - setCurrentIndex(0); - setIsRest(false); - window.open(selectedRoutine.exercises[currentIndex].link, "_blank", "noopener,noreferrer"); - }; - - /* - 운동 중 Pause 버튼 클릭 시 - */ - const handlePause = () => { - if (isActive) setIsPaused((prev) => !prev); // 정지 상태 토글 - }; - - /* - 루틴 선택 시 운동 영상 재생시간 합하여 타이머에 반영 - */ - const handleRoutineSelect = (routine) => { - setSelectedRoutine(routine); - const totalTime = routine.exercises.reduce((sum, exercise) => sum + exercise.duration, 0); - setTotalDuration(totalTime + restSeconds * (routine.exercises.length - 1)); - setCurrentIndex(0); - setIsRest(false); - }; - - /* - 운동 시 Next 클릭 시 다음 운동 영상 링크 오픈과 함께 넘어감 - 휴식 시는 휴식 종료 - */ - const handleNext = () => { - if (isPaused) return; - if (isRest) { - setIsRest(false); - } else if (currentIndex < selectedRoutine.exercises.length - 1) { - setIsRest(true); - setCurrentIndex(currentIndex + 1); - if (isActive) window.open(selectedRoutine.exercises[currentIndex + 1].link, "_blank", "noopener,noreferrer"); - - } else if (isActive) { - setEndSignal(true); - setIsActive(false); - setCurrentIndex(currentIndex + 1); - } - }; - - const onendClick = () => { - window.location.reload(); - } - - /* - 쉬는 시간 반영하여 타이머에 반영 - */ - useEffect(() => { - if (!selectedRoutine) return; - - const totalExerciseTime = selectedRoutine.exercises.reduce( - (sum, exercise) => sum + exercise.duration, - 0 - ); - - const totalRestTime = restSeconds * (selectedRoutine.exercises.length - 1); - - setTotalDuration(totalExerciseTime + totalRestTime); - }, [restSeconds, selectedRoutine]); - - /* - 드래그로 변경한 운동 순서 반영 - */ - const handleRoutineChange = (updatedExercises) => { - setSelectedRoutine({ ...selectedRoutine, exercises: updatedExercises }); - }; - - useEffect(() => { - const handleBeforeUnload = (event) => { - event.preventDefault(); - }; - - window.addEventListener("beforeunload", handleBeforeUnload); - return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); - }; - }, []); - - return ( - <div id="box"> - <div id="left"> - <div id="up"> - <div id="now"> - {selectedRoutine && ( - <Now - currentVideo={selectedRoutine.exercises[currentIndex]} - isRest={isRest} - restSeconds={restSeconds} - onNext={handleNext} - isActive={isActive} - onPause={handlePause} - isPaused={isPaused} - onAddTime={(time) => setProgressTimes(time)} - endSignal={endSignal} - /> - )} - </div> - <div id="rest"> - <Rest onRestChange={setRestSeconds} isActive={isActive} /> - </div> - <div id="timer"> - <Timer - duration={totalDuration} - isActive={isActive && !isPaused} - /> - </div> - </div> - <div id="down"> - <div id="progress"> - {selectedRoutine && ( - <Progress - currentVideo={selectedRoutine.exercises[currentIndex-1]} - times={progressTimes} - endSignal={endSignal} - onendClick={onendClick} - /> - )} - </div> - <div id="video"> - {selectedRoutine && ( - <Video - routine={selectedRoutine} - onRoutineChange={handleRoutineChange} - onVideoClick={(video) => - setCurrentIndex(selectedRoutine.exercises.indexOf(video)) - } - isActive={isActive || isPaused} - currentIndex={currentIndex} - isRest={isRest} - restSeconds={restSeconds} - /> - )} - </div> - </div> - </div> - <div id="right"> - <div id="list"> - <List onRoutineSelect={handleRoutineSelect} isActive={isActive} /> - </div> - <div id="start"> - <Start - ButtonClick={ButtonClick} - isActive={isActive} - hasRoutine={selectedRoutine !== null} - endSignal={endSignal} - /> - </div> - </div> - </div> - ); -} - -export default Routine; +import React, { useState, useEffect } from "react"; +import "./Routine.css"; +import Start from "../components/routine/Start"; +import Timer from "../components/routine/Timer"; +import Rest from "../components/routine/Rest"; +import List from "../components/routine/List"; +import Video from "../components/routine/Video"; +import Now from "../components/routine/Now"; +import Progress from "../components/routine/Progress"; + +function Routine() { + const [isActive, setIsActive] = useState(false); // 운동 시작 여부 + const [isPaused, setIsPaused] = useState(false); // 운동 중 정지 여부 + const [currentIndex, setCurrentIndex] = useState(0); // 현재 운동 + const [selectedRoutine, setSelectedRoutine] = useState(null); // 선택된 루틴 + const [totalDuration, setTotalDuration] = useState(0); // 총 타이머 시간 + const [restSeconds, setRestSeconds] = useState(60); // 쉬는 시간 + const [isRest, setIsRest] = useState(false); // 휴식 상태 여부 + const [progressTimes, setProgressTimes] = useState(null); + const [endSignal, setEndSignal] = useState(false); // 종료 여부 + + /* + START 버튼 클릭 시 + */ + const ButtonClick = () => { + setIsActive((prev) => !prev); + setIsPaused(false); // 시작 시 정지 상태 해제 + setCurrentIndex(0); + setIsRest(false); + window.open(selectedRoutine.exercises[currentIndex].link, "_blank", "noopener,noreferrer"); + }; + + /* + 운동 중 Pause 버튼 클릭 시 + */ + const handlePause = () => { + if (isActive) setIsPaused((prev) => !prev); // 정지 상태 토글 + }; + + /* + 루틴 선택 시 운동 영상 재생시간 합하여 타이머에 반영 + */ + const handleRoutineSelect = (routine) => { + setSelectedRoutine(routine); + const totalTime = routine.exercises.reduce((sum, exercise) => sum + exercise.duration, 0); + setTotalDuration(totalTime + restSeconds * (routine.exercises.length - 1)); + setCurrentIndex(0); + setIsRest(false); + }; + + /* + 운동 시 Next 클릭 시 다음 운동 영상 링크 오픈과 함께 넘어감 + 휴식 시는 휴식 종료 + */ + const handleNext = () => { + if (isPaused) return; + if (isRest) { + setIsRest(false); + } else if (currentIndex < selectedRoutine.exercises.length - 1) { + setIsRest(true); + setCurrentIndex(currentIndex + 1); + if (isActive) window.open(selectedRoutine.exercises[currentIndex + 1].link, "_blank", "noopener,noreferrer"); + + } else if (isActive) { + setEndSignal(true); + setIsActive(false); + setCurrentIndex(currentIndex + 1); + } + }; + + const onendClick = () => { + window.location.reload(); + } + + /* + 쉬는 시간 반영하여 타이머에 반영 + */ + useEffect(() => { + if (!selectedRoutine) return; + + const totalExerciseTime = selectedRoutine.exercises.reduce( + (sum, exercise) => sum + exercise.duration, + 0 + ); + + const totalRestTime = restSeconds * (selectedRoutine.exercises.length - 1); + + setTotalDuration(totalExerciseTime + totalRestTime); + }, [restSeconds, selectedRoutine]); + + /* + 드래그로 변경한 운동 순서 반영 + */ + const handleRoutineChange = (updatedExercises) => { + setSelectedRoutine({ ...selectedRoutine, exercises: updatedExercises }); + }; + + useEffect(() => { + const handleBeforeUnload = (event) => { + event.preventDefault(); + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, []); + + return ( + <div id="box"> + <div id="left"> + <div id="up"> + <div id="now"> + {selectedRoutine && ( + <Now + currentVideo={selectedRoutine.exercises[currentIndex]} + isRest={isRest} + restSeconds={restSeconds} + onNext={handleNext} + isActive={isActive} + onPause={handlePause} + isPaused={isPaused} + onAddTime={(time) => setProgressTimes(time)} + endSignal={endSignal} + /> + )} + </div> + <div id="rest"> + <Rest onRestChange={setRestSeconds} isActive={isActive} /> + </div> + <div id="timer"> + <Timer + duration={totalDuration} + isActive={isActive && !isPaused} + /> + </div> + </div> + <div id="down"> + <div id="progress"> + {selectedRoutine && ( + <Progress + currentVideo={selectedRoutine.exercises[currentIndex-1]} + times={progressTimes} + endSignal={endSignal} + onendClick={onendClick} + /> + )} + </div> + <div id="video"> + {selectedRoutine && ( + <Video + routine={selectedRoutine} + onRoutineChange={handleRoutineChange} + onVideoClick={(video) => + setCurrentIndex(selectedRoutine.exercises.indexOf(video)) + } + isActive={isActive || isPaused} + currentIndex={currentIndex} + isRest={isRest} + restSeconds={restSeconds} + /> + )} + </div> + </div> + </div> + <div id="right"> + <div id="list"> + <List onRoutineSelect={handleRoutineSelect} isActive={isActive} /> + </div> + <div id="start"> + <Start + ButtonClick={ButtonClick} + isActive={isActive} + hasRoutine={selectedRoutine !== null} + endSignal={endSignal} + /> + </div> + </div> + </div> + ); +} + +export default Routine;