diff --git a/src/apis/user.ts b/src/apis/user.ts index 3c06989..626280a 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -15,7 +15,7 @@ import { FindEmailRequest, FindPasswordResponse, FindPasswordRequest, - CreateVideoAlarmRequest + CreateVideoAlarmRequest, } from '@/models/user'; import { AlarmResponse, @@ -93,11 +93,8 @@ export const createVideoAlarmAPI = ( PREFIX + `/videoAlarm/${videoId}/${status}`, data, ); -} +}; -export const findPasswordAPI = (data : FindPasswordRequest) => { - return axios.post( - PREFIX + '/findPassword', - data - ); -} +export const findPasswordAPI = (data: FindPasswordRequest) => { + return axios.post(PREFIX + '/findPassword', data); +}; diff --git a/src/components/Home/RecentVideos.tsx b/src/components/Home/RecentVideos.tsx index c627d8b..80cbaa0 100644 --- a/src/components/Home/RecentVideos.tsx +++ b/src/components/Home/RecentVideos.tsx @@ -13,9 +13,10 @@ import { IVideoProps } from 'types/videos'; interface IRecentVideosProp { videos: IVideoProps[]; + searchRef: React.RefObject; } -const RecentVideos = ({ videos }: IRecentVideosProp) => { +const RecentVideos = ({ videos, searchRef }: IRecentVideosProp) => { return (
@@ -39,7 +40,12 @@ const RecentVideos = ({ videos }: IRecentVideosProp) => { window.scrollTo({ top: 0, behavior: 'smooth' })} > -

영상 정리해보기

+

searchRef.current?.focus()} + > + 영상 정리해보기 +

)} diff --git a/src/components/Home/SearchYoutube.tsx b/src/components/Home/SearchYoutube.tsx index 78a24b9..679f7f6 100644 --- a/src/components/Home/SearchYoutube.tsx +++ b/src/components/Home/SearchYoutube.tsx @@ -1,8 +1,5 @@ import React, { useState, FormEvent } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; - -import { createVideoAPI } from '@/apis/videos'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import VideoIcon from '@/assets/icons/video.svg?react'; import WarningIcon from '@/assets/icons/warning.svg?react'; @@ -15,7 +12,7 @@ import { SearchContainer, } from '@/styles/HomepageStyle'; -import { recommendationModalState, errorModalState } from '@/stores/modal'; +import { recommendationModalState } from '@/stores/modal'; import { modelingDataState, @@ -23,22 +20,23 @@ import { modelingStatusState, videoLinkState, } from '@/stores/model-controller'; -import { userTokenState } from '@/stores/user'; import { validateYoutubeLink } from '@/utils/validation'; import ProgressBar from './ProgressBar'; +import useCreateVideo from '@/hooks/useCreateVideo'; -const SearchYoutube = () => { - const navigate = useNavigate(); +type Props = { + searchRef: React.RefObject; +}; - const userToken = useRecoilValue(userTokenState); +const SearchYoutube = ({ searchRef }: Props) => { const setIsOpenModal = useSetRecoilState(recommendationModalState); - const setIsOpenErrorModal = useSetRecoilState(errorModalState); + const setModelingData = useSetRecoilState(modelingDataState); const setVideoLink = useSetRecoilState(videoLinkState); const setProgress = useSetRecoilState(modelingProgressState); const [status, setStatus] = useRecoilState(modelingStatusState); - const [modelingData, setModelingData] = useRecoilState(modelingDataState); + const { createVideo } = useCreateVideo(); const [inputLink, setInputLink] = useState(''); @@ -97,28 +95,6 @@ const SearchYoutube = () => { setModelingData(null); }; - const handleClickCreateVideoButton = async () => { - if (!modelingData) return; - - if (userToken) { - try { - const { video_id } = (await createVideoAPI(modelingData)).data.result; - - navigate(`/summary/${video_id}`); - setModelingData(null); - } catch (e) { - console.error(e); - setIsOpenErrorModal(true); - } - } else { - navigate('/summary/guest'); - } - - setVideoLink(null); - setStatus('NONE'); - setProgress(0); - }; - return ( <> @@ -161,6 +137,7 @@ const SearchYoutube = () => { disabled={status === 'CONTINUE'} onChange={handleChangeInput} placeholder="https://youtube.com/..." + ref={searchRef} /> @@ -171,7 +148,7 @@ const SearchYoutube = () => { color: theme.color.gray500, backgroundColor: theme.color.green400, }} - onClick={handleClickCreateVideoButton} + onClick={createVideo} > 영상 읽기 diff --git a/src/components/layout/header/alarm/AlarmItem.tsx b/src/components/layout/header/alarm/AlarmItem.tsx index d026ac1..f59d184 100644 --- a/src/components/layout/header/alarm/AlarmItem.tsx +++ b/src/components/layout/header/alarm/AlarmItem.tsx @@ -11,12 +11,20 @@ import ErrorImage from '@/assets/Error.png'; import { Container } from '@/styles/layout/header/alarm/AlarmItem.style'; import { diffTime } from '@/utils/date'; +import { useRecoilValue } from 'recoil'; +import { + modelingProgressState, + modelingStatusState, +} from '@/stores/model-controller'; +import theme from '@/styles/theme'; +import { confirmSelectAlarmAPI } from '@/apis/user'; type Props = { alarm: IAlarm; selectIdList: number[]; onUpdateSelectIdList: (list: number[]) => void; onClose: () => void; + onRefresh: () => void; }; const AlarmItem = ({ @@ -24,8 +32,11 @@ const AlarmItem = ({ selectIdList, onUpdateSelectIdList, onClose, + onRefresh, }: Props) => { const navigate = useNavigate(); + const status = useRecoilValue(modelingStatusState); + const progress = useRecoilValue(modelingProgressState); const isSelected = selectIdList.indexOf(alarm.alarm_id) > -1; const type = () => { @@ -58,11 +69,21 @@ const AlarmItem = ({ return `${second}초`; }; - const handleClick = () => { + const handleClick = async () => { if (alarm.type === 'notice') { navigate('/guide'); onClose(); } + if (alarm.type === 'video' && !alarm.is_confirm && alarm.alarm_id !== 999) { + try { + await confirmSelectAlarmAPI({ alarms: [alarm.alarm_id] }); + onRefresh(); + navigate(`/summary/${alarm.video_id}`); + onClose(); + } catch (e) { + console.error(e); + } + } }; const handleClickRemoveButton: React.MouseEventHandler = ( @@ -110,6 +131,23 @@ const AlarmItem = ({ {alarm.content} + {status !== 'NONE' && alarm.alarm_id === 999 && ( +
+
+
+
+ + + {status === 'ERROR' ? '변환 중 오류' : `${progress}%`} + +
+ )} ); }; diff --git a/src/components/layout/header/alarm/AlarmList.tsx b/src/components/layout/header/alarm/AlarmList.tsx index 72336a5..41b5a53 100644 --- a/src/components/layout/header/alarm/AlarmList.tsx +++ b/src/components/layout/header/alarm/AlarmList.tsx @@ -75,6 +75,7 @@ const AlarmList = ({ alarmList, onRefresh, onClose }: Props) => { selectIdList={selectIdList} onUpdateSelectIdList={setSelectIdList} onClose={onClose} + onRefresh={onRefresh} /> ))}
diff --git a/src/components/layout/header/alarm/index.tsx b/src/components/layout/header/alarm/index.tsx index 365177c..b60c51d 100644 --- a/src/components/layout/header/alarm/index.tsx +++ b/src/components/layout/header/alarm/index.tsx @@ -1,20 +1,18 @@ import { useEffect, useState } from 'react'; -import { useRecoilValue } from 'recoil'; - -import { getAlarmAPI } from '@/apis/user'; +import { useRecoilState, useRecoilValue } from 'recoil'; import NotifyOffIcon from '@/assets/icons/notify-off.svg?react'; import NotifyOnIcon from '@/assets/icons/notify-on.svg?react'; import useOutsideClick from '@/hooks/useOutsideClick'; -import { IAlarm } from '@/models/alarm'; - import { modelingStatusState } from '@/stores/model-controller'; import * as HeaderStyle from '@/styles/layout/header'; import AlarmList from './AlarmList'; +import { userAlarmState } from '@/stores/user'; +import useGetAlarm from '@/hooks/useGetAlarm'; type Props = { isDark: boolean; @@ -23,26 +21,39 @@ type Props = { const Alarm = ({ isDark }: Props) => { const status = useRecoilValue(modelingStatusState); const [isOpen, setIsOpen] = useState(false); - const [alarmList, setAlarmList] = useState([]); + const [alarmList, setAlarmList] = useRecoilState(userAlarmState); + const { getAlarm } = useGetAlarm(); const [alarmRef] = useOutsideClick(() => setIsOpen(false)); const hasNotReadAlarm = alarmList.find((item) => !item.is_confirm); - const callAPI = async () => { - const { alarms } = (await getAlarmAPI()).data.result; - - setAlarmList(alarms); - }; - useEffect(() => { - callAPI(); + getAlarm(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - if (status === 'ERROR') { - callAPI(); + if (status === 'ERROR' || status === 'COMPLETE') { + getAlarm(); + } + + if (status === 'CONTINUE') { + setAlarmList([ + { + state: 'success', + type: 'video', + alarm_id: 999, + title: '열심히 영상을 변환하는 중이에요!', + content: '잠시후 멋진 글을 만날 수 있어요:)', + is_confirm: 0, + created_at: new Date().toString(), + updated_at: new Date().toString(), + }, + ...alarmList, + ]); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [status]); return ( @@ -61,7 +72,7 @@ const Alarm = ({ isDark }: Props) => { {isOpen && ( setIsOpen(false)} /> )} diff --git a/src/components/layout/sideBar/ConvertVideo.tsx b/src/components/layout/sideBar/ConvertVideo.tsx index 2b8cdca..aee3b06 100644 --- a/src/components/layout/sideBar/ConvertVideo.tsx +++ b/src/components/layout/sideBar/ConvertVideo.tsx @@ -1,8 +1,6 @@ import { useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; - -import { createVideoAPI } from '@/apis/videos'; +import { useLocation } from 'react-router-dom'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import VideoSvg from '@/assets/icons/video.svg?react'; import DownSvg from '@/assets/icons/down.svg?react'; @@ -11,30 +9,25 @@ import UpSvg from '@/assets/icons/up.svg?react'; import * as ConvertVideoStyle from '@/styles/layout/sideBar/ConvertVideo.style'; import { CommonTitle } from '@/styles/layout/sideBar/UserMode.style'; -import { errorModalState, recommendationModalState } from '@/stores/modal'; +import { recommendationModalState } from '@/stores/modal'; import { - modelingDataState, modelingProgressState, modelingStatusState, videoLinkState, } from '@/stores/model-controller'; -import { userTokenState } from '@/stores/user'; import theme from '@/styles/theme'; import { validateYoutubeLink } from '@/utils/validation'; +import useCreateVideo from '@/hooks/useCreateVideo'; const ConvertVideo = () => { const { pathname } = useLocation(); - const navigate = useNavigate(); - - const userToken = useRecoilValue(userTokenState); const setIsOpenModal = useSetRecoilState(recommendationModalState); const setVideoLink = useSetRecoilState(videoLinkState); - const setIsOpenErrorModal = useSetRecoilState(errorModalState); - const [status, setStatus] = useRecoilState(modelingStatusState); - const [progress, setProgress] = useRecoilState(modelingProgressState); - const [modelingData, setModelingData] = useRecoilState(modelingDataState); + const status = useRecoilValue(modelingStatusState); + const progress = useRecoilValue(modelingProgressState); + const { createVideo } = useCreateVideo(); const [isOpen, setIsOpen] = useState(false); const [isFocus, setIsFocus] = useState(false); @@ -42,28 +35,6 @@ const ConvertVideo = () => { const isValidate = validateYoutubeLink(url); - const handleClickCreateVideoButton = async () => { - if (!modelingData) return; - - if (userToken) { - try { - const { video_id } = (await createVideoAPI(modelingData)).data.result; - - navigate(`/summary/${video_id}`); - setModelingData(null); - } catch (e) { - setIsOpenErrorModal(true) - console.error(e); - } - } else { - navigate('/summary/guest'); - } - - setVideoLink(null); - setStatus('NONE'); - setProgress(0); - }; - const handleClickStartConvertButton: React.MouseEventHandler< HTMLButtonElement > = (e) => { @@ -114,7 +85,7 @@ const ConvertVideo = () => { color: theme.color.gray500, backgroundColor: theme.color.green400, }} - onClick={handleClickCreateVideoButton} + onClick={createVideo} > start diff --git a/src/hooks/useCreateVideo.ts b/src/hooks/useCreateVideo.ts new file mode 100644 index 0000000..75e6a6e --- /dev/null +++ b/src/hooks/useCreateVideo.ts @@ -0,0 +1,57 @@ +import { createVideoAlarmAPI } from '@/apis/user'; +import { createVideoAPI } from '@/apis/videos'; +import { errorModalState } from '@/stores/modal'; +import { + modelingDataState, + modelingProgressState, + modelingStatusState, + videoLinkState, +} from '@/stores/model-controller'; +import { userTokenState } from '@/stores/user'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import useGetAlarm from './useGetAlarm'; + +const useCreateVideo = () => { + const [modelingData, setModelingData] = useRecoilState(modelingDataState); + const userToken = useRecoilValue(userTokenState); + const setIsOpenErrorModal = useSetRecoilState(errorModalState); + const setVideoLink = useSetRecoilState(videoLinkState); + const setStatus = useSetRecoilState(modelingStatusState); + const setProgress = useSetRecoilState(modelingProgressState); + const { getAlarm } = useGetAlarm(); + const navigate = useNavigate(); + + const createVideo = async () => { + if (!modelingData) return; + + if (userToken) { + try { + const { video_id } = (await createVideoAPI(modelingData)).data.result; + + await createVideoAlarmAPI(video_id, 'success', { + title: `[${modelingData.title}]`, + content: + '영상이 모두 변환되었어요!\n이제 정리 된 영상을 확인하러 가볼까요?', + is_confirm: false, + }); + getAlarm(); + navigate(`/summary/${video_id}`); + setModelingData(null); + } catch (e) { + console.error(e); + setIsOpenErrorModal(true); + } + } else { + navigate('/summary/guest'); + } + + setVideoLink(null); + setStatus('NONE'); + setProgress(0); + }; + + return { createVideo }; +}; + +export default useCreateVideo; diff --git a/src/hooks/useGetAlarm.ts b/src/hooks/useGetAlarm.ts new file mode 100644 index 0000000..abca080 --- /dev/null +++ b/src/hooks/useGetAlarm.ts @@ -0,0 +1,16 @@ +import { getAlarmAPI } from '@/apis/user'; +import { userAlarmState } from '@/stores/user'; +import { useSetRecoilState } from 'recoil'; + +const useGetAlarm = () => { + const setAlarmList = useSetRecoilState(userAlarmState); + const getAlarm = async () => { + const { alarms } = (await getAlarmAPI()).data.result; + + setAlarmList(alarms); + }; + + return { getAlarm }; +}; + +export default useGetAlarm; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 3e77d6e..7feffd7 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { IVideoProps } from 'types/videos'; @@ -35,6 +35,7 @@ const HomePage: React.FC = () => { const [recentVideos, setRecentVideos] = useState([]); const [dummyVideos, setDummyVideos] = useState([]); const { createToast } = useCreateToast(); + const searchRef = useRef(null); const onFileClick = async ( videoId: number, @@ -71,7 +72,7 @@ const HomePage: React.FC = () => { return ( <> - +
{ backgroundColor: 'white', }} > - + ({ key: 'user-info', @@ -14,3 +15,8 @@ export const userTokenState = atom({ default: null, effects_UNSTABLE: [localStorageEffect], }); + +export const userAlarmState = atom({ + key: 'user-alarm', + default: [], +}); diff --git a/src/styles/layout/header/alarm/AlarmItem.style.ts b/src/styles/layout/header/alarm/AlarmItem.style.ts index 6b9496d..eb4e232 100644 --- a/src/styles/layout/header/alarm/AlarmItem.style.ts +++ b/src/styles/layout/header/alarm/AlarmItem.style.ts @@ -1,3 +1,4 @@ +import theme from '@/styles/theme'; import styled from 'styled-components'; export const Container = styled.div` @@ -119,7 +120,37 @@ export const Container = styled.div` & h1 { font-weight: bold; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + -webkit-line-clamp: 2; } } } + + & div.progress-wrap { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + } + + & div.progress-bar { + width: 100%; + height: 8px; + border-radius: 100px; + background-color: ${(props) => props.theme.color.gray100}; + overflow: hidden; + + & > div { + height: 100%; + transition: 1s; + transition-delay: 0.5s; + } + } + + & span.progress-text { + ${theme.typography.Caption3} + color: ${theme.color.gray400}; + } `;