Skip to content

Feat(#209, #240) 다시보기 페이지 프로그래스바 및 프롬프트 기능 구현 #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/public/reviewLecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"text": "타자석에 위치해 있습니다."
},
{
"start": 57840,
"start": 157840,
"text": "가 제기되고 있습니다. 기존 15m의 안전벨트를 20m까지 높이는 공사를 진행하고 있지만 근본적인 해결책이 되기 어렵습니다."
}
]
4 changes: 2 additions & 2 deletions frontend/src/assets/svgs/progressPlay.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 47 additions & 7 deletions frontend/src/components/LogContainer/LogContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import SendMessage from "@/assets/svgs/sendMessage.svg?react";
import participantSocketRefState from "@/stores/stateParticipantSocketRef";

import { useEffect, useRef, useState } from "react";
import { useRecoilValue } from "recoil";
import { CSSProperties, useEffect, useLayoutEffect, useRef, useState } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { useLocation } from "react-router-dom";
import { convertMsToTimeString } from "@/utils/convertMsToTimeString";
import axios from "axios";
import progressMsTimeState from "@/stores/stateProgressMsTime";
import convertTimeStringToMS from "@/utils/converTimeStringToMS";

interface LogItemInterface {
key?: string;
title: string;
contents: string;
className?: string;
onClick?: any;
style?: CSSProperties;
}

interface LogContainerInterface {
type: "question" | "prompt";
className: string;
}

