From ccf3999a93f99a6767c93f4a5b6cf79ddee3bd5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8C=E1=85=A5=E1=86=BC=E1=84=89=E1=85=A5=E1=86=BC?= =?UTF-8?q?=E1=84=92=E1=85=B1?= Date: Mon, 19 Feb 2024 02:41:05 +0900 Subject: [PATCH] =?UTF-8?q?feature-074:=20=EB=AA=A8=EB=8D=B8=EB=A7=81=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=A7=84=ED=96=89=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 7 +- package.json | 1 + src/assets/icons/pause.svg | 5 + src/components/Home/SearchYoutube.tsx | 9 +- .../SummaryDetailBox/SummaryDetailBox.tsx | 104 +++++++++++++++--- .../ScriptViewer/ScriptViewer.tsx | 50 +++++++-- .../SummaryScriptBox/ToolBox/ToolBox.tsx | 5 +- .../ModelController/ModelController.tsx | 34 ++++-- .../layout/header/alarm/AlarmItem.tsx | 30 ++++- .../layout/header/alarm/AlarmList.tsx | 4 +- src/components/layout/header/alarm/index.tsx | 22 +++- src/models/video.ts | 5 +- src/stores/summary.ts | 10 ++ src/styles/SummaryPage.ts | 31 +++++- src/utils/date.ts | 19 ++++ yarn.lock | 5 + 16 files changed, 292 insertions(+), 49 deletions(-) create mode 100644 src/assets/icons/pause.svg diff --git a/index.html b/index.html index 8769ef3..5b45a52 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,11 @@
- + + diff --git a/package.json b/package.json index 0a19650..70b7584 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@types/jest": "^29.5.11", "@types/node": "^20.11.17", "@types/react-modal": "^3.16.3", + "@types/youtube": "^0.0.50", "axios": "^1.6.4", "lodash": "^4.17.21", "react": "^18.2.0", diff --git a/src/assets/icons/pause.svg b/src/assets/icons/pause.svg new file mode 100644 index 0000000..d30f950 --- /dev/null +++ b/src/assets/icons/pause.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Home/SearchYoutube.tsx b/src/components/Home/SearchYoutube.tsx index 0ef4b36..779cb9b 100644 --- a/src/components/Home/SearchYoutube.tsx +++ b/src/components/Home/SearchYoutube.tsx @@ -91,6 +91,13 @@ const SearchYoutube = ({ searchRef }: Props) => { } }; + const handleChangeInput: React.ChangeEventHandler = (e) => { + setInputLink(e.target.value); + setVideoLink(null); + setStatus('NONE'); + setProgress(0); + }; + const handleClickCreateVideoButton = async () => { if (!modelingData) return; @@ -153,7 +160,7 @@ const SearchYoutube = ({ searchRef }: Props) => { type="text" value={inputLink} disabled={status === 'CONTINUE'} - onChange={(e) => setInputLink(e.target.value)} + onChange={handleChangeInput} placeholder="https://youtube.com/..." /> diff --git a/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx b/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx index 4714388..8358d15 100644 --- a/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx @@ -1,4 +1,5 @@ -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useEffect, useRef } from 'react'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { updateVideoCategoryIdAPI } from '@/apis/videos'; @@ -6,8 +7,10 @@ import { IVideo } from '@/models/video'; import { summaryIsEditingViewState, + summaryPlaySubHeadingIdState, summaryUpdateVideoState, summaryVideoState, + summaryVideoTimeState, } from '@/stores/summary'; import { toastListState } from '@/stores/toast'; @@ -24,9 +27,15 @@ type Props = { }; const SummaryDetailBox = ({ onRefresh }: Props) => { + const player = useRef(); + const summaryVideo = useRecoilValue(summaryVideoState) as IVideo; const summaryUpdateVideo = useRecoilValue(summaryUpdateVideoState); + const setSummaryVideoTime = useSetRecoilState(summaryVideoTimeState); const isEditingView = useRecoilValue(summaryIsEditingViewState); + const [playSubHeadingId, setPlaySubHeadingId] = useRecoilState( + summaryPlaySubHeadingIdState, + ); const [toastList, setToastList] = useRecoilState(toastListState); const subHeading = isEditingView @@ -50,6 +59,75 @@ const SummaryDetailBox = ({ onRefresh }: Props) => { } }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleMessage = (e: any) => { + if (e.origin === 'https://www.youtube.com') { + try { + const { info } = JSON.parse(e.data); + + if (!info) return; + + setPlaySubHeadingId((id) => { + const item = subHeading.find((s) => s.id === id); + + // END or PAUSE + if ([0, 2].includes(info.playerState)) { + return -1; + } + + if (item) { + if ( + item.start_time > info.currentTime || + info.currentTime > item.end_time + ) + return -1; + } + + return id; + }); + + setSummaryVideoTime(info.currentTime); + } catch (e) { + console.error(e); + } + } + }; + + useEffect(() => { + if (player.current) return; + + player.current = new YT.Player('player', { + videoId: summaryVideo.youtube_id, + }); + + window.onmessage = handleMessage; + + return () => { + setSummaryVideoTime(0); + setPlaySubHeadingId(-1); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [summaryVideo]); + + useEffect(() => { + if (player.current) { + if (playSubHeadingId > -1) { + const item = subHeading.find((s) => s.id === playSubHeadingId); + + if (item) { + player.current.seekTo(item.start_time, true); + player.current.playVideo(); + } + } else if (playSubHeadingId === -2) { + // 영상 멈추기 + player.current.pauseVideo(); + + setPlaySubHeadingId(-1); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [playSubHeadingId]); + return (
{ ))}
-