From be78ad6dc94076ba0d4524fe82eed6e5c22fa8e1 Mon Sep 17 00:00:00 2001 From: oikkoikk Date: Fri, 10 Nov 2023 18:29:31 +0900 Subject: [PATCH] [SWM-410] Feat : timestamp in lecturenote --- src/api/materials/materials.ts | 1 - src/components/course/CourseTaking.tsx | 22 +++++++------ src/components/course/YoutubePlayer.tsx | 12 +++++-- .../courseMaterial/CourseMaterialContent.tsx | 5 ++- .../courseMaterial/CourseMaterialDrawer.tsx | 8 ++++- .../CourseMaterialLectureNotes.tsx | 31 ++++++++++++++++++- .../LectureEnrollmentButton.tsx | 2 +- .../LectureEnrollmentModal.tsx | 2 +- .../lectureEnrollment/SchedulingModal.tsx | 2 +- src/constants/time/time.ts | 1 + .../convertCompactFormattedTimeToSeconds.ts | 14 +++++++++ 11 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 src/util/time/convertCompactFormattedTimeToSeconds.ts diff --git a/src/api/materials/materials.ts b/src/api/materials/materials.ts index 39ffe29..ef9528d 100644 --- a/src/api/materials/materials.ts +++ b/src/api/materials/materials.ts @@ -10,7 +10,6 @@ export async function fetchCourseMaterials(course_video_id: number) { headers }).then(async (res) => { if (res.ok) { - console.log('강의자료 api 호출! ', course_video_id) return (await res.json()) as Promise; } else { return fetchErrorHandling(res, ErrorMessage.COURSE_MATERIALS); diff --git a/src/components/course/CourseTaking.tsx b/src/components/course/CourseTaking.tsx index 8488591..cfe5502 100644 --- a/src/components/course/CourseTaking.tsx +++ b/src/components/course/CourseTaking.tsx @@ -11,6 +11,8 @@ import { showModalHandler } from '@/src/util/modal/modalHandler'; import { useQueryClient } from '@tanstack/react-query'; import { QueryKeys } from '@/src/api/queryKeys'; import { ONE_SECOND_IN_MS } from '@/src/constants/time/time'; +import convertCompactFormattedTimeToSeconds from '@/src/util/time/convertCompactFormattedTimeToSeconds'; +import { YouTubePlayer } from 'react-youtube'; type Props = { courseDetail: CourseDetail; @@ -25,15 +27,12 @@ export default function CourseTaking({ const last_view_video = findVideoById() as LastViewVideo; const currentPlayingVideo = last_view_video; - const lastVideoInCourse = - courseDetail.sections[courseDetail.sections.length - 1].videos[ - courseDetail.sections[courseDetail.sections.length - 1].videos.length - 1 - ]; const [prevPlayingVideo, setPrevPlayingVideo] = useState(last_view_video); const [nextPlayingVideo, setNextPlayingVideo] = useState(last_view_video); + const viewDuration = useRef( parseInt( sessionStorage.getItem( @@ -42,6 +41,7 @@ export default function CourseTaking({ ) ); const currentIntervalID = useRef(null); + const playerRef = useRef(null); function findVideoById() { const videos = courseDetail.sections.flatMap((section) => section.videos); @@ -138,11 +138,12 @@ export default function CourseTaking({ } }, 1 * ONE_SECOND_IN_MS); } - }, [ - courseDetail.course_id, - courseDetail.progress, - queryClient - ]); + }, [courseDetail.course_id, courseDetail.progress, queryClient]); + + const handleTimestampClick = (formattedTimestamp: string) => { + const timestamp = convertCompactFormattedTimeToSeconds(formattedTimestamp); + playerRef.current?.seekTo(timestamp, true); + }; return (
@@ -166,6 +167,7 @@ export default function CourseTaking({ viewDuration={viewDuration} is_completed={currentPlayingVideo.is_completed} currentIntervalID={currentIntervalID} + playerRef={playerRef} />
- + ); diff --git a/src/components/course/YoutubePlayer.tsx b/src/components/course/YoutubePlayer.tsx index 7c54b94..a400e8b 100644 --- a/src/components/course/YoutubePlayer.tsx +++ b/src/components/course/YoutubePlayer.tsx @@ -4,10 +4,11 @@ import { QueryKeys } from '@/src/api/queryKeys'; import { SessionStorageKeys } from '@/src/constants/courseTaking/courseTaking'; import { ONE_MINUTE_IN_MS, ONE_SECOND_IN_MS } from '@/src/constants/time/time'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; import { memo, useCallback, useEffect, useRef } from 'react'; import YouTube, { YouTubeEvent } from 'react-youtube'; import PlayerStates from 'youtube-player/dist/constants/PlayerStates'; -import { Options } from 'youtube-player/dist/types'; +import { Options, YouTubePlayer } from 'youtube-player/dist/types'; type Props = { width: number | string; @@ -20,6 +21,7 @@ type Props = { viewDuration: React.MutableRefObject; is_completed: boolean; currentIntervalID: React.MutableRefObject; + playerRef: React.MutableRefObject; }; const UPDATE_INTERVAL = 10; @@ -36,9 +38,12 @@ const YoutubePlayer = ({ end, viewDuration, is_completed, - currentIntervalID + currentIntervalID, + playerRef }: Props) => { const queryClient = useQueryClient(); + const router = useRouter(); + isCompleted = is_completed; const currentRevalidateID = useRef(null); @@ -164,6 +169,9 @@ const YoutubePlayer = ({ videoId={videoId} className='relative pb-[56.25%] pt-0 h-0 w-full' iframeClassName='absolute top-0 left-0 w-full h-full' + onReady={(event) => { + playerRef.current = event.target; + }} onStateChange={async (event) => { onPlayerStateChange(event); }} diff --git a/src/components/course/drawer/courseMaterial/CourseMaterialContent.tsx b/src/components/course/drawer/courseMaterial/CourseMaterialContent.tsx index 58d13a4..409a6fb 100644 --- a/src/components/course/drawer/courseMaterial/CourseMaterialContent.tsx +++ b/src/components/course/drawer/courseMaterial/CourseMaterialContent.tsx @@ -16,6 +16,7 @@ import LoadingSpinner from '@/src/components/ui/LoadingSpinner'; type Props = { courseVideoId: number; drawerHandler: () => void; + handleTimestampClick: (formattedTimestamp: string) => void; }; const STATUS = { @@ -28,7 +29,8 @@ const REFETCH_INTERVAL = 10 * ONE_SECOND_IN_MS; export default function CourseMaterialContent({ courseVideoId, - drawerHandler + drawerHandler, + handleTimestampClick }: Props) { const [activeTab, setActiveTab] = useState('lecture-notes'); @@ -81,6 +83,7 @@ export default function CourseMaterialContent({ )} {activeTab === 'quizzes' && ( diff --git a/src/components/course/drawer/courseMaterial/CourseMaterialDrawer.tsx b/src/components/course/drawer/courseMaterial/CourseMaterialDrawer.tsx index 02a6cde..b12ce28 100644 --- a/src/components/course/drawer/courseMaterial/CourseMaterialDrawer.tsx +++ b/src/components/course/drawer/courseMaterial/CourseMaterialDrawer.tsx @@ -13,6 +13,7 @@ import { COURSE_MATERIAL_BREAKPOINT } from '@/src/constants/window/window'; type Props = { courseVideoId: number; + handleTimestampClick: (formattedTimestamp: string) => void; }; const BOTTOM_SHEET_MAX_HEIGHT = 700; @@ -20,7 +21,10 @@ const BOTTOM_SHEET_RESIZE_MIN = 100; const DRAWER_MAX_WIDTH = 580; const DRAWER_RESIZE_MIN = 250; -export default function CourseMaterialDrawer({ courseVideoId }: Props) { +export default function CourseMaterialDrawer({ + courseVideoId, + handleTimestampClick +}: Props) { const controls = useAnimationControls(); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDragging, setIsDragging] = useState(false); @@ -214,6 +218,7 @@ export default function CourseMaterialDrawer({ courseVideoId }: Props) { @@ -245,6 +250,7 @@ export default function CourseMaterialDrawer({ courseVideoId }: Props) { diff --git a/src/components/course/drawer/courseMaterial/lectureNotes/CourseMaterialLectureNotes.tsx b/src/components/course/drawer/courseMaterial/lectureNotes/CourseMaterialLectureNotes.tsx index 25c93e9..015aba9 100644 --- a/src/components/course/drawer/courseMaterial/lectureNotes/CourseMaterialLectureNotes.tsx +++ b/src/components/course/drawer/courseMaterial/lectureNotes/CourseMaterialLectureNotes.tsx @@ -29,12 +29,14 @@ const MarkdownPreview = dynamic( type Props = { lectureNotes: LectureNote; courseVideoId: number; + handleTimestampClick: (formattedTimestamp: string) => void; }; const DEBOUNCE_TIME = ONE_SECOND_IN_MS / 2; export default function CourseMaterialLectureNotes({ lectureNotes, - courseVideoId + courseVideoId, + handleTimestampClick }: Props) { const sessionStorageContentKey = `${SessionStorageKeys.LECTURE_NOTES}-${courseVideoId}`; const sessionStorageIsEditModeKey = `${SessionStorageKeys.LECTURE_NOTES_IS_EDIT_MODE}-${courseVideoId}`; @@ -109,6 +111,33 @@ export default function CourseMaterialLectureNotes({ sessionStorageIsEditModeKey ]); + useEffect(() => { + const listeners: { [key: string]: () => void } = {}; + + setTimeout(() => { + const timestampButtons = document.querySelectorAll('button.timestamp'); + timestampButtons.forEach((timestampButton) => { + const clickHandler = () => { + handleTimestampClick(timestampButton.id); + }; + + listeners[timestampButton.id] = clickHandler; + + timestampButton.addEventListener('click', clickHandler); + }); + }, 500); + + return () => { + const timestampButtons = document.querySelectorAll('button.timestamp'); + timestampButtons.forEach((timestampButton) => { + const clickHandler = listeners[timestampButton.id]; + if (clickHandler) { + timestampButton.removeEventListener('click', clickHandler); + } + }); + }; + }); + useLayoutEffect(() => { setContent( () => diff --git a/src/components/lectureEnrollment/LectureEnrollmentButton.tsx b/src/components/lectureEnrollment/LectureEnrollmentButton.tsx index 74b2a9d..967482f 100644 --- a/src/components/lectureEnrollment/LectureEnrollmentButton.tsx +++ b/src/components/lectureEnrollment/LectureEnrollmentButton.tsx @@ -76,7 +76,7 @@ export default function LectureEnrollmentButton({ }, 5 * ONE_SECOND_IN_MS); setLectureEnrollToast(lecture_code, () => { clearTimeout(navigateToCourseTaking); - toast.remove('lecture_enrollment'); + toast.remove(`lecture_enrollment_${lecture_code}`); }); } queryClient.invalidateQueries([QueryKeys.DETAIL, lecture_code]); diff --git a/src/components/lectureEnrollment/LectureEnrollmentModal.tsx b/src/components/lectureEnrollment/LectureEnrollmentModal.tsx index d22d6be..ff17aa0 100644 --- a/src/components/lectureEnrollment/LectureEnrollmentModal.tsx +++ b/src/components/lectureEnrollment/LectureEnrollmentModal.tsx @@ -55,7 +55,7 @@ export default function LectureEnrollmentModal({ }, 5 * ONE_SECOND_IN_MS); setLectureEnrollToast(lecture_code, () => { clearTimeout(navigateToCourseTaking); - toast.remove('lecture_enrollment'); + toast.remove(`lecture_enrollment_${lecture_code}`); }); } queryClient.invalidateQueries([QueryKeys.DETAIL, lecture_code]); diff --git a/src/components/lectureEnrollment/SchedulingModal.tsx b/src/components/lectureEnrollment/SchedulingModal.tsx index 6d36dc5..2396600 100644 --- a/src/components/lectureEnrollment/SchedulingModal.tsx +++ b/src/components/lectureEnrollment/SchedulingModal.tsx @@ -68,7 +68,7 @@ export default function SchedulingModal({ }, 5 * ONE_SECOND_IN_MS); setLectureEnrollToast(lecture_code, () => { clearTimeout(navigateToCourseTaking); - toast.remove('lecture_enrollment'); + toast.remove(`lecture_enrollment_${lecture_code}`); }); } queryClient.invalidateQueries([ diff --git a/src/constants/time/time.ts b/src/constants/time/time.ts index 7b31982..fefefae 100644 --- a/src/constants/time/time.ts +++ b/src/constants/time/time.ts @@ -1,5 +1,6 @@ export const ONE_SECOND_IN_MS = 1000; //login +export const ONE_MINUTE = 60; export const ONE_HOUR = 60 * 60; export const ONE_DAY = 24 * ONE_HOUR; export const SESSION_AGE = ONE_DAY * 3 - ONE_HOUR; diff --git a/src/util/time/convertCompactFormattedTimeToSeconds.ts b/src/util/time/convertCompactFormattedTimeToSeconds.ts new file mode 100644 index 0000000..92eb50a --- /dev/null +++ b/src/util/time/convertCompactFormattedTimeToSeconds.ts @@ -0,0 +1,14 @@ +import { ONE_HOUR, ONE_MINUTE } from '@/src/constants/time/time'; + +export default function convertCompactFormattedTimeToSeconds( + formattedTime: string +) { + const len = formattedTime.length; + + const seconds = parseInt(formattedTime.substring(len - 2)); + const minutes = parseInt(formattedTime.substring(len - 4, len - 2)); + + const hours = len > 4 ? parseInt(formattedTime.substring(0, len - 4)) : 0; + + return hours * ONE_HOUR + minutes * ONE_MINUTE + seconds; +}