Skip to content

Commit b8aab16

Browse files
authored
Merge pull request #241 from Byeonjin/feature/231209-progress-bar
Feat(#209, #240) ๋‹ค์‹œ๋ณด๊ธฐ ํŽ˜์ด์ง€ ํ”„๋กœ๊ทธ๋ž˜์Šค๋ฐ” ๋ฐ ํ”„๋กฌํ”„ํŠธ ๊ธฐ๋Šฅ ๊ตฌํ˜„
2 parents df17cf7 + 723cdb7 commit b8aab16

File tree

8 files changed

+183
-27
lines changed

8 files changed

+183
-27
lines changed

โ€Žfrontend/public/reviewLecture.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"text": "ํƒ€์ž์„์— ์œ„์น˜ํ•ด ์žˆ์Šต๋‹ˆ๋‹ค."
2525
},
2626
{
27-
"start": 57840,
27+
"start": 157840,
2828
"text": "๊ฐ€ ์ œ๊ธฐ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด 15m์˜ ์•ˆ์ „๋ฒจํŠธ๋ฅผ 20m๊นŒ์ง€ ๋†’์ด๋Š” ๊ณต์‚ฌ๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์ง€๋งŒ ๊ทผ๋ณธ์ ์ธ ํ•ด๊ฒฐ์ฑ…์ด ๋˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค."
2929
}
3030
]
Lines changed: 2 additions & 2 deletions
Loading

โ€Žfrontend/src/components/LogContainer/LogContainer.tsx

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
11
import SendMessage from "@/assets/svgs/sendMessage.svg?react";
22
import participantSocketRefState from "@/stores/stateParticipantSocketRef";
33

4-
import { useEffect, useRef, useState } from "react";
5-
import { useRecoilValue } from "recoil";
4+
import { CSSProperties, useEffect, useLayoutEffect, useRef, useState } from "react";
5+
import { useRecoilState, useRecoilValue } from "recoil";
66
import { useLocation } from "react-router-dom";
77
import { convertMsToTimeString } from "@/utils/convertMsToTimeString";
88
import axios from "axios";
9+
import progressMsTimeState from "@/stores/stateProgressMsTime";
10+
import convertTimeStringToMS from "@/utils/converTimeStringToMS";
911

1012
interface LogItemInterface {
1113
key?: string;
1214
title: string;
1315
contents: string;
16+
className?: string;
17+
onClick?: any;
18+
style?: CSSProperties;
1419
}
1520

1621
interface LogContainerInterface {
1722
type: "question" | "prompt";
1823
className: string;
1924
}
2025

