From e81c3977f64a8f05d57af02796dadd152d44e679 Mon Sep 17 00:00:00 2001 From: PortalCube <35104213+PortalCube@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:31:55 +0900 Subject: [PATCH 1/7] =?UTF-8?q?Feat:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20API?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/librarys/axios.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/librarys/axios.js b/src/librarys/axios.js index e551b27..35cf2dc 100644 --- a/src/librarys/axios.js +++ b/src/librarys/axios.js @@ -5,7 +5,7 @@ export default axios.create({ timeout: 10000, }); -export function getSpringAxios(token) { +export function getSpringAxios(token = null) { const options = { baseURL: "http://motus.website/", timeout: 10000, @@ -26,3 +26,41 @@ export function getAIAxios() { timeout: 1000 * 60 * 60 * 24, }); } + +export async function createVideo(options) { + const axios = getSpringAxios(); + + const data = new FormData(); + data.append("title", options.title); + data.append("description", options.description); + data.append("category", options.category); + data.append("position", options.pose); + data.append("frame", options.totalFrame); + data.append("playTime", options.duration); + data.append("files[0]", options.video); + data.append("files[1]", options.skeleton); + + const response = await axios.post("/video/create", data); + return response.data; +} + +export async function removeVideo(id) { + const axios = getSpringAxios(); + + const response = await axios.delete("/video/delete/" + id); + return response.data; +} + +export async function listVideo() { + const axios = getSpringAxios(); + + const response = await axios.get("/video/list"); + return response.data; +} + +export async function getVideo(id) { + const axios = getSpringAxios(); + + const response = await axios.get("/video/" + id); + return response.data; +} From 4d943671a43a3acf230b8272df35fc8afb295872 Mon Sep 17 00:00:00 2001 From: PortalCube <35104213+PortalCube@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:33:19 +0900 Subject: [PATCH 2/7] =?UTF-8?q?Feat:=20=EB=8F=99=EC=98=81=EC=83=81=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20?= =?UTF-8?q?=EC=B5=9C=EC=A2=85=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reducer 패턴을 이용하여 코드 구성 단순화 - 각종 dispatch 코드 및 handle 코드 리팩토링 --- src/components/SkeletonVideo.jsx | 29 ++++----- src/components/TagSelect.jsx | 100 +++++++++++++++++++++++++++++++ src/components/VideoUploader.jsx | 100 +++++++++++++++++++++---------- src/pages/AddExercise.jsx | 87 ++++++++++++++++++++++----- src/reducer/upload.js | 54 +++++++++++++++++ 5 files changed, 308 insertions(+), 62 deletions(-) create mode 100644 src/components/TagSelect.jsx create mode 100644 src/reducer/upload.js diff --git a/src/components/SkeletonVideo.jsx b/src/components/SkeletonVideo.jsx index 14078c2..ca32058 100644 --- a/src/components/SkeletonVideo.jsx +++ b/src/components/SkeletonVideo.jsx @@ -30,6 +30,7 @@ const Loading = styled.div` flex-direction: column; align-items: center; justify-content: center; + color: white; background-color: rgba(0, 0, 0, 0.5); &.disable { @@ -39,13 +40,13 @@ const Loading = styled.div` const Text = styled.p` margin-top: 12px; - font-size: 12px; + font-size: 16px; text-align: center; `; const Icon = styled(ImSpinner2)` - width: 48px; - height: 48px; + width: 72px; + height: 72px; animation: spin 5s infinite linear; @keyframes spin { @@ -124,6 +125,17 @@ function drawPoints(context, data, ratio) { }); } +function getContainedSize(video) { + const ratio = video.videoWidth / video.videoHeight; + let width = video.clientHeight * ratio; + let height = video.clientHeight; + if (width > video.clientWidth) { + width = video.clientWidth; + height = video.clientWidth / ratio; + } + return [width, height]; +} + const SkeletonVideo = ({ src, skeleton, onLoad, ...props }) => { const containerRef = useRef(null); const canvasRef = useRef(null); @@ -197,17 +209,6 @@ const SkeletonVideo = ({ src, skeleton, onLoad, ...props }) => { drawPoints(context, data[currentFrame], ratio); } - function getContainedSize(video) { - const ratio = video.videoWidth / video.videoHeight; - let width = video.clientHeight * ratio; - let height = video.clientHeight; - if (width > video.clientWidth) { - width = video.clientWidth; - height = video.clientWidth / ratio; - } - return [width, height]; - } - function getWidthAndHeight(event) { onLoad(event); setAspectRatio(event.target.videoWidth / event.target.videoHeight); diff --git a/src/components/TagSelect.jsx b/src/components/TagSelect.jsx new file mode 100644 index 0000000..c055666 --- /dev/null +++ b/src/components/TagSelect.jsx @@ -0,0 +1,100 @@ +import { useContext, useState } from "react"; +import styled from "styled-components"; +import PropTypes from "prop-types"; +import { CATEGORY, POSITION } from "../librarys/type"; +import { ReducerContext } from "../librarys/context"; + +const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + gap: 16px; +`; + +const FilterSection = styled.div` + height: 36px; + display: flex; + align-items: center; + gap: 12px; +`; + +const Header = styled.div` + width: 120px; + display: flex; + align-items: center; + justify-content: space-between; + + font-size: 20px; + font-weight: 700; + color: #5f5f5f; + + &::after { + content: ""; + width: 2px; + height: 20px; + background-color: #5f5f5f; + } +`; + +const Button = styled.button` + width: 100px; + height: 100%; + background-color: ${(props) => (props.selected ? "#6968CC" : "#1E1E1E")}; + color: #f2f2f2; + border-radius: 10px; + font-size: 16px; + border: none; + cursor: pointer; + + &:focus { + outline: none; + } +`; + +const TagSelect = () => { + const [state, dispatch] = useContext(ReducerContext); + const { category, pose } = state; + + function onClick(type, payload) { + dispatch({ + type, + payload, + }); + } + + return ( + + +
카테고리
+ {CATEGORY.map(({ key, value }) => ( + + ))} +
+ + +
자세
+ {POSITION.map(({ key, value }) => ( + + ))} +
+
+ ); +}; + +TagSelect.propTypes = { + onChange: PropTypes.func, +}; + +export default TagSelect; diff --git a/src/components/VideoUploader.jsx b/src/components/VideoUploader.jsx index 75617f8..ddba024 100644 --- a/src/components/VideoUploader.jsx +++ b/src/components/VideoUploader.jsx @@ -1,6 +1,10 @@ -import styled from 'styled-components'; -import PropTypes from 'prop-types'; -import { useState } from 'react'; +import styled from "styled-components"; +import PropTypes from "prop-types"; +import { useContext, useMemo, useRef, useState } from "react"; +import SkeletonVideo from "./SkeletonVideo"; +import { getSkeletons } from "../librarys/skeleton-api"; +import classNames from "classnames"; +import { ReducerContext } from "../librarys/context"; const VideoUploadContainer = styled.div` width: 540px; @@ -11,12 +15,12 @@ const VideoUploadContainer = styled.div` display: flex; align-items: center; justify-content: center; - cursor: pointer; + cursor: pointer; position: relative; - overflow: hidden; + overflow: hidden; - &:hover { - opacity: 0.8; + &.disable { + cursor: default; } `; @@ -28,39 +32,75 @@ const UploadText = styled.span` `; const HiddenInput = styled.input` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0; - cursor: pointer; + display: none; `; -const VideoPreview = styled.video` +const VideoPreview = styled(SkeletonVideo)` width: 100%; height: 100%; border-radius: 10px; `; -const VideoUploader = ({ onUpload }) => { - const [videoPreview, setVideoPreview] = useState(null); +const VideoUploader = () => { + const input = useRef(null); + const [state, dispatch] = useContext(ReducerContext); + const { skeleton } = state; + const url = useMemo( + () => (state.video ? URL.createObjectURL(state.video) : null), + [state.video], + ); - const handleFileChange = (event) => { + const handleVideoChange = async (event) => { const file = event.target.files[0]; - if (file) { - // 파일 객체에서 URL을 생성하여 미리보기에 사용 - const videoURL = URL.createObjectURL(file); - setVideoPreview(videoURL); - onUpload && onUpload(file); + + if (!file) { + return; } + + // 클라이언트 단에서 영상 띄우기 + dispatch({ + type: "video", + payload: file, + }); + + // AI에게 영상 binary를 전송 + const formData = new FormData(); + formData.append("video_file", file); + const skeleton = await getSkeletons(formData); + + // 스켈레톤 받아오면 등록 + dispatch({ + type: "skeleton", + payload: skeleton, + }); }; + function handleMetadata(event) { + dispatch({ + type: "duration", + payload: Number(event.target.duration), + }); + } + + function onClick() { + if (input && url === null) { + input.current.click(); + } + } + return ( - - - {videoPreview ? ( - + + + {url ? ( + ) : ( 여기를 클릭해서
동영상 업로드 @@ -70,8 +110,4 @@ const VideoUploader = ({ onUpload }) => { ); }; -VideoUploader.propTypes = { - onUpload: PropTypes.func, -}; - -export default VideoUploader; \ No newline at end of file +export default VideoUploader; diff --git a/src/pages/AddExercise.jsx b/src/pages/AddExercise.jsx index 8a3e94a..b61ac1b 100644 --- a/src/pages/AddExercise.jsx +++ b/src/pages/AddExercise.jsx @@ -1,9 +1,13 @@ import AddHeader from "../components/AddHeader.jsx"; import VideoUploader from "../components/VideoUploader.jsx"; import styled from "styled-components"; -import { useState } from "react"; +import { useReducer, useState } from "react"; import UploadButton from "../components/UploadButton.jsx"; -import FilterButtons from "../components/FilterButtons.jsx"; +import TagSelect from "../components/TagSelect.jsx"; +import { createVideo } from "../librarys/axios.js"; +import { useNavigate } from "react-router-dom"; +import { intialUploadState, uploadReducer } from "../reducer/upload.js"; +import { ReducerContext } from "../librarys/context.js"; const PageContainer = styled.div` width: 1200px; @@ -30,12 +34,13 @@ const Title = styled.h1` width: 100%; font-weight: bold; font-size: 24px; - color: #ffffff; + color: #f2f2f2; `; const StyledInput = styled.input` width: 100%; height: 50px; + color: #f2f2f2; background-color: #242424; border-radius: 10px; border: 1px solid #444444; @@ -45,6 +50,7 @@ const StyledInput = styled.input` const StyledTextarea = styled.textarea` width: 100%; height: 120px; + color: #f2f2f2; background-color: #242424; border-radius: 10px; border: 1px solid #444444; @@ -61,24 +67,73 @@ const UploadButtonContainer = styled.div` `; const AddExercise = () => { - const [title, setTitle] = useState(""); - const [description, setDesctiption] = useState(""); + const navigate = useNavigate(); + const [state, dispatch] = useReducer(uploadReducer, intialUploadState); + const { title, description, video, skeleton } = state; - const handleTitleChange = (e) => { - setTitle(e.target.value); + const handleChange = (type) => { + return (e) => dispatch({ type, payload: e.target.value }); }; - const handleDescriptionChange = (e) => { - setDesctiption(e.target.value); + const handleTagChange = ({ category, pose }) => { + dispatch({ + type: "tag", + payload: [category, pose], + }); }; + async function upload() { + if (video === null) { + alert("동영상을 업로드해주세요."); + return; + } + + // if (skeleton === null) { + // alert("동영상의 처리가 완료될 때까지 기다려주세요."); + // return; + // } + + if (title.length < 1) { + alert("제목을 2자 이상 입력해주세요."); + return; + } + + if (description.length < 1) { + alert("설명을 2자 이상 입력해주세요."); + return; + } + + const dummy = { + error: "테스트 데이터입니다.", + video_length: "0", + }; + + const blob = new Blob([JSON.stringify(dummy)], { + type: "application/json", + }); + + const options = { + ...state, + totalFrame: parseInt(dummy.video_length), + skeleton: blob, + }; + + console.log(options); + + const programResponse = await createVideo(options); + console.log(programResponse); + + alert("비디오를 성공적으로 게시했습니다."); + navigate("/"); + } + return ( -
+ - 동영상 및 스켈레톤 데이터 + 동영상 및 AI 스켈레톤 데이터 @@ -87,24 +142,24 @@ const AddExercise = () => { placeholder="제목을 입력하세요..." maxLength={50} value={title} - onChange={handleTitleChange} + onChange={handleChange("title")} /> 운동 설명 카테고리 및 태그 - + - + -
+ ); }; export default AddExercise; diff --git a/src/reducer/upload.js b/src/reducer/upload.js new file mode 100644 index 0000000..9bca252 --- /dev/null +++ b/src/reducer/upload.js @@ -0,0 +1,54 @@ +import { CATEGORY, POSITION } from "../librarys/type"; + +export const intialUploadState = { + title: "", + description: "", + category: CATEGORY[0].key, + pose: POSITION[0].key, + video: null, + duration: null, + skeleton: null, +}; + +export function uploadReducer(state, action) { + switch (action.type) { + case "title": + return { + ...state, + title: action.payload, + }; + case "description": + return { + ...state, + description: action.payload, + }; + case "category": + return { + ...state, + category: action.payload, + }; + case "pose": + return { + ...state, + pose: action.payload, + }; + case "video": + return { + ...state, + video: action.payload, + }; + case "duration": + return { + ...state, + duration: action.payload, + }; + case "skeleton": + return { + ...state, + skeleton: action.payload, + }; + default: + console.error("[UploadReducer] Undefined action: " + action.type); + return state; + } +} From c5d9da627014aa59298d9ace53c3700ba57d7a5a Mon Sep 17 00:00:00 2001 From: PortalCube <35104213+PortalCube@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:33:37 +0900 Subject: [PATCH 3/7] =?UTF-8?q?Chore:=20type=EC=9D=84=20API=EC=99=80=20?= =?UTF-8?q?=EB=8F=99=EC=9D=BC=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/librarys/type.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/librarys/type.js b/src/librarys/type.js index 10430a5..f4de5b0 100644 --- a/src/librarys/type.js +++ b/src/librarys/type.js @@ -1,8 +1,8 @@ export const CATEGORY = [ - { key: "ARMS", value: "팔" }, - { key: "SHOULDERS", value: "어깨" }, - { key: "KNEES", value: "무릎" }, - { key: "THIGHS", value: "허벅지" }, + { key: "ARM", value: "팔" }, + { key: "SHOULDER", value: "어깨" }, + { key: "KNEE", value: "무릎" }, + { key: "THIGH", value: "허벅지" }, ]; export const POSITION = [ From 459ce8dc4a2042e5467607ec3c0fec199c7bab0b Mon Sep 17 00:00:00 2001 From: PortalCube <35104213+PortalCube@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:33:52 +0900 Subject: [PATCH 4/7] =?UTF-8?q?Feat:=20reducerContext=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/librarys/context.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/librarys/context.js diff --git a/src/librarys/context.js b/src/librarys/context.js new file mode 100644 index 0000000..7e7042f --- /dev/null +++ b/src/librarys/context.js @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const ReducerContext = createContext(); From ea9e7c771a216ec95665174bbbf2b34d65c5c6a3 Mon Sep 17 00:00:00 2001 From: PortalCube <35104213+PortalCube@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:34:05 +0900 Subject: [PATCH 5/7] =?UTF-8?q?Chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20user?= =?UTF-8?q?id=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/librarys/player.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/librarys/player.js b/src/librarys/player.js index 6760fca..194d35b 100644 --- a/src/librarys/player.js +++ b/src/librarys/player.js @@ -12,7 +12,6 @@ class Player { guideDuration = null; name = null; id = null; - userId = null; videoId = null; status = 0; @@ -181,10 +180,6 @@ class Player { } const score = response ? response.metrics : 0.01; - console.log(this.videoId, this.userId, score); - - await modifyMetrics(this.videoId, this.userId, score); - this.onComplete(time, score); } } From 0830249a43ae9abfb6a9d25de2fcf63f4374039f Mon Sep 17 00:00:00 2001 From: PortalCube <35104213+PortalCube@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:34:23 +0900 Subject: [PATCH 6/7] =?UTF-8?q?Chore:=20=EC=9D=BC=EB=B6=80=20=EC=83=9D?= =?UTF-8?q?=EB=9E=B5=EB=90=9C=20=ED=99=95=EC=9E=A5=EC=9E=90=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/player/ControllerSection.jsx | 2 +- src/components/player/GuideSection.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/player/ControllerSection.jsx b/src/components/player/ControllerSection.jsx index b21443b..ebedf0d 100644 --- a/src/components/player/ControllerSection.jsx +++ b/src/components/player/ControllerSection.jsx @@ -5,7 +5,7 @@ import { MdClose, MdPlayArrow, MdRefresh } from "react-icons/md"; import classNames from "classnames"; import styled from "styled-components"; -import { DispatchContext, StateContext } from "../../librarys/context"; +import { DispatchContext, StateContext } from "../../librarys/context.jsx"; import Player from "../../librarys/player.js"; import { useNavigate } from "react-router-dom"; diff --git a/src/components/player/GuideSection.jsx b/src/components/player/GuideSection.jsx index 1bf5658..af3f49d 100644 --- a/src/components/player/GuideSection.jsx +++ b/src/components/player/GuideSection.jsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useContext } from "react"; import styled from "styled-components"; import Player from "../../librarys/player"; -import { DispatchContext, StateContext } from "../../librarys/context"; +import { DispatchContext, StateContext } from "../../librarys/context.jsx"; const Container = styled.div` max-width: 20%; From 4e8b28929d1dbc1a3c7574e07b7a256730e7eb99 Mon Sep 17 00:00:00 2001 From: PortalCube <35104213+PortalCube@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:44:07 +0900 Subject: [PATCH 7/7] =?UTF-8?q?Fix:=20=EC=83=9D=EB=9E=B5=EB=90=9C=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=EC=9E=90=20=EB=AA=85=EC=8B=9C=20(2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/TagSelect.jsx | 2 +- src/components/VideoUploader.jsx | 2 +- src/components/player/BorderBox.jsx | 2 +- src/components/player/Countdown.jsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/TagSelect.jsx b/src/components/TagSelect.jsx index c055666..8b27031 100644 --- a/src/components/TagSelect.jsx +++ b/src/components/TagSelect.jsx @@ -2,7 +2,7 @@ import { useContext, useState } from "react"; import styled from "styled-components"; import PropTypes from "prop-types"; import { CATEGORY, POSITION } from "../librarys/type"; -import { ReducerContext } from "../librarys/context"; +import { ReducerContext } from "../librarys/context.js"; const Container = styled.div` display: flex; diff --git a/src/components/VideoUploader.jsx b/src/components/VideoUploader.jsx index ddba024..b31fac3 100644 --- a/src/components/VideoUploader.jsx +++ b/src/components/VideoUploader.jsx @@ -4,7 +4,7 @@ import { useContext, useMemo, useRef, useState } from "react"; import SkeletonVideo from "./SkeletonVideo"; import { getSkeletons } from "../librarys/skeleton-api"; import classNames from "classnames"; -import { ReducerContext } from "../librarys/context"; +import { ReducerContext } from "../librarys/context.js"; const VideoUploadContainer = styled.div` width: 540px; diff --git a/src/components/player/BorderBox.jsx b/src/components/player/BorderBox.jsx index e4eecd1..b41fea2 100644 --- a/src/components/player/BorderBox.jsx +++ b/src/components/player/BorderBox.jsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useContext } from "react"; import styled from "styled-components"; -import { StateContext } from "../../librarys/context"; +import { StateContext } from "../../librarys/context.jsx"; const Container = styled.div` width: 100%; diff --git a/src/components/player/Countdown.jsx b/src/components/player/Countdown.jsx index 6cce66a..6f2260b 100644 --- a/src/components/player/Countdown.jsx +++ b/src/components/player/Countdown.jsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useContext } from "react"; import styled from "styled-components"; import PropTypes from "prop-types"; -import { StateContext } from "../../librarys/context"; +import { StateContext } from "../../librarys/context.jsx"; const Container = styled.div` max-width: 700px;