const LogItem = ({ title, contents }: LogItemInterface) => {
const LogItem = ({ title, contents, className, onClick, style }: LogItemInterface) => {
return (
<li className="h-21 p-4 border mt-4 mb-2 first-of-type:mt-0 bg-grayscale-white border-grayscale-lightgray rounded-lg">
<li
className={`${className} h-21 p-4 border mt-4 mb-2 first-of-type:mt-0 bg-grayscale-white border-grayscale-lightgray rounded-lg`}
style={style}
onClick={onClick}
>
<p className="semibold-16">{title}</p>
<p className="mt-2 medium-12 text-grayscale-darkgray">{contents}</p>
</li>
Expand All @@ -33,7 +42,10 @@ const LogContainer = ({ type, className }: LogContainerInterface) => {
const [scriptList, setScriptList] = useState<Array<{ start: string; text: string }>>([]);
const messageInputRef = useRef<HTMLInputElement | null>(null);
const logContainerRef = useRef<HTMLUListElement | null>(null);
const [progressMsTime, setProgressMsTime] = useRecoilState(progressMsTimeState);
const socket = useRecoilValue(participantSocketRefState);
const [hilightedItemIndex, setHilightedItemIndex] = useState(0);

const roomid = new URLSearchParams(useLocation().search).get("roomid") || "999999";

if (type === "prompt") {
Expand All @@ -46,6 +58,20 @@ const LogContainer = ({ type, className }: LogContainerInterface) => {
console.log("프롬프트에 표시할 스크립트 로딩 실패", error);
});
}, []);

useLayoutEffect(() => {
let currentIndexOfPrompt =
scriptList.findIndex((value) => {
const startTime = Math.floor(+value.start / 1000) * 1000;

return startTime > progressMsTime;
}) - 1;
const lastStartTime = +scriptList[scriptList.length - 1]?.start;
if (Math.floor(lastStartTime / 1000) * 1000 <= progressMsTime) {
Comment on lines +65 to +70
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가능하면 아래 선언한 상수를 활용해서 이 부분도 매직넘버를 사용하지 않도록 고치면 좋을 것 같습니다.

currentIndexOfPrompt = scriptList.length - 1;
} else if (currentIndexOfPrompt < 0) setHilightedItemIndex(0);
setHilightedItemIndex(currentIndexOfPrompt);
}, [progressMsTime]);
} else {
useEffect(() => {
if (!logContainerRef.current) return;
Expand Down Expand Up @@ -75,7 +101,7 @@ const LogContainer = ({ type, className }: LogContainerInterface) => {
// 추후 사용자의 닉네임을 가져와야한다.
setQuestionList([...questionList, { title: "닉네임", contents: messageContents }]);

socket.emit("ask", {
socket?.emit("ask", {
type: "question",
roomId: roomid,
content: messageContents
Expand Down Expand Up @@ -104,9 +130,23 @@ const LogContainer = ({ type, className }: LogContainerInterface) => {
</ul>
)}
{type === "prompt" && (
<ul className="px-4 flex-grow overflow-y-auto ">
<ul className="px-4 flex-grow overflow-y-auto " ref={logContainerRef}>
{scriptList.map(({ start, text }, index) => {
return <LogItem key={`k-${index}`} title={convertMsToTimeString(start)} contents={text} />;
return (
<LogItem
key={`k-${index}`}
title={convertMsToTimeString(start)}
contents={text}
className={`cursor-point`}
style={{ borderColor: hilightedItemIndex === index ? "#4f4ffb" : "#e6e6e6" }}
onClick={(event: MouseEvent) => {
const currentTarget = event.currentTarget as HTMLLIElement;
if (!currentTarget.children[0].textContent) return;
convertTimeStringToMS(currentTarget.children[0].textContent);
setProgressMsTime(convertTimeStringToMS(currentTarget.children[0].textContent));
}}
/>
);
})}
</ul>
)}
Expand Down
45 changes: 37 additions & 8 deletions frontend/src/pages/Review/Review.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,68 @@
import { useSetRecoilState, useRecoilValue } from "recoil";
import { useRecoilValue } from "recoil";
import { useEffect, useRef } from "react";
import { fabric } from "fabric";

import CloseIcon from "@/assets/svgs/close.svg?react";
import ScriptIcon from "@/assets/svgs/whiteboard/script.svg?react";

import isQuestionLogOpenState from "@/stores/stateIsQuestionLogOpen";
import videoRefState from "../Test/components/stateVideoRef";

import LogToggleButton from "@/components/Button/LogToggleButton";
import LogContainer from "@/components/LogContainer/LogContainer";
import Header from "@/components/Header/Header";
import ProgressBar from "./components/ProgressBar";

// 추후 해당 다시보기의 전체 플레이 타임을 받아올 수 있어야 할 것 같습니다.
const TOTAL_MS_TIME_OF_REVIEW = 200000;
Comment on lines +15 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서버에 요청해서 받아오거나 음성 파일 길이로 계산해볼 수 있겠네요!


const Review = () => {
const setVideoRef = useSetRecoilState(videoRefState);
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const canvasContainerRef = useRef<HTMLDivElement>(null);
const isQuestionLogOpen = useRecoilValue(isQuestionLogOpenState);

useEffect(() => {
setVideoRef(videoRef);
if (!canvasContainerRef.current || !canvasRef.current) return;

const canvasContainer = canvasContainerRef.current;
// 캔버스 생성
const newCanvas = new fabric.Canvas(canvasRef.current, {
width: canvasContainer.offsetWidth,
height: canvasContainer.offsetHeight,
selection: false
});
newCanvas.backgroundColor = "lightgray";
newCanvas.defaultCursor = "default";

var text = new fabric.Text("화이트보드를 불러오고 있습니다", {
fontSize: 18,
textAlign: "center",
originX: "center",
originY: "center",
left: canvasContainer.offsetWidth / 2,
top: canvasContainer.offsetHeight / 2,
selectable: false
});
newCanvas.add(text);

// 언마운트 시 캔버스 정리
return () => {
newCanvas.dispose();
};
}, []);

return (
<>
<Header type="review" />
<section className="relative">
<video className="w-[100vw] h-[calc(100vh-5rem)]" autoPlay muted ref={videoRef}></video>
<section className="relative w-screen h-[calc(100vh-5rem)]" ref={canvasContainerRef}>
<canvas className="-z-10" ref={canvasRef} />
<LogContainer
type="prompt"
className={`absolute top-2.5 right-2.5 ${isQuestionLogOpen ? "block" : "hidden"}`}
/>
<LogToggleButton className="absolute top-2.5 right-2.5">
{isQuestionLogOpen ? <CloseIcon /> : <ScriptIcon fill="black" />}
</LogToggleButton>
<ProgressBar className="absolute bottom-2.5 left-1/2 -translate-x-1/2" />
<ProgressBar className="absolute bottom-2.5 left-1/2 -translate-x-1/2" totalTime={TOTAL_MS_TIME_OF_REVIEW} />
</section>
</>
);
Expand Down
81 changes: 74 additions & 7 deletions frontend/src/pages/Review/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,93 @@
import PlayIcon from "@/assets/svgs/progressPlay.svg?react";
import PauseIcon from "@/assets/svgs/progressPause.svg?react";
import { useState } from "react";

const ProgressBar = ({ className }: { className: string }) => {
import { useRecoilState } from "recoil";
import { useEffect, useRef, useState } from "react";
import { convertMsToTimeString } from "@/utils/convertMsToTimeString";

import progressMsTimeState from "@/stores/stateProgressMsTime";

const getPercentOfProgress = (progressTime: number, totalTime: number) => {
const percent = (progressTime / totalTime) * 100;
let result;

if (percent < 0) {
result = 0;
} else if (percent > 100) {
result = 100;
} else {
result = percent;
}

return result.toFixed(1) + "%";
};

const ProgressBar = ({ className, totalTime }: { className: string; totalTime: number }) => {
const [isPlaying, setIsPlaying] = useState(false);
const [progressMsTime, setProgressMsTime] = useRecoilState(progressMsTimeState);
const timerRef = useRef<any>();
const lastUpdatedTime = useRef<any>();
const UPDATE_INTERVAL_MS = 150;

const handleProgressBarMouseDown = (event: React.MouseEvent) => {
const { left, width } = event.currentTarget.getBoundingClientRect();
const mouseClickedX = event.clientX;
const percent = (mouseClickedX - left) / width;
setProgressMsTime(Math.round(totalTime * percent));
};

useEffect(() => {
if (isPlaying) {
lastUpdatedTime.current = new Date().getTime();

timerRef.current = setInterval(() => {
const dateNow = new Date().getTime();
const diffTime = dateNow - lastUpdatedTime.current;
setProgressMsTime((progressMsTime) => progressMsTime + diffTime);

lastUpdatedTime.current = dateNow;
}, UPDATE_INTERVAL_MS);
} else {
clearInterval(timerRef.current);
}
}, [isPlaying]);

useEffect(() => {
if (progressMsTime >= totalTime) {
setProgressMsTime(totalTime);
setIsPlaying(false);
clearInterval(timerRef.current);
}
}, [progressMsTime]);

return (
<div
className={`${className} flex gap-4 justify-between items-center w-[70vw] h-12 min-w-[400px] rounded-lg border border-grayscale-lightgray shadow-md p-4`}
className={`${className} flex gap-4 justify-between items-center w-[70vw] h-12 min-w-[400px] bg-grayscale-white rounded-lg border border-grayscale-lightgray shadow-md p-4`}
>
<button
type="button"
className="medium-12 w-8 p-2"
onClick={() => {
setIsPlaying(!isPlaying);
}}
disabled={progressMsTime >= totalTime ? true : false}
>
{isPlaying ? <PlayIcon /> : <PauseIcon />}
{isPlaying ? <PauseIcon /> : <PlayIcon fill={`${progressMsTime >= totalTime && "gray"}`} />}
</button>
<div className="relative grow h-1 bg-grayscale-lightgray">
<div className="absolute top-0 left-0 h-1 w-1/2 bg-boarlog-100"></div>
<div
className="flex h-4 grow items-center"
onMouseDown={(event) => {
handleProgressBarMouseDown(event);
}}
>
<div className="relative grow h-[6px] bg-grayscale-lightgray">
<div
className={`absolute top-0 left-0 h-[6px] w-[0%] bg-boarlog-100`}
style={{ width: `${getPercentOfProgress(progressMsTime, totalTime)}` }}
></div>
</div>
</div>
<span className="medium-12">00:00:00</span>
<span className="medium-12">{convertMsToTimeString(progressMsTime)}</span>
</div>
);
};
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/stores/stateProgressMsTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { atom } from "recoil";

const progressMsTimeState = atom<number>({
key: "progressMsTimeState",
default: 0
});

export default progressMsTimeState;
12 changes: 12 additions & 0 deletions frontend/src/utils/converTimeStringToMS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const MS_OF_SECOND = 1000;
const MS_OF_MINUTE = 60 * MS_OF_SECOND;
const MS_OF_HOUR = 60 * MS_OF_MINUTE;
Comment on lines +1 to +3
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수 선언 보기 좋습니다!


const convertTimeStringToMS = (timeString: string) => {
const [hour, minute, second] = timeString.split(":");
const result = parseInt(hour) * MS_OF_HOUR + parseInt(minute) * MS_OF_MINUTE + parseInt(second) * MS_OF_SECOND;

return result;
};

export default convertTimeStringToMS;
4 changes: 2 additions & 2 deletions frontend/src/utils/convertMsToTimeString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ const MS_OF_SECOND = 1000;
const SECOND_OF_HOUR = 3600;
const MINUTE_OF_HOUR = 60;

export const convertMsToTimeString = (ms: string) => {
let msNumber = parseInt(ms);
export const convertMsToTimeString = (ms: string | number) => {
let msNumber = typeof ms === "string" ? parseInt(ms) : ms;
let seconds = Math.floor(msNumber / MS_OF_SECOND);
let hours = Math.floor(seconds / SECOND_OF_HOUR);
seconds = seconds % 3600;
Expand Down