21-
const LogItem = ({ title, contents }: LogItemInterface) => {
26+
const LogItem = ({ title, contents, className, onClick, style }: LogItemInterface) => {
2227
return (
23-
<li className="h-21 p-4 border mt-4 mb-2 first-of-type:mt-0 bg-grayscale-white border-grayscale-lightgray rounded-lg">
28+
<li
29+
className={`${className} h-21 p-4 border mt-4 mb-2 first-of-type:mt-0 bg-grayscale-white border-grayscale-lightgray rounded-lg`}
30+
style={style}
31+
onClick={onClick}
32+
>
2433
<p className="semibold-16">{title}</p>
2534
<p className="mt-2 medium-12 text-grayscale-darkgray">{contents}</p>
2635
</li>
@@ -33,7 +42,10 @@ const LogContainer = ({ type, className }: LogContainerInterface) => {
3342
const [scriptList, setScriptList] = useState<Array<{ start: string; text: string }>>([]);
3443
const messageInputRef = useRef<HTMLInputElement | null>(null);
3544
const logContainerRef = useRef<HTMLUListElement | null>(null);
45+
const [progressMsTime, setProgressMsTime] = useRecoilState(progressMsTimeState);
3646
const socket = useRecoilValue(participantSocketRefState);
47+
const [hilightedItemIndex, setHilightedItemIndex] = useState(0);
48+
3749
const roomid = new URLSearchParams(useLocation().search).get("roomid") || "999999";
3850

3951
if (type === "prompt") {
@@ -46,6 +58,20 @@ const LogContainer = ({ type, className }: LogContainerInterface) => {
4658
console.log("ํ”„๋กฌํ”„ํŠธ์— ํ‘œ์‹œํ•  ์Šคํฌ๋ฆฝํŠธ ๋กœ๋”ฉ ์‹คํŒจ", error);
4759
});
4860
}, []);
61+
62+
useLayoutEffect(() => {
63+
let currentIndexOfPrompt =
64+
scriptList.findIndex((value) => {
65+
const startTime = Math.floor(+value.start / 1000) * 1000;
66+
67+
return startTime > progressMsTime;
68+
}) - 1;
69+
const lastStartTime = +scriptList[scriptList.length - 1]?.start;
70+
if (Math.floor(lastStartTime / 1000) * 1000 <= progressMsTime) {
71+
currentIndexOfPrompt = scriptList.length - 1;
72+
} else if (currentIndexOfPrompt < 0) setHilightedItemIndex(0);
73+
setHilightedItemIndex(currentIndexOfPrompt);
74+
}, [progressMsTime]);
4975
} else {
5076
useEffect(() => {
5177
if (!logContainerRef.current) return;
@@ -75,7 +101,7 @@ const LogContainer = ({ type, className }: LogContainerInterface) => {
75101
// ์ถ”ํ›„ ์‚ฌ์šฉ์ž์˜ ๋‹‰๋„ค์ž„์„ ๊ฐ€์ ธ์™€์•ผํ•œ๋‹ค.
76102
setQuestionList([...questionList, { title: "๋‹‰๋„ค์ž„", contents: messageContents }]);
77103

78-
socket.emit("ask", {
104+
socket?.emit("ask", {
79105
type: "question",
80106
roomId: roomid,
81107
content: messageContents
@@ -104,9 +130,23 @@ const LogContainer = ({ type, className }: LogContainerInterface) => {
104130
</ul>
105131
)}
106132
{type === "prompt" && (
107-
<ul className="px-4 flex-grow overflow-y-auto ">
133+
<ul className="px-4 flex-grow overflow-y-auto " ref={logContainerRef}>
108134
{scriptList.map(({ start, text }, index) => {
109-
return <LogItem key={`k-${index}`} title={convertMsToTimeString(start)} contents={text} />;
135+
return (
136+
<LogItem
137+
key={`k-${index}`}
138+
title={convertMsToTimeString(start)}
139+
contents={text}
140+
className={`cursor-point`}
141+
style={{ borderColor: hilightedItemIndex === index ? "#4f4ffb" : "#e6e6e6" }}
142+
onClick={(event: MouseEvent) => {
143+
const currentTarget = event.currentTarget as HTMLLIElement;
144+
if (!currentTarget.children[0].textContent) return;
145+
convertTimeStringToMS(currentTarget.children[0].textContent);
146+
setProgressMsTime(convertTimeStringToMS(currentTarget.children[0].textContent));
147+
}}
148+
/>
149+
);
110150
})}
111151
</ul>
112152
)}

โ€Žfrontend/src/pages/Review/Review.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,68 @@
1-
import { useSetRecoilState, useRecoilValue } from "recoil";
1+
import { useRecoilValue } from "recoil";
22
import { useEffect, useRef } from "react";
3+
import { fabric } from "fabric";
34

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

78
import isQuestionLogOpenState from "@/stores/stateIsQuestionLogOpen";
8-
import videoRefState from "../Test/components/stateVideoRef";
99

1010
import LogToggleButton from "@/components/Button/LogToggleButton";
1111
import LogContainer from "@/components/LogContainer/LogContainer";
1212
import Header from "@/components/Header/Header";
1313
import ProgressBar from "./components/ProgressBar";
1414

15+
// ์ถ”ํ›„ ํ•ด๋‹น ๋‹ค์‹œ๋ณด๊ธฐ์˜ ์ „์ฒด ํ”Œ๋ ˆ์ด ํƒ€์ž„์„ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์–ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
16+
const TOTAL_MS_TIME_OF_REVIEW = 200000;
17+
1518
const Review = () => {
16-
const setVideoRef = useSetRecoilState(videoRefState);
17-
const videoRef = useRef<HTMLVideoElement>(null);
19+
const canvasRef = useRef<HTMLCanvasElement>(null);
20+
const canvasContainerRef = useRef<HTMLDivElement>(null);
1821
const isQuestionLogOpen = useRecoilValue(isQuestionLogOpenState);
1922

2023
useEffect(() => {
21-
setVideoRef(videoRef);
24+
if (!canvasContainerRef.current || !canvasRef.current) return;
25+
26+
const canvasContainer = canvasContainerRef.current;
27+
// ์บ”๋ฒ„์Šค ์ƒ์„ฑ
28+
const newCanvas = new fabric.Canvas(canvasRef.current, {
29+
width: canvasContainer.offsetWidth,
30+
height: canvasContainer.offsetHeight,
31+
selection: false
32+
});
33+
newCanvas.backgroundColor = "lightgray";
34+
newCanvas.defaultCursor = "default";
35+
36+
var text = new fabric.Text("ํ™”์ดํŠธ๋ณด๋“œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค", {
37+
fontSize: 18,
38+
textAlign: "center",
39+
originX: "center",
40+
originY: "center",
41+
left: canvasContainer.offsetWidth / 2,
42+
top: canvasContainer.offsetHeight / 2,
43+
selectable: false
44+
});
45+
newCanvas.add(text);
46+
47+
// ์–ธ๋งˆ์šดํŠธ ์‹œ ์บ”๋ฒ„์Šค ์ •๋ฆฌ
48+
return () => {
49+
newCanvas.dispose();
50+
};
2251
}, []);
2352

2453
return (
2554
<>
2655
<Header type="review" />
27-
<section className="relative">
28-
<video className="w-[100vw] h-[calc(100vh-5rem)]" autoPlay muted ref={videoRef}></video>
56+
<section className="relative w-screen h-[calc(100vh-5rem)]" ref={canvasContainerRef}>
57+
<canvas className="-z-10" ref={canvasRef} />
2958
<LogContainer
3059
type="prompt"
3160
className={`absolute top-2.5 right-2.5 ${isQuestionLogOpen ? "block" : "hidden"}`}
3261
/>
3362
<LogToggleButton className="absolute top-2.5 right-2.5">
3463
{isQuestionLogOpen ? <CloseIcon /> : <ScriptIcon fill="black" />}
3564
</LogToggleButton>
36-
<ProgressBar className="absolute bottom-2.5 left-1/2 -translate-x-1/2" />
65+
<ProgressBar className="absolute bottom-2.5 left-1/2 -translate-x-1/2" totalTime={TOTAL_MS_TIME_OF_REVIEW} />
3766
</section>
3867
</>
3968
);

โ€Žfrontend/src/pages/Review/components/ProgressBar.tsx

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,93 @@
11
import PlayIcon from "@/assets/svgs/progressPlay.svg?react";
22
import PauseIcon from "@/assets/svgs/progressPause.svg?react";
3-
import { useState } from "react";
43

5-
const ProgressBar = ({ className }: { className: string }) => {
4+
import { useRecoilState } from "recoil";
5+
import { useEffect, useRef, useState } from "react";
6+
import { convertMsToTimeString } from "@/utils/convertMsToTimeString";
7+
8+
import progressMsTimeState from "@/stores/stateProgressMsTime";
9+
10+
const getPercentOfProgress = (progressTime: number, totalTime: number) => {
11+
const percent = (progressTime / totalTime) * 100;
12+
let result;
13+
14+
if (percent < 0) {
15+
result = 0;
16+
} else if (percent > 100) {
17+
result = 100;
18+
} else {
19+
result = percent;
20+
}
21+
22+
return result.toFixed(1) + "%";
23+
};
24+
25+
const ProgressBar = ({ className, totalTime }: { className: string; totalTime: number }) => {
626
const [isPlaying, setIsPlaying] = useState(false);
27+
const [progressMsTime, setProgressMsTime] = useRecoilState(progressMsTimeState);
28+
const timerRef = useRef<any>();
29+
const lastUpdatedTime = useRef<any>();
30+
const UPDATE_INTERVAL_MS = 150;
31+
32+
const handleProgressBarMouseDown = (event: React.MouseEvent) => {
33+
const { left, width } = event.currentTarget.getBoundingClientRect();
34+
const mouseClickedX = event.clientX;
35+
const percent = (mouseClickedX - left) / width;
36+
setProgressMsTime(Math.round(totalTime * percent));
37+
};
38+
39+
useEffect(() => {
40+
if (isPlaying) {
41+
lastUpdatedTime.current = new Date().getTime();
42+
43+
timerRef.current = setInterval(() => {
44+
const dateNow = new Date().getTime();
45+
const diffTime = dateNow - lastUpdatedTime.current;
46+
setProgressMsTime((progressMsTime) => progressMsTime + diffTime);
47+
48+
lastUpdatedTime.current = dateNow;
49+
}, UPDATE_INTERVAL_MS);
50+
} else {
51+
clearInterval(timerRef.current);
52+
}
53+
}, [isPlaying]);
54+
55+
useEffect(() => {
56+
if (progressMsTime >= totalTime) {
57+
setProgressMsTime(totalTime);
58+
setIsPlaying(false);
59+
clearInterval(timerRef.current);
60+
}
61+
}, [progressMsTime]);
62+
763
return (
864
<div
9-
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`}
65+
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`}
1066
>
1167
<button
1268
type="button"
1369
className="medium-12 w-8 p-2"
1470
onClick={() => {
1571
setIsPlaying(!isPlaying);
1672
}}
73+
disabled={progressMsTime >= totalTime ? true : false}
1774
>
18-
{isPlaying ? <PlayIcon /> : <PauseIcon />}
75+
{isPlaying ? <PauseIcon /> : <PlayIcon fill={`${progressMsTime >= totalTime && "gray"}`} />}
1976
</button>
20-
<div className="relative grow h-1 bg-grayscale-lightgray">
21-
<div className="absolute top-0 left-0 h-1 w-1/2 bg-boarlog-100"></div>
77+
<div
78+
className="flex h-4 grow items-center"
79+
onMouseDown={(event) => {
80+
handleProgressBarMouseDown(event);
81+
}}
82+
>
83+
<div className="relative grow h-[6px] bg-grayscale-lightgray">
84+
<div
85+
className={`absolute top-0 left-0 h-[6px] w-[0%] bg-boarlog-100`}
86+
style={{ width: `${getPercentOfProgress(progressMsTime, totalTime)}` }}
87+
></div>
88+
</div>
2289
</div>
23-
<span className="medium-12">00:00:00</span>
90+
<span className="medium-12">{convertMsToTimeString(progressMsTime)}</span>
2491
</div>
2592
);
2693
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { atom } from "recoil";
2+
3+
const progressMsTimeState = atom<number>({
4+
key: "progressMsTimeState",
5+
default: 0
6+
});
7+
8+
export default progressMsTimeState;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const MS_OF_SECOND = 1000;
2+
const MS_OF_MINUTE = 60 * MS_OF_SECOND;
3+
const MS_OF_HOUR = 60 * MS_OF_MINUTE;
4+
5+
const convertTimeStringToMS = (timeString: string) => {
6+
const [hour, minute, second] = timeString.split(":");
7+
const result = parseInt(hour) * MS_OF_HOUR + parseInt(minute) * MS_OF_MINUTE + parseInt(second) * MS_OF_SECOND;
8+
9+
return result;
10+
};
11+
12+
export default convertTimeStringToMS;

โ€Žfrontend/src/utils/convertMsToTimeString.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ const MS_OF_SECOND = 1000;
22
const SECOND_OF_HOUR = 3600;
33
const MINUTE_OF_HOUR = 60;
44

5-
export const convertMsToTimeString = (ms: string) => {
6-
let msNumber = parseInt(ms);
5+
export const convertMsToTimeString = (ms: string | number) => {
6+
let msNumber = typeof ms === "string" ? parseInt(ms) : ms;
77
let seconds = Math.floor(msNumber / MS_OF_SECOND);
88
let hours = Math.floor(seconds / SECOND_OF_HOUR);
99
seconds = seconds % 3600;

0 commit comments

Comments
ย (0)