Skip to content
Snippets Groups Projects
Commit 9bc0d8a2 authored by 지윤 장's avatar 지윤 장
Browse files

Merge branch 'feat/루틴' into 'develop'

Feat/루틴

See merge request !43
parents 30557ecb cc12e6af
No related branches found
No related tags found
1 merge request!43Feat/루틴
Pipeline #10867 failed
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import "./List.css";
import truncateText from './truncateText';
import { getUserRoutines, getRoutineVideos, deleteRoutine } from './api';
function List({ onRoutineSelect, isActive }) {
const initialRoutines = [
{
id: 1,
......@@ -111,15 +111,82 @@ function List({ onRoutineSelect, isActive }) {
},
];
const [routines] = useState(initialRoutines); // 루틴 목록
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) => {
setSelectedRoutine(routine);
if (!modify) setSelectedRoutine(routine);
};
/*
......@@ -129,6 +196,15 @@ function List({ onRoutineSelect, isActive }) {
setSelectedRoutine(null);
};
const handledelete = async (name) => {
try {
await deleteRoutine(name);
setIsModalOpen(false);
} catch (err) {
alert(err);
}
};
/*
토글 클릭 시 해당 운동의 루틴 제외 여부 설정
*/
......@@ -153,9 +229,31 @@ function List({ onRoutineSelect, isActive }) {
{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>
) : (
......@@ -198,7 +296,7 @@ function List({ onRoutineSelect, isActive }) {
>
선택
</button>
<button onClick={handleBackClick}>뒤로가기</button>
<button className='back' onClick={handleBackClick}>뒤로가기</button>
</div>
</div>
</div>
......
......@@ -35,10 +35,25 @@
.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-container button{
width: 80px;
height: 30px;
margin: 20px;
.now-content {
display: flex;
align-items: center;
font-size: 22px;
font-weight: 600;
width: 420px;
height: 160px;
}
\ No newline at end of file
......@@ -20,6 +20,7 @@ function Now({
const [isTakingBreak, setIsTakingBreak] = useState(false); // 운동 중 휴식 상태
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 여부
const [isFlag, setFlag] = useState(true);
const [oneRest, setoneRest] = useState(false);
/*
운동 시작 전 설정한 휴식 시간으로 타이머 시간 설정
......@@ -32,11 +33,10 @@ function Now({
마지막 운동에 대한 시간 전달
*/
useEffect(() => {
if (endSignal&&!isFlag){
if (endSignal) {
onAddTime(`Exercise: ${exerciseTime}`);
setFlag(true);
}
}, [exerciseTime]);
}, [exerciseTime,endSignal]);
/*
운동 시간 타이머
......@@ -75,6 +75,10 @@ function Now({
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);
......@@ -97,18 +101,22 @@ function Now({
onAddTime(`Exercise: ${exerciseTime}`);
setExerciseTime(0);
setRestTimeLeft(restSeconds);
} else if (!isRest && !isFlag && isActive) onAddTime(`Rest: ${restSeconds}`);
else if (!isRest && isFlag && isActive) setFlag(false);
}
}, [currentVideo, isRest, isActive]);
const handleRestStart = () => {
if (!isPaused && isActive) setIsTakingBreak(true);
if (!isPaused && isActive && !oneRest) {
setIsTakingBreak(true);
setoneRest(true);
}
};
const handleRestStop = () => {
if (isTakingBreak) {
setIsTakingBreak(false);
onAddTime(`Rest: ${restTime}`);
setRestTime(0);
}
};
/*
......@@ -124,26 +132,34 @@ function Now({
};
return (
<div className={`now-container ${isRest ? "rest-highlight" : ""}`}>
<div className='now-container'>
{isTakingBreak ? (
<div className="now-break">
<p>Taking a Break</p>
<h3>{formatTime(restTime)}</h3>
<button className="stop-rest-button" onClick={handleRestStop}>
Resume Exercise
<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 className="now-rest">
<p>Rest Period</p>
<div>
<div className='now-content'>
<span>Rest Period / </span>
{!isModalOpen ? (
<h3>{formatTime(restTimeLeft)}</h3>
<span>/ {formatTime(restTimeLeft)}</span>
) : (
<h3>다음 운동으로</h3>
<span>/ 다음 운동으로</span>
)}
</div>
<div className="underbar">
<button className="next-button" onClick={handleRestNext}>
Next
</button>
</div>
{isModalOpen && (
<div className="modal-backdrop">
<div className="modal">
......@@ -155,7 +171,8 @@ function Now({
)}
</div>
) : (
<div className="now-container">
<div>
<div className='now-content'>
{currentVideo?.thumbnail && (
<div className="now-thumbnail">
<img
......@@ -166,15 +183,16 @@ function Now({
<p className="now-title">{truncateText(currentVideo.title, 20)}</p>
</div>
)}
<span>Exercise Timer: {formatTime(exerciseTime)}</span>
<p>Timer: {formatTime(exerciseTime)}</p>
</div>
<div className="underbar">
<button className="next-button" onClick={onNext}>
<button onClick={onNext}>
Next
</button>
<button className="pause-button" onClick={onPause}>
<button onClick={onPause}>
{isPaused ? "Start" : "Pause"}
</button>
<button className="rest-button" onClick={handleRestStart}>
<button onClick={handleRestStart}>
Rest
</button>
</div>
......
......@@ -61,9 +61,10 @@ function Routine() {
setCurrentIndex(currentIndex + 1);
if (isActive) window.open(selectedRoutine.exercises[currentIndex + 1].link, "_blank", "noopener,noreferrer");
} else {
} else if (isActive) {
setEndSignal(true);
setIsActive(false);
setCurrentIndex(currentIndex + 1);
}
};
......@@ -94,6 +95,17 @@ function Routine() {
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">
......@@ -125,7 +137,14 @@ function Routine() {
</div>
<div id="down">
<div id="progress">
<Progress times={progressTimes} endSignal={endSignal} onendClick={onendClick} />
{selectedRoutine && (
<Progress
currentVideo={selectedRoutine.exercises[currentIndex-1]}
times={progressTimes}
endSignal={endSignal}
onendClick={onendClick}
/>
)}
</div>
<div id="video">
{selectedRoutine && (
......@@ -153,6 +172,7 @@ function Routine() {
ButtonClick={ButtonClick}
isActive={isActive}
hasRoutine={selectedRoutine !== null}
endSignal={endSignal}
/>
</div>
</div>
......
import React, { useState } from 'react';
import './Start.css';
function Start({ ButtonClick, isActive, hasRoutine }) {
function Start({ ButtonClick, isActive, hasRoutine, endSignal }) {
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 여부
const [countdown, setCountdown] = useState(null); // 카운트다운 상태
......@@ -31,7 +31,7 @@ function Start({ ButtonClick, isActive, hasRoutine }) {
운동 시작 전이면 모달 표시
*/
const handleStartClick = () => {
if (!isActive) setIsModalOpen(true);
if (!isActive&&!endSignal) setIsModalOpen(true);
};
/*
......
......@@ -67,6 +67,7 @@
height: 25.04px;
background: #8C7DFF;
border: 2px solid #B87EED;
border-radius: 14.389px;
}
......
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 };
import React, { useState, useEffect } from "react";
import "./progress.css";
import "./Progress.css";
import { addExerciseRecord } from './api';
function Progress({ times, endSignal, onendClick }) {
function Progress({ currentVideo, times, endSignal, onendClick }) {
const [restTimes, setRestTimes] = useState([]);
const [exerciseTimes, setExerciseTimes] = useState([]);
const [allTimes, setAllTimes] = useState([]);
......@@ -21,8 +22,19 @@ function Progress({ times, endSignal, onendClick }) {
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]);
......@@ -41,7 +53,7 @@ function Progress({ times, endSignal, onendClick }) {
if (endSignal) setIsModalOpen(true);
}, [endSignal]);
const calculateHeight = (value) => 5 * ((value + 60) / 60 + 1);
const calculateHeight = (value) => 2 * ((value + 60) / 60 + 1);
return (
<div className="progress-container">
......@@ -60,7 +72,7 @@ function Progress({ times, endSignal, onendClick }) {
/>
);
})}
{isModalOpen && (
{(isModalOpen && endSignal) && (
<div className="modal-backdrop">
<div className="modal">
<div className="modal-content">
......
......@@ -60,7 +60,7 @@ function Video({
*/
const handleVideoClick = (exercise) => {
window.open(exercise.link, "_blank", "noopener,noreferrer");
onVideoClick(exercise);
if (!isActive) onVideoClick(exercise);
};
return (
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment