From bc6ab1f07def75dd07b61826909dd820af56b258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9C=A4?= <asdfasdf001234@ajou.ac.kr> Date: Sun, 8 Dec 2024 10:22:57 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20API=20import=20=EB=B0=8F=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/App.jsx | 2 +- .../routine/api.js => api/routineAPI.js} | 127 ++-- front/src/api/workoutAPI.js | 23 +- front/src/assets/explains.json | 2 + front/src/components/Footer.jsx | 31 - front/src/components/Header.jsx | 40 -- front/src/components/Resizable.jsx | 39 -- .../components/{ => common}/ExerciseBlock.jsx | 11 +- .../components/{ => common}/Thumbnails.jsx | 0 .../{pages => components}/routine/List.css | 282 ++++---- .../{pages => components}/routine/List.jsx | 618 +++++++++--------- .../src/{pages => components}/routine/Now.css | 116 ++-- .../src/{pages => components}/routine/Now.jsx | 410 ++++++------ .../routine/Progress.jsx} | 188 +++--- .../{pages => components}/routine/Rest.css | 98 +-- .../{pages => components}/routine/Rest.jsx | 104 +-- .../{pages => components}/routine/Start.css | 258 ++++---- .../{pages => components}/routine/Start.jsx | 200 +++--- .../{pages => components}/routine/Timer.css | 110 ++-- .../{pages => components}/routine/Timer.jsx | 106 +-- .../{pages => components}/routine/Video.css | 196 +++--- .../routine/Video.jsx} | 250 +++---- .../routine/formatTime.js | 14 +- .../{pages => components}/routine/index.css | 336 +++++----- .../routine/progress.css | 56 +- .../routine/truncateText.js | 8 +- front/src/components/workout/VideoDetails.jsx | 3 +- front/src/components/workout/VideoLists.jsx | 6 +- front/src/pages/{routine => }/Routine.css | 336 +++++----- front/src/pages/{routine => }/Routine.jsx | 366 +++++------ 30 files changed, 2115 insertions(+), 2221 deletions(-) rename front/src/{pages/routine/api.js => api/routineAPI.js} (61%) delete mode 100644 front/src/components/Footer.jsx delete mode 100644 front/src/components/Header.jsx delete mode 100644 front/src/components/Resizable.jsx rename front/src/components/{ => common}/ExerciseBlock.jsx (93%) rename front/src/components/{ => common}/Thumbnails.jsx (100%) rename front/src/{pages => components}/routine/List.css (93%) rename front/src/{pages => components}/routine/List.jsx (98%) rename front/src/{pages => components}/routine/Now.css (94%) rename front/src/{pages => components}/routine/Now.jsx (97%) rename front/src/{pages/routine/progress.jsx => components/routine/Progress.jsx} (95%) rename front/src/{pages => components}/routine/Rest.css (93%) rename front/src/{pages => components}/routine/Rest.jsx (97%) rename front/src/{pages => components}/routine/Start.css (94%) rename front/src/{pages => components}/routine/Start.jsx (96%) rename front/src/{pages => components}/routine/Timer.css (93%) rename front/src/{pages => components}/routine/Timer.jsx (97%) rename front/src/{pages => components}/routine/Video.css (94%) rename front/src/{pages/routine/video.jsx => components/routine/Video.jsx} (97%) rename front/src/{pages => components}/routine/formatTime.js (97%) rename front/src/{pages => components}/routine/index.css (94%) rename front/src/{pages => components}/routine/progress.css (95%) rename front/src/{pages => components}/routine/truncateText.js (97%) rename front/src/pages/{routine => }/Routine.css (94%) rename front/src/pages/{routine => }/Routine.jsx (94%) diff --git a/front/src/App.jsx b/front/src/App.jsx index 3619886..c4c6494 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 75b640f..6172610 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 4fd8503..7cb8558 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 1138d9e..492e8f0 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 a5ee582..0000000 --- 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 7414da5..0000000 --- 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 63db821..0000000 --- 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 8bbc538..e4d6995 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 02c4b62..5a240d9 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 aad379c..ed16b33 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 2514ef7..477d7ec 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 e11ddf1..23c8c9c 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 0ead796..6a4eac3 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 8ca6b2a..958fbdf 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 907c760..bd95ff7 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 0cc6378..068e234 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 8eb4fc6..d89ff75 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 2bc3fc6..e5a7433 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 2ce7cd3..3a767d5 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 8381711..f21a59c 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 f818b28..e3299ec 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 f19adbf..169dc1e 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 3702209..3302e33 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 20c782f..9270468 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 06d99f4..142dc31 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 5a61178..e34aaad 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 bc28833..9ff94da 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 3702209..3302e33 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 eff8478..756a153 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; -- GitLab