From 6a33fd02833714d848c20707428aaca4ee9ab1ed Mon Sep 17 00:00:00 2001 From: whistleJs Date: Tue, 20 Feb 2024 00:08:31 +0900 Subject: [PATCH 01/31] =?UTF-8?q?feature-074:=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EB=A7=81=20=EC=97=B0=EB=8F=99=20=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/videos.ts | 11 ++- src/components/Home/SearchYoutube.tsx | 1 + .../SummaryDetailBox/SummaryDetailBox.tsx | 12 ++- .../ModelController/ModelController.tsx | 44 ++++++----- src/components/modals/RecommendationModal.tsx | 17 ++++- src/models/modeling.ts | 33 ++++++++- src/pages/SummaryPage.tsx | 73 +++++++++++++++++-- src/stores/model-controller.ts | 5 +- src/utils/date.ts | 2 +- 9 files changed, 158 insertions(+), 40 deletions(-) diff --git a/src/apis/videos.ts b/src/apis/videos.ts index 4131996..c2e5092 100644 --- a/src/apis/videos.ts +++ b/src/apis/videos.ts @@ -1,4 +1,5 @@ import { APIBaseResponse, APIResponse } from '@/models/config/axios'; +import { ModelingFinalData } from '@/models/modeling'; import { IVideo, UpdateVideoCategoryRequest, @@ -13,7 +14,7 @@ import { IVideoProps } from 'types/videos'; const PREFIX = '/videos'; -export const createVideoAPI = (data: IVideo) => { +export const createVideoAPI = (data: ModelingFinalData) => { return axios.post>(PREFIX + `/new-video`, data); }; @@ -24,6 +25,10 @@ export const getVideoAPI = ( return axios.get>(PREFIX + `/${videoId}/${versionId}`); }; +export const getDummyVideoAPI = (videoId: string | number) => { + return axios.get>(PREFIX + `/dummyVideos/${videoId}/get`); +}; + export const deleteVideos = async (videos: number[] | undefined) => { const response = await axiosInstance.delete('/videos/selectDelete', { data: { videos }, @@ -76,6 +81,10 @@ export const getUnReadDummyVideosAPI = () => { return axios.get>('/videos/dummyVideos/unRead'); }; +export const getAllDummyVideosAPI = () => { + return axios.get>('/videos/dummyVideos'); +}; + export const getUnReadDummyVideos = async (): Promise< APIResponse> > => { diff --git a/src/components/Home/SearchYoutube.tsx b/src/components/Home/SearchYoutube.tsx index 779cb9b..df48f7f 100644 --- a/src/components/Home/SearchYoutube.tsx +++ b/src/components/Home/SearchYoutube.tsx @@ -96,6 +96,7 @@ const SearchYoutube = ({ searchRef }: Props) => { setVideoLink(null); setStatus('NONE'); setProgress(0); + setModelingData(null); }; const handleClickCreateVideoButton = async () => { diff --git a/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx b/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx index 8358d15..9feb236 100644 --- a/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx @@ -93,6 +93,14 @@ const SummaryDetailBox = ({ onRefresh }: Props) => { } }; + useEffect(() => { + return () => { + setSummaryVideoTime(0); + setPlaySubHeadingId(-1); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { if (player.current) return; @@ -102,10 +110,6 @@ const SummaryDetailBox = ({ onRefresh }: Props) => { window.onmessage = handleMessage; - return () => { - setSummaryVideoTime(0); - setPlaySubHeadingId(-1); - }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [summaryVideo]); diff --git a/src/components/common/ModelController/ModelController.tsx b/src/components/common/ModelController/ModelController.tsx index 90de4ea..f20dc83 100644 --- a/src/components/common/ModelController/ModelController.tsx +++ b/src/components/common/ModelController/ModelController.tsx @@ -1,11 +1,5 @@ import { useEffect, useRef } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - -import { - modelingProcess1, - modelingProcess2, - modelingProcess3, -} from '@/apis/video'; +import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil'; import { modelingDataState, @@ -13,10 +7,18 @@ import { modelingStatusState, videoLinkState, } from '@/stores/model-controller'; +import { + modelingProcess1, + modelingProcess2, + modelingProcess3, +} from '@/apis/video'; +import { userTokenState } from '@/stores/user'; + import { createVideoAlarmAPI } from '@/apis/user'; const ModelController = () => { const interval = useRef(); + const userToken = useRecoilValue(userTokenState); const [videoLink, setVideoLink] = useRecoilState(videoLinkState); const setModelingStatus = useSetRecoilState(modelingStatusState); const setModelingProgress = useSetRecoilState(modelingProgressState); @@ -32,11 +34,13 @@ const ModelController = () => { const handleError = async () => { try { - await createVideoAlarmAPI(0, 'fail', { - title: '앗, 영상 변환 중 오류가 생겼어요', - content: '어떤 문제인지 확인해보세요!', - is_confirm: false, - }); + if (userToken) { + await createVideoAlarmAPI(0, 'fail', { + title: '앗, 영상 변환 중 오류가 생겼어요', + content: '어떤 문제인지 확인해보세요!', + is_confirm: false, + }); + } } catch (e) { console.error(e); } @@ -45,6 +49,8 @@ const ModelController = () => { clearInterval(interval.current); } + clearInterval(interval.current); + setModelingStatus('ERROR'); setVideoLink(null); }; @@ -53,6 +59,8 @@ const ModelController = () => { if (!videoLink) return; const callProcess1API = async () => { + setModelingProgress(Math.ceil(Math.random() * 5)); + try { const { videoId } = (await modelingProcess1(videoLink)).data.result; @@ -83,11 +91,14 @@ const ModelController = () => { try { const { finalData } = (await modelingProcess3({ videoId })).data.result; - if (interval.current) { - clearInterval(interval.current); - } + clearInterval(interval.current); - setModelingData(finalData); + setModelingData({ + ...finalData, + youtube_id: videoId, + created_at: new Date().toString(), + updated_at: new Date().toString(), + }); setModelingProgress(100); setModelingStatus('COMPLETE'); } catch (e) { @@ -98,7 +109,6 @@ const ModelController = () => { }; setModelingStatus('CONTINUE'); - setModelingProgress(Math.ceil(Math.random() * 5)); callProcess1API(); startInterval(); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/modals/RecommendationModal.tsx b/src/components/modals/RecommendationModal.tsx index 209253a..21c32d5 100644 --- a/src/components/modals/RecommendationModal.tsx +++ b/src/components/modals/RecommendationModal.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { useSetRecoilState, useRecoilValue } from 'recoil'; -import { getUnReadDummyVideosAPI } from '@/apis/videos'; +import { getAllDummyVideosAPI, getUnReadDummyVideosAPI } from '@/apis/videos'; import CloseIcon from '@/assets/icons/close.svg?react'; import TransformationIcon from '@/assets/icons/transformation.svg?react'; @@ -12,10 +12,12 @@ import useOutsideClick from '@/hooks/useOutsideClick'; import { IVideo } from '@/models/video'; import { recommendationModalState } from '@/stores/modal'; +import { userTokenState } from '@/stores/user'; import { RecommendationModalContainer } from '@/styles/modals/RecommendationModal.style'; const RecommendationModal = () => { + const userToken = useRecoilValue(userTokenState); const setIsOpenModal = useSetRecoilState(recommendationModalState); const [dummyVideo, setDummyVideo] = useState(); @@ -25,10 +27,17 @@ const RecommendationModal = () => { useEffect(() => { const callAPI = async () => { + let videos: IVideo[] = []; + try { - const { videos } = (await getUnReadDummyVideosAPI()).data.result; const random = Math.round(Math.random() * (videos.length - 1)); + if (userToken) { + videos = (await getUnReadDummyVideosAPI()).data.result.videos; + } else { + videos = (await getAllDummyVideosAPI()).data.result.videos; + } + setDummyVideo(videos[random]); } catch (e) { console.error(e); @@ -36,7 +45,7 @@ const RecommendationModal = () => { }; callAPI(); - }, [setDummyVideo]); + }, [userToken, setDummyVideo]); return ( diff --git a/src/models/modeling.ts b/src/models/modeling.ts index 8c281b4..c8b16e3 100644 --- a/src/models/modeling.ts +++ b/src/models/modeling.ts @@ -1,5 +1,3 @@ -import { IVideo } from './video'; - export type ModelingStatus = | 'NONE' | 'CONTINUE' @@ -17,7 +15,36 @@ export interface ModelingProcessRequest { videoId: string; } +export interface ModelingSubHeading { + content: string; + end_time: number; + name: string; + start_time: number; +} + +export interface ModelingSummary { + content: string; +} + +export interface ModelingTag { + name: string; +} + +export interface ModelingFinalData { + description: string; + link: string; + subheading: ModelingSubHeading[]; + summary: ModelingSummary[]; + tag: ModelingTag[]; + title: string; + youtube_created_at: string; + youtube_id: string; + + created_at: string; + updated_at: string; +} + export interface ModelingResponse { - finalData: IVideo; + finalData: ModelingFinalData; message: string; } diff --git a/src/pages/SummaryPage.tsx b/src/pages/SummaryPage.tsx index d59d6b8..4abc89c 100644 --- a/src/pages/SummaryPage.tsx +++ b/src/pages/SummaryPage.tsx @@ -1,29 +1,54 @@ import { useCallback, useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useRecoilState } from 'recoil'; +import { useNavigate, useParams, useLocation } from 'react-router-dom'; +import { useRecoilState, useRecoilValue } from 'recoil'; -import { getVideoAPI } from '@/apis/videos'; +import { getDummyVideoAPI, getVideoAPI } from '@/apis/videos'; import { SummaryDetailBox } from '@/components/SummaryPage'; import { SummaryScriptBox } from '@/components/SummaryPage'; +import { IVideo } from '@/models/video'; + +import { modelingDataState } from '@/stores/model-controller'; import { summaryVideoState } from '@/stores/summary'; +import { userTokenState } from '@/stores/user'; import { Container } from '@/styles/SummaryPage'; const SummaryPage = () => { const navigate = useNavigate(); const { videoId } = useParams(); + const { search } = useLocation(); + + const userToken = useRecoilValue(userTokenState); + const [modelingData, setModelingData] = useRecoilState(modelingDataState); const [summaryVideo, setSummaryVideo] = useRecoilState(summaryVideoState); const callAPI = useCallback(async () => { if (!videoId) return; + const searchParam = new URLSearchParams(search); + const isInsight = searchParam.get('insight') === 'true'; + + let isSuccess = false; + let result: IVideo | null = null; + try { - const { isSuccess, result } = (await getVideoAPI(videoId)).data; + if (isInsight) { + const { data } = await getDummyVideoAPI(videoId); + + isSuccess = data.isSuccess; + result = data.result; + } else { + const { data } = await getVideoAPI(videoId); + + isSuccess = data.isSuccess; + result = data.result; + } if (!isSuccess) { navigate('/'); + return; } setSummaryVideo(result); @@ -31,15 +56,49 @@ const SummaryPage = () => { console.error(e); navigate('/'); } - }, [videoId, navigate, setSummaryVideo]); + }, [search, videoId, navigate, setSummaryVideo]); + + const setGuestSummaryVideo = () => { + if (!modelingData) { + navigate('/'); + return; + } + + const { subheading, tag, summary, ...others } = modelingData; + + setSummaryVideo({ + subHeading: subheading.map((item, id) => { + return { id, ...item }; + }), + tag: tag.map((item, id) => { + return { id, ...item }; + }), + summary: summary.map((item, id) => { + return { id, ...item }; + }), + video_id: 0, + image: '', + ...others, + }); + }; useEffect(() => { - callAPI(); + if (userToken) { + callAPI(); + } else { + if (videoId === 'guest') { + setGuestSummaryVideo(); + } else { + callAPI(); + } + } return () => { setSummaryVideo(null); + // setModelingData(null); }; - }, [callAPI, setSummaryVideo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userToken]); return ( diff --git a/src/stores/model-controller.ts b/src/stores/model-controller.ts index 4022a82..1989b8a 100644 --- a/src/stores/model-controller.ts +++ b/src/stores/model-controller.ts @@ -1,7 +1,6 @@ import { atom } from 'recoil'; -import { IVideo } from '@/models/video'; -import { ModelingStatus } from '@/models/modeling'; +import { ModelingFinalData, ModelingStatus } from '@/models/modeling'; import localStorageEffect from './effects/localStorageEffect'; @@ -20,7 +19,7 @@ export const modelingStatusState = atom({ default: 'NONE', }); -export const modelingDataState = atom({ +export const modelingDataState = atom({ key: 'modeling-data', default: null, effects_UNSTABLE: [localStorageEffect], diff --git a/src/utils/date.ts b/src/utils/date.ts index d7d55fb..828e2d6 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -43,7 +43,7 @@ export const getDate = (dateString?: string) => { export const formatTime = (time: number) => { const hour = Math.floor(time / 60 / 60); - const minute = Math.floor(time / 60); + const minute = Math.floor(time / 60) - hour * 60; const second = Math.floor(time % 60); if (hour > 0) { From 42c754d3be9629cf10555f19b8ad8b4477e0720e 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: Tue, 20 Feb 2024 00:56:35 +0900 Subject: [PATCH 02/31] =?UTF-8?q?feature-074:=20favicon=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- public/assets/favicon.png | Bin 0 -> 6517 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 public/assets/favicon.png diff --git a/index.html b/index.html index 5b45a52..31fcf3b 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Vi.No diff --git a/public/assets/favicon.png b/public/assets/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f46c21f171596254abe89efcec8e88c64d513e GIT binary patch literal 6517 zcmeG>XH-*Lv-<=HEr>`b6qWlaC_5v2wK(z|pqfb{wf-t~UGpYQKmFKeB3_MSa6XJ+=y?AepY7tD-!xWu>s06fNL zPG1558rpq0&JkrOxlblYuWh`H-acxgA zwng2h%bmRc$Y1ys!!ELT*7y|DP$IL}CQ>7vDC;CZ6i}5dZRf&1Dn29R{zgdB3ez8@ z%XJEs$C#+J=zKQ&c413nMO&&xI>AmA?Ves%?O)w&$J$w8vu97gV4=O`;0slJwYA~h zH(i^n7u?C`Ua-J!Z&$u?cWnH4I9WJi8D|3b=DYV$p4K5X6Fo$QcX-c)YA8Ve`eY)U>pZ=lqwOv|JH1IT|K zG8R-gUlJvgf=RcB-{*Xfj*!Aft>)xKMQZX5DOXnLj(j7L)*1}UK>sttFCF;c3sK?c z4cMs~wDeX_mD){{wfQ~QK-sa`w~imgYQL#Qhuet`QQzA|`JNY=^u^mw|lEfa@JF;Rwk{5V$rp6b9N4RO?#}o|K zu9HlZ&qy26h_Mz+VZYP6D)vHPx-GudYnji>cBV2{Y$8^eLhxB?)qUq`ZH#dz>?=*47oySsKXq zxLkeFB>-}b8B?q&oBMc>`puHQSZ*(R$G6e%%6MWKYDr=|mjZ8^)ECcqx1@WLqR^^>7 zKlwQ6(m6PH4Mk_ecI+cLV2j6f*p5{JA5#gpPu@IU{(0V_wdWupVSak;M`f+<=Kk@k zo{-sjK3TO?FE)@&6{wX?m9@0IvS+*qos=?b@2S%(3N+l(Ne(2(#*dAVS*cj&gsY{L zJUj5QBMn4f+PGq<#IEijD>A~Z7cAai-CdQ?NJYCd1VNC+60^HS@Tj?(*EGmFG3S%} zDlrO7W^-dN1`>P9A7rK6cf6ImA8e`G^FEO^6&z{`^RQlFd$sZ;MS)%6v%sO0`)1}X z7W0D`KzQ?p@|t8*qfUL5^GLFQA!Flq;c)@klGmGbvd&x z<^I4+|798v=<0rW^rHBQOYiJOCJskyqpFU5`f?O>^`{w2(vN@V=t3qUEru#wk`wN8 z*3v_{tqQ=4yVPK@RnQwSS9( z@ez8KuP7T+5hY(O1Za&fUT-d#>5>_T@-~CxpCb#WJ7pmd&`VVMz6Tn( z`vwC{X2vu7XHbFf#l?AcQ3qZO%Cd*d>UufAD+U-8h%b-Aik16p`I+dl=t;c&&wz7h zaBxI_3%HPf^kQESk{M!js#H;VI_hFT&*NA4YW?J>8wWnKpYAKxK%J- z95^7?vv7%Tz7_zX>$3Idtsr?`Hc+5f>&0UYLxi;YQ^Y@mdu>B%xezyk9xv+(tSiPc z5dt$9+Rp-onm7h5g;?JmI}5g`3h=`_qn*_eo#bu(itXu*npMXtDj%=*X10iyzYExs zrd^2jcF}!%WkX-Ee`VB1rOossn+P8vK31Shphvk)dyBCW3ROKva4@e@Sw$$%?rTPR z2=*)2n@X`no`aoC(ubbW0EqYkaP%_g>Sw)s(4sLwOZ6{hIq!e^rUTcJpponU`jIbqtZx(}2Npa%LjfKDM ztT84`kFl5#WeHqi)ddPw>rv;~i_6`4MEs56gdYC$5we= zO)qtcCLh*6)5Hn0&2m7{LN)gF?a*fD7nLZlvkE;~F_qG^#^=Q!8OPapke&TAK1H6E#AKJIk`_}dW&tJPgo@M8;K;aTHVm ziA-uhl&4i|KMx@CCyz@!F+4jz+k1Jjr#lB)3?GuQXFvJUV`oDhY9A?W~f|C zNa!{)m98zFiBsXg?Xo{IRfKj4xhIc+vl`BSEkYDOh4{UXLt61iy!F_R8Cm&j{q-5W{S|S9wST0rz?L|i1F zBvbI;_tSRSF#mO^Tl@rn*<>Cw&Gbaq*`HyMPby&8d1zoUKGDd4sPqD{vOLNpA2F0^ zZ%m2rG70n_rY+ir=V!a_M~t!ZME5;X=Y+H%-d4Gvwzo-lm()-EQPZyLLlJ+`3dRua zRY!_KON!;4VV%8%u8|@%;VG-gd*j5yC%WYQw=`V@Fv*8$ULg}H)penAuY3ot4WVVL z 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 literal 0 HcmV?d00001 From 1a5ebabb1acd2c4c03ca59a6e56ec6bdbcb1da2a 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: Tue, 20 Feb 2024 01:39:54 +0900 Subject: [PATCH 03/31] =?UTF-8?q?feature-074:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/SearchPage.tsx | 213 +++++++----- src/pages/SearchResultPage.tsx | 19 +- src/styles/SearchResult.ts | 581 ++++++++++++++++----------------- 3 files changed, 425 insertions(+), 388 deletions(-) diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index 88d60ea..ac399ca 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -5,99 +5,140 @@ import TagInput from '@/components/SearchPage/SearchComponent'; import { tagAPI } from '@/apis/search'; const SearchPage = () => { - const [tags, setTags] = useState([]); - const [input, setInput] = useState(''); - const [searchType, setSearchType] = useState(true); // True : keyword | False : hashTag - const [userHashTag, setUserHashTag] = useState([]); - const [selectedHashtags, setSelectedHashtags] = useState([]); - - useEffect(() => { - const handleTagAPI = async () => { - try { - const {data} = (await tagAPI()); - const extraData = data.result.map((item) => { - return item.name; - }); - const shuffleData = sortShuffle(extraData).slice(0,10); - setUserHashTag(shuffleData); + const [tags, setTags] = useState([]); + const [input, setInput] = useState(''); + const [searchType, setSearchType] = useState(true); // True : keyword | False : hashTag + const [userHashTag, setUserHashTag] = useState([]); + const [selectedHashtags, setSelectedHashtags] = useState([]); - } catch(e) { - setUserHashTag([]); - } - } - handleTagAPI(); - }, []); - - - const handleHashtagBox = (value : string) => { - const isSelected = selectedHashtags.includes(value); - setSelectedHashtags(prev => - isSelected ? prev.filter(idx => idx !== value) : [...prev, value] - ); - isSelected ? setTags(tags.filter((prev) => prev !== '#'+value)) : setTags([...tags, `#${value}`]); - setSearchType(false); // 박스를 클릭했을 때도 type 변경 - } - - const sortShuffle = (arr : string[]) => { - return arr.sort(() => Math.random() - 0.5); + useEffect(() => { + const handleTagAPI = async () => { + try { + const { data } = await tagAPI(); + const extraData = data.result.map((item) => { + return item.name; + }); + const shuffleData = sortShuffle(extraData).slice(0, 10); + setUserHashTag(shuffleData); + } catch (e) { + setUserHashTag([]); } - const firstHalf = userHashTag.slice(0, 5); - const secondHalf = userHashTag.slice(5); + }; + handleTagAPI(); + }, []); - return ( - -
-
-
-
- 찾고 싶은 키워드가 있나요? - 찾고자 하는 키워드를 검색하면 관련 영상을 찾아드릴게요 -
+ const handleHashtagBox = (value: string) => { + const isSelected = selectedHashtags.includes(value); + setSelectedHashtags((prev) => + isSelected ? prev.filter((idx) => idx !== value) : [...prev, value], + ); + isSelected + ? setTags(tags.filter((prev) => prev !== '#' + value)) + : setTags([...tags, `#${value}`]); + setSearchType(false); // 박스를 클릭했을 때도 type 변경 + }; -
-
-
- -
-
-
- -
- {(input.length === 0 && tags.length === 0) ? ( - - <> - 1. 키워드 검색 : 키워드를 검색하면 해당 키워드가 언급 된 영상들을 찾아드려요! -
- 2. 해시태그 검색 : #을 함께 검색하면 해당 해시태그가 있는 영상들을 찾아드려요! - -
- ) - : '' - } -
+ const sortShuffle = (arr: string[]) => { + return arr.sort(() => Math.random() - 0.5); + }; + const firstHalf = userHashTag.slice(0, 5); + const secondHalf = userHashTag.slice(5); -
-
- { - firstHalf.map((value : string, idx : number) => { - return( handleHashtagBox(value)} - className={selectedHashtags.includes(value) ? 'toggle' : ''}>{'#' + value}) - }) - } -
-
- { - secondHalf.map((value : string, idx : number) => { - return( handleHashtagBox(value)} - className={selectedHashtags.includes(value) ? 'toggle' : ''}>{'#' + value}) - }) - } -
+ return ( + +
+
+
+
+ + 찾고 싶은 키워드가 있나요? + + + 찾고자 하는 키워드를 검색하면 관련 영상을 찾아드릴게요 + +
+ +
+
+
+
+
- -); +
+ {input.length === 0 && tags.length === 0 ? ( + + <> + 1. 키워드 검색 : 키워드를 검색하면 해당 키워드가 언급 된 + 영상들을 찾아드려요! +
+ 2. 해시태그 검색 : #을 함께 검색하면 해당 해시태그가 있는 + 영상들을 찾아드려요! + +
+ ) : ( + '' + )} +
+ +
+
+ {firstHalf.map((value: string, idx: number) => { + return ( + handleHashtagBox(value)} + className={selectedHashtags.includes(value) ? 'toggle' : ''} + > + {'#' + value} + + ); + })} +
+
+ {secondHalf.map((value: string, idx: number) => { + return ( + handleHashtagBox(value)} + className={selectedHashtags.includes(value) ? 'toggle' : ''} + > + {'#' + value} + + ); + })} +
+
+
+
+ ); }; export default SearchPage; diff --git a/src/pages/SearchResultPage.tsx b/src/pages/SearchResultPage.tsx index c6dd4d9..82e331e 100644 --- a/src/pages/SearchResultPage.tsx +++ b/src/pages/SearchResultPage.tsx @@ -39,7 +39,7 @@ const SearchResult = () => { case 'hashtag': const tagValues = searchParams.get('value') as string; const tagtype = searchParams.get('type') as string; - + setTags(tagValues.replace(/\s+/g, '').split('&')); setSearchType(false); handleSearchAPI(tagValues, tagtype, '&'); @@ -52,7 +52,7 @@ const SearchResult = () => { // 기타 에러 } }, [location.search]); - + const handleSearchAPI = async ( inputValues: string, type: string, @@ -81,7 +81,7 @@ const SearchResult = () => { const formatContent = (content: string, keyword: string) => { let result = escapeHTML(content); const keywordArr = keyword.split(' '); - + keywordArr.forEach((keyword) => { if (keyword.trim() !== '') { result = result @@ -89,17 +89,18 @@ const SearchResult = () => { .join(`${escapeHTML(keyword)}`); } }); - + result = result.replace(/\n/g, '
'); - + return result; }; const dataDuplicateHandler = (videos: IVideo[], check: string) => { - const uniqueData = videos.filter((v, index, arr) => - arr.findIndex(t => t.video_id === v.video_id) === index + const uniqueData = videos.filter( + (v, index, arr) => + arr.findIndex((t) => t.video_id === v.video_id) === index, ); - + const mappingData = uniqueData.map((video) => { return { ...video, @@ -157,7 +158,7 @@ const SearchResult = () => { ) : ( data.map((item, index) => ( - + )) )}
diff --git a/src/styles/SearchResult.ts b/src/styles/SearchResult.ts index 07379d2..10befac 100644 --- a/src/styles/SearchResult.ts +++ b/src/styles/SearchResult.ts @@ -1,305 +1,300 @@ -import styled from "styled-components"; -import theme from "./theme"; +import styled from 'styled-components'; +import theme from './theme'; const Container = styled.div` - display : flex; - flex : 1 1 auto; - flex-direction : column; - - & mark { - color : ${(props) => props.theme.color.green600} !important; - background : transparent; - } - - & div.inputContainer { - display: flex; - flex-direction: column; - align-items: center; - padding: 40px 0px 40px; - gap: 40px; - - background: ${(props) => props.theme.color.white}; + display: flex; + flex: 1 1 auto; + flex-direction: column; + + & mark { + color: ${(props) => props.theme.color.green600} !important; + background: transparent; + } + + & div.inputContainer { + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 0px 40px; + gap: 40px; + + background: ${(props) => props.theme.color.white}; + } + & div.inputwrap { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + gap: 20px; + + background: ${(props) => props.theme.color.gray100}; + border-radius: 12px; + white-space: nowrap; + overflow: hidden; + } + + & div.inputwrap:hover { + box-shadow: 1px 1px 20px ${(props) => props.theme.color.gray100}; + } + + & div.input-inner { + display: flex; + justify-content: space-between; + white-space: nowrap; + } + + & div.input { + display: flex; + gap: 10px; + } + + & input::placeholder { + ${(props) => props.theme.typography.Subheader2}; + + color: ${(props) => props.theme.color.gray300}; + } + + & button.search-btn { + ${(props) => props.theme.typography.Body1}; + + color: ${(props) => props.theme.color.white}; + + background: ${(props) => props.theme.color.gray500}; + + border-radius: 8px; + order: 1; + border: 0; + &:hover { + background-color: ${theme.color.green500}; + color: ${theme.color.gray500}; } - & div.inputwrap { - display : flex; - flex-direction : column; - justify-content: center; - align-items: center; - padding: 0px; - gap: 20px; - - background: ${(props) => props.theme.color.gray100}; - border-radius: 12px; - white-space: nowrap; - overflow: hidden; + } + + & button:disabled { + background: ${(props) => props.theme.color.gray300}; + } + + & div.result { + display: flex; + min-height: 800px; + overflow: scroll; + align-items: center; + flex-direction: column; + gap: 10px; + } + + & div.filter { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + padding: 0px; + gap: 12px; + & span { + ${(props) => props.theme.typography.Body3}; + color: ${(props) => props.theme.color.gray300}; } - - & div.inputwrap:hover { - box-shadow: 1px 1px 20px ${(props) => props.theme.color.gray100}; - } - - & div.input-inner { - display : flex; - justify-content : space-between; - white-space: nowrap; - } - - & div.input { - display : flex; - gap : 10px; - } - - & input::placeholder { - ${(props) => props.theme.typography.Subheader2}; - - color: ${(props) => props.theme.color.gray300}; - } - - & button.search-btn { - - ${(props) => props.theme.typography.Body1}; - - color: ${(props) => props.theme.color.white}; - - - background : ${(props) => props.theme.color.gray500}; - - border-radius: 8px; - order : 1; - border : 0; - &:hover { - background-color : ${theme.color.green500}; - color : ${theme.color.gray500}; - } - } - - & button:disabled { - background: ${(props) => props.theme.color.gray300}; - } - - & div.result { - display: flex; - min-height : 800px; - overflow : scroll; - align-items : center; - flex-direction: column; - gap : 10px; - } - - & div.filter { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; - padding: 0px; - gap: 12px; - & span { - ${(props) => props.theme.typography.Body3}; - color : ${(props) => props.theme.color.gray300}; - } - } - & div.content { - display : flex; - flex-direction : column; - padding : 10px 0 10px 0; - gap : 20px; - } + } + & div.content { + display: flex; + flex-direction: column; + padding: 10px 0 10px 0; + gap: 20px; + } `; const VideoCard = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; - padding: 0px; - - background : ${(props) => props.theme.color.white} - flex: none; - order: 1; - flex-grow: 0; - - box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.05); - border-radius: 16px; - - & div.main { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 24px; - gap: 24px; - - background : transparent; - - flex: none; - order: 0; - flex-grow: 1; - } - - & div.user { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 0px 0px 0px 24px; - gap: 8px; - - flex: none; - order: 0; - flex-grow: 0; - } - - & span.userName { - - ${(props) => props.theme.typography.Caption1}; - color: ${(props) => props.theme.color.gray300}; - flex: none; - order: 0; - flex-grow: 0; - } - - & span.contour { - border: 1px solid ${(props) => props.theme.color.gray300}; - - flex: none; - order: 1; - flex-grow: 0; - } - - & span.userDate { - ${(props) => props.theme.typography.Caption1}; - - color: ${(props) => props.theme.color.gray300}; - - flex: none; - order: 2; - flex-grow: 0; - } - - & div.content { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 0px; - gap: 8px; - - flex: none; - order: 1; - align-self: stretch; - flex-grow: 0; - } - - & div.title { - ${(props) => props.theme.typography.Subheader3}; - - color: ${(props) => props.theme.color.gray500}; - - flex: none; - order: 0; - align-self: stretch; - flex-grow: 0; - } - - & div.subtitle { - ${(props) => props.theme.typography.Body3}; - - color: ${(props) => props.theme.color.gray400}; - - flex: none; - order: 1; - align-self: stretch; - flex-grow: 0; - } - - & div.subcontent { - ${(props) => props.theme.typography.Body3}; - - color: ${(props) => props.theme.color.gray300}; - overflow : hidden; - text-overflow : ellipsis; - flex: none; - order: 2; - align-self: stretch; - flex-grow: 0; - } - - & div.hashtag { - display: flex; - flex-direction: row; - align-items: flex-start; - padding: 0px; - gap: 8px; - order : 2; - } - - & div.imgBox { - padding: 0; - width : 213px; - height : 254px; - border-radius : 0px 16px 16px 0px; - overflow:hidden; - margin:0 auto; - } - & img { - width : 100%; - height : 100%; - object-fit: cover; - } -` + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + padding: 0px; + + background: ${(props) => props.theme.color.white}; + flex: none; + order: 1; + flex-grow: 0; + + box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.05); + border-radius: 16px; + cursor: pointer; + + & div.main { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 24px; + gap: 24px; + + background: transparent; + + flex: none; + order: 0; + flex-grow: 1; + } + + & div.user { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0px 0px 0px 24px; + gap: 8px; + + flex: none; + order: 0; + flex-grow: 0; + } + + & span.userName { + ${(props) => props.theme.typography.Caption1}; + color: ${(props) => props.theme.color.gray300}; + flex: none; + order: 0; + flex-grow: 0; + } + + & span.contour { + border: 1px solid ${(props) => props.theme.color.gray300}; + + flex: none; + order: 1; + flex-grow: 0; + } + + & span.userDate { + ${(props) => props.theme.typography.Caption1}; + + color: ${(props) => props.theme.color.gray300}; + + flex: none; + order: 2; + flex-grow: 0; + } + + & div.content { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 8px; + + flex: none; + order: 1; + align-self: stretch; + flex-grow: 0; + } + + & div.title { + ${(props) => props.theme.typography.Subheader3}; + + color: ${(props) => props.theme.color.gray500}; + + flex: none; + order: 0; + align-self: stretch; + flex-grow: 0; + } + + & div.subtitle { + ${(props) => props.theme.typography.Body3}; + + color: ${(props) => props.theme.color.gray400}; + + flex: none; + order: 1; + align-self: stretch; + flex-grow: 0; + } + + & div.subcontent { + ${(props) => props.theme.typography.Body3}; + + color: ${(props) => props.theme.color.gray300}; + overflow: hidden; + text-overflow: ellipsis; + flex: none; + order: 2; + align-self: stretch; + flex-grow: 0; + } + + & div.hashtag { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: 0px; + gap: 8px; + order: 2; + } + + & div.imgBox { + padding: 0; + width: 213px; + height: 254px; + border-radius: 0px 16px 16px 0px; + overflow: hidden; + margin: 0 auto; + } + & img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; const hashtagBox = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 6px 10px; - gap: 10px; - - height: 31px; - - background: ${(props) => props.theme.color.gray100}; - color : ${(props) => props.theme.color.gray400}; - border-radius: 8px; - - flex: none; - order: 2; - flex-grow: 0; - - ${(props) => props.theme.typography.Caption1}; - ` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 6px 10px; + gap: 10px; -const SearchNotFoundContainer = styled.div` - width : 500px; - height : 337px; - display : flex; - flex-direction : column; - justify-content: center; - align-items: center; - gap : 40px; - - & div.text { - ${(props) => props.theme.typography.Header3}; - display : flex; - flex-direction : column; - justify-content: center; - align-items: center; - } - & span.user { - color : ${(props) => props.theme.color.gray300}; - text-align : center; - } - - & button { - width : 235px; - height : 56px; - background : ${(props) => props.theme.color.gray500}; - color : ${(props) => props.theme.color.white}; - border : none; - border-radius : 100px; - padding: 12px 32px; - gap: 10px; - ${(props) => props.theme.typography.Subheader2}; - } - ` -export default {Container, VideoCard, hashtagBox, SearchNotFoundContainer}; + height: 31px; + + background: ${(props) => props.theme.color.gray100}; + color: ${(props) => props.theme.color.gray400}; + border-radius: 8px; + flex: none; + order: 2; + flex-grow: 0; + ${(props) => props.theme.typography.Caption1}; +`; +const SearchNotFoundContainer = styled.div` + width: 500px; + height: 337px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 40px; + + & div.text { + ${(props) => props.theme.typography.Header3}; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + & span.user { + color: ${(props) => props.theme.color.gray300}; + text-align: center; + } + + & button { + width: 235px; + height: 56px; + background: ${(props) => props.theme.color.gray500}; + color: ${(props) => props.theme.color.white}; + border: none; + border-radius: 100px; + padding: 12px 32px; + gap: 10px; + ${(props) => props.theme.typography.Subheader2}; + } +`; +export default { Container, VideoCard, hashtagBox, SearchNotFoundContainer }; From 9862d0c6ca1ca88d99cf0442fea817a9791b6115 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: Tue, 20 Feb 2024 01:40:23 +0900 Subject: [PATCH 04/31] =?UTF-8?q?feature-074:=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=84=A0=ED=83=9D=20=EB=B0=95=EC=8A=A4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CategorySelectBox/CategorySelectBox.tsx | 24 +++++++------------ src/components/category/Card.tsx | 18 +++----------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx index cd9acfe..f6c7079 100644 --- a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx @@ -12,18 +12,16 @@ import { userTokenState } from '@/stores/user'; import { CategoryDropdown } from './CategoryDropdown'; type Props = { + size?: 'SMALL' | 'LARGE'; disabled?: boolean; selectedCategoryId?: number; - startSelect?: boolean; - setStartSelect?: React.Dispatch>; onSelect: (categoryId: number, categoryName?: string) => void; }; const CategorySelectBox = ({ + size, disabled, selectedCategoryId, - startSelect, - setStartSelect, onSelect, }: Props) => { const userToken = useRecoilValue(userTokenState); @@ -59,17 +57,11 @@ const CategorySelectBox = ({ const handleSelect = (categoryId: number) => { setSelectedId(categoryId); - setStartSelect && setStartSelect(true); setIsOpen(false); }; const handleClick = () => { - if ( - !selectedCategory || - selectedId === selectedCategoryId || - disabled || - !startSelect - ) + if (!selectedCategory || selectedId === selectedCategoryId || disabled) return; onSelect(selectedCategory.categoryId, selectedCategory.name); @@ -92,7 +84,9 @@ const CategorySelectBox = ({ {userToken ? selectedCategory ? selectedCategory.name - : '어떤 카테고리에 넣을까요?' + : size === 'SMALL' + ? '카테고리 선택' + : '어떤 카테고리에 넣을까요?' : '로그인하고 요약한 영상을 아카이빙해요!'} @@ -105,9 +99,9 @@ const CategorySelectBox = ({
diff --git a/src/components/category/Card.tsx b/src/components/category/Card.tsx index ea7e021..a132950 100644 --- a/src/components/category/Card.tsx +++ b/src/components/category/Card.tsx @@ -1,11 +1,8 @@ -import React, { useState } from 'react'; -import { useRecoilValue } from 'recoil'; +import React from 'react'; import { IVideoProps } from 'types/videos'; import { CategorySelectBox } from '@/components/SummaryPage/SummaryDetailBox/CategorySelectBox'; -import { categoryState } from '@/stores/category'; - import * as CardStyles from '@/styles/category/Card.style'; import Chip from '../common/chip/Chip'; @@ -28,15 +25,7 @@ const Card: React.FC = ({ setCheckedVideos, onFileClick, }) => { - const category = useRecoilValue(categoryState); - - const [selectedCategoryId, setSelectedCategoryId] = useState( - category.length ? category[0].categoryId : -1, - ); - const [startSelect, setStartSelect] = useState(false); - const onFileClickWithProps = (categoryId: number, categoryName?: string) => { - setSelectedCategoryId(categoryId); onFileClick && onFileClick(video.video_id, categoryId, categoryName); }; @@ -80,9 +69,8 @@ const Card: React.FC = ({ {mode === 'recommend' && ( From 2727c81c1c471c563ac65be9dfae926ecb27f22b 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: Tue, 20 Feb 2024 01:59:21 +0900 Subject: [PATCH 05/31] =?UTF-8?q?feature-074:=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EB=A7=81=ED=81=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/category/Card.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/category/Card.tsx b/src/components/category/Card.tsx index a132950..85fc000 100644 --- a/src/components/category/Card.tsx +++ b/src/components/category/Card.tsx @@ -54,9 +54,7 @@ const Card: React.FC = ({ {video.title} {video.description} From 24711bce040519053b9162fc3a34235cf1a92753 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: Tue, 20 Feb 2024 02:33:14 +0900 Subject: [PATCH 06/31] =?UTF-8?q?feature-074:=20Indicator=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SearchPage/SearchResultBox.tsx | 113 +++++++++++------- .../ScriptViewer/ScriptViewer.tsx | 2 +- .../SummaryScriptBox/SummaryScriptBox.tsx | 6 +- .../SummaryScriptBox/ToolBox/Indicator.tsx | 75 ++++++++++-- .../SummaryScriptBox/ToolBox/ToolBox.tsx | 12 +- 5 files changed, 144 insertions(+), 64 deletions(-) diff --git a/src/components/SearchPage/SearchResultBox.tsx b/src/components/SearchPage/SearchResultBox.tsx index 4013054..cc2a41d 100644 --- a/src/components/SearchPage/SearchResultBox.tsx +++ b/src/components/SearchPage/SearchResultBox.tsx @@ -6,48 +6,77 @@ import { userInfoState } from '@/stores/user'; import { useRecoilValue } from 'recoil'; interface SearchResultProp { - video : IVideo; - tags : string[]; + video: IVideo; + tags: string[]; } -const SearchResultBox : React.FC= ({video, tags}) => { - const nav = useNavigate(); - const userName = useRecoilValue(userInfoState); - const date = video.created_at.toString().split('T')[0].split('-'); - const handleImg = (event : React.SyntheticEvent) => { - const target = event.target as HTMLImageElement; - target.style.display = 'none'; - } - const handleOnclick = () => { - nav(`/summary/${video.video_id}?insight=${userName?.name === video.user}`); - } - - return ( - -
-
- {video.user} - - - {`${date[0]}년 ${date[1]}월 ${date[2]}일`} - -
-
-
-
-
-
-
- {video.tag.map((item, index) => - {item.name} - )} -
-
-
- handleImg(event)}> -
-
- ); -} +const SearchResultBox: React.FC = ({ video, tags }) => { + const nav = useNavigate(); + const userName = useRecoilValue(userInfoState); + const date = video.created_at.toString().split('T')[0].split('-'); + const handleImg = (event: React.SyntheticEvent) => { + const target = event.target as HTMLImageElement; + target.style.display = 'none'; + }; + const handleOnclick = () => { + nav(`/summary/${video.video_id}?insight=${userName?.name === video.user}`); + }; + + return ( + +
+
+ + {video.user} + + + + {`${date[0]}년 ${date[1]}월 ${date[2]}일`} + +
+
+
+
+
+
+
+ {video.tag.map((item, index) => ( + + # {item.name} + + ))} +
+
+
+ handleImg(event)}> +
+
+ ); +}; -export default SearchResultBox; \ No newline at end of file +export default SearchResultBox; diff --git a/src/components/SummaryPage/SummaryScriptBox/ScriptViewer/ScriptViewer.tsx b/src/components/SummaryPage/SummaryScriptBox/ScriptViewer/ScriptViewer.tsx index ecbbefe..7ccd19b 100644 --- a/src/components/SummaryPage/SummaryScriptBox/ScriptViewer/ScriptViewer.tsx +++ b/src/components/SummaryPage/SummaryScriptBox/ScriptViewer/ScriptViewer.tsx @@ -67,7 +67,7 @@ const ScriptViewer = ({ keyword }: Props) => { return (
{scriptList.map((script) => ( -
+
{ -
+
{isEditingView ? : }
diff --git a/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx b/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx index 9b04cec..0a0fb23 100644 --- a/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx +++ b/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx @@ -1,22 +1,71 @@ -// 임시 타입 -interface Item { - id: number; -} - -type Props = { - list: Item[]; - focusId: number; - onChange: (focusId: number) => void; -}; +import { useEffect, useState } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { IVideo } from '@/models/video'; + +import { + summaryPlaySubHeadingIdState, + summaryVideoState, +} from '@/stores/summary'; + +const Indicator = () => { + const summaryVideo = useRecoilValue(summaryVideoState) as IVideo; + const [playSubHeadingId, setPlaySubHeadingId] = useRecoilState( + summaryPlaySubHeadingIdState, + ); + + const [focusId, setFocusId] = useState(summaryVideo.subHeading[0].id); + + useEffect(() => { + const handleScroll = () => { + const { top: containerTop } = container.getBoundingClientRect(); + const list = Array.from(document.querySelectorAll('.script-box')) + .map((el) => el.getBoundingClientRect().top - containerTop) + .filter((top) => top < 100); + // window.innerHeight * 0.3 + const { id } = summaryVideo.subHeading[list.length - 1]; + + setFocusId(id); + }; + + const container = document.querySelector('#script-box') as HTMLDivElement; + container.addEventListener('scroll', handleScroll); + + return () => { + container.removeEventListener('scroll', handleScroll); + }; + }, [summaryVideo]); + + useEffect(() => { + if (playSubHeadingId < 0) return; + + const findIdx = summaryVideo.subHeading.findIndex( + (s) => s.id === playSubHeadingId, + ); + + if (findIdx > -1) { + const container = document.querySelector('#script-box') as HTMLDivElement; + const element = document.querySelectorAll('.script-box')[findIdx]; + + const { top: containerTop } = container.getBoundingClientRect(); + const { top } = element.getBoundingClientRect(); + + container.scrollTo({ + top: container.scrollTop + top - containerTop, + behavior: 'smooth', + }); + } + + setFocusId(playSubHeadingId); + }, [playSubHeadingId, summaryVideo]); -const Indicator = ({ list, focusId, onChange }: Props) => { return (
- {list.map((item) => ( + {summaryVideo.subHeading.map((item) => (
onChange(item.id)} + onClick={() => setPlaySubHeadingId(item.id)} /> ))}
diff --git a/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx b/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx index f219031..1056b81 100644 --- a/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx +++ b/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx @@ -5,6 +5,8 @@ import { getVideoAPI, updateVideoAPI } from '@/apis/videos'; import ModifyIcon from '@/assets/icons/modify.svg?react'; +import useCreateToast from '@/hooks/useCreateToast'; + import { IVideo } from '@/models/video'; import { @@ -17,7 +19,6 @@ import { import Indicator from './Indicator'; import { SearchKeyword } from './SearchKeyword'; import { ChangeKeyword } from './ChangeKeyword'; -import useCreateToast from '@/hooks/useCreateToast'; type Props = { onRefresh: () => void; @@ -33,10 +34,11 @@ const ToolBox = ({ onRefresh, onChangeKeyword }: Props) => { const [isEditingView, setIsEditingView] = useRecoilState( summaryIsEditingViewState, ); - const { createToast } = useCreateToast(); const [originalSummary, setOriginalSummary] = useState(null); + const { createToast } = useCreateToast(); + const handleClickModifyIcon = () => { setPlaySubHeadingId(-1); setIsEditingView(true); @@ -125,11 +127,7 @@ const ToolBox = ({ onRefresh, onChangeKeyword }: Props) => { ) : ( <> - {}} - /> +
From 08df9c31e5da6543bb4a724b77e200778635f17b 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: Tue, 20 Feb 2024 02:44:55 +0900 Subject: [PATCH 07/31] =?UTF-8?q?feature-074:=20=ED=99=88=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Home/RecentVideos.tsx | 9 +++--- src/components/Home/SearchYoutube.tsx | 7 +---- src/pages/HomePage.tsx | 41 +++++++++++++-------------- src/styles/HomepageStyle.ts | 13 +++------ 4 files changed, 29 insertions(+), 41 deletions(-) diff --git a/src/components/Home/RecentVideos.tsx b/src/components/Home/RecentVideos.tsx index d0d4e48..c627d8b 100644 --- a/src/components/Home/RecentVideos.tsx +++ b/src/components/Home/RecentVideos.tsx @@ -13,12 +13,11 @@ import { IVideoProps } from 'types/videos'; interface IRecentVideosProp { videos: IVideoProps[]; - searchRef: React.RefObject; } -const RecentVideos = ({ videos, searchRef }: IRecentVideosProp) => { +const RecentVideos = ({ videos }: IRecentVideosProp) => { return ( - +
최근 읽은 영상 @@ -37,7 +36,9 @@ const RecentVideos = ({ videos, searchRef }: IRecentVideosProp) => { 처음 방문하셨나요?
아직 정리해본 영상이 없어요!
- searchRef?.current?.focus()}> + window.scrollTo({ top: 0, behavior: 'smooth' })} + >

영상 정리해보기

diff --git a/src/components/Home/SearchYoutube.tsx b/src/components/Home/SearchYoutube.tsx index df48f7f..11ee27f 100644 --- a/src/components/Home/SearchYoutube.tsx +++ b/src/components/Home/SearchYoutube.tsx @@ -28,11 +28,7 @@ import { validateYoutubeLink } from '@/utils/validation'; import ProgressBar from './ProgressBar'; -type Props = { - searchRef: React.RefObject; -}; - -const SearchYoutube = ({ searchRef }: Props) => { +const SearchYoutube = () => { const navigate = useNavigate(); const userToken = useRecoilValue(userTokenState); @@ -157,7 +153,6 @@ const SearchYoutube = ({ searchRef }: Props) => {
{ ); } }; - const searchRef = useRef(null); useEffect(() => { userToken && @@ -72,27 +71,25 @@ const HomePage: React.FC = () => { return ( <> - + - {userToken ? ( -
- - -
- ) : ( -
- - -
- )} +
+ + +
{isOpenModal && } diff --git a/src/styles/HomepageStyle.ts b/src/styles/HomepageStyle.ts index 65971a5..218ecb3 100644 --- a/src/styles/HomepageStyle.ts +++ b/src/styles/HomepageStyle.ts @@ -5,7 +5,9 @@ export const HomePageContainer = styled.div` background-color: ${theme.color.white}; min-height: 100vh; width: 100%; + background-color: ${theme.color.gray500}; `; + export const SearchContainer = styled.div` display: flex; flex-direction: column; @@ -13,7 +15,6 @@ export const SearchContainer = styled.div` justify-content: center; width: 100%; padding-bottom: 100px; - background-color: ${theme.color.gray500}; `; export const SearchForm = styled.form` @@ -116,15 +117,12 @@ export const SearchButton = styled.button` } `; -export const RecentVideosContainer = styled.div<{ length: number }>` +export const RecentVideosContainer = styled.div` background-color: ${theme.color.white}; width: 100%; display: flex; justify-content: center; - border-radius: 50px 50px 0px 0px; position: relative; - bottom: 50px; - padding: ${(props) => (props.length ? '100px' : '0')} 0 110px; .container { width: 910px; @@ -220,15 +218,12 @@ export const VideoButton = styled.button` } `; -export const InsightVideosContainer = styled.div<{ user: string | null }>` +export const InsightVideosContainer = styled.div` display: flex; justify-content: center; background-color: ${theme.color.white}; width: 100%; - border-radius: 50px 50px 0 0; - padding: ${(props) => (props.user ? '0' : '100px')} 0 110px; position: relative; - bottom: 50px; .insight-container { display: flex; From b63827ec8dc2c45d11bc144f9bccb33f1eba9a0d 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: Tue, 20 Feb 2024 02:54:27 +0900 Subject: [PATCH 08/31] =?UTF-8?q?feature-074:=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Home/InsightVideos.tsx | 4 ++-- src/components/category/Card.tsx | 12 +++++++++--- src/styles/category/Card.style.ts | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/Home/InsightVideos.tsx b/src/components/Home/InsightVideos.tsx index ec9a34a..f295aec 100644 --- a/src/components/Home/InsightVideos.tsx +++ b/src/components/Home/InsightVideos.tsx @@ -55,7 +55,7 @@ const InsightVideos: React.FC = ({ }, [userToken]); return ( - +

이런 인사이트는 어때요?

@@ -66,7 +66,7 @@ const InsightVideos: React.FC = ({ {dummyVideos.map((video) => ( = ({ setCheckedVideos, onFileClick, }) => { + const userToken = useRecoilValue(userTokenState); + const onFileClickWithProps = (categoryId: number, categoryName?: string) => { onFileClick && onFileClick(video.video_id, categoryId, categoryName); }; @@ -37,7 +43,7 @@ const Card: React.FC = ({ } }; return ( - +
{mode === 'category' && ( @@ -64,7 +70,7 @@ const Card: React.FC = ({ ))} - {mode === 'recommend' && ( + {mode === 'recommend' && userToken && ( ` +export const Wrap = styled.div<{ token: string | null; mode: string }>` display: flex; flex-direction: column; width: 290px; @@ -122,6 +122,7 @@ export const Wrap = styled.div<{ mode: string }>` box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.1); ${(props) => props.mode === 'recommend' && + props.token && css` height: 424px; `} From b4457d695dcf34bd78940d8484a5657c044f36d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EB=82=98?= Date: Tue, 20 Feb 2024 03:00:27 +0900 Subject: [PATCH 09/31] =?UTF-8?q?feature-027:=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=A4=91=20=EC=97=90=EB=9F=AC=20=EC=8B=9C=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Home/SearchYoutube.tsx | 5 +- src/components/layout/Layout.tsx | 7 +- .../layout/sideBar/ConvertVideo.tsx | 4 +- src/components/modals/ErrorModal.tsx | 39 +++++++++ src/stores/modal.ts | 5 ++ src/styles/modals/ErrorModal.style.ts | 85 +++++++++++++++++++ 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 src/components/modals/ErrorModal.tsx create mode 100644 src/styles/modals/ErrorModal.style.ts diff --git a/src/components/Home/SearchYoutube.tsx b/src/components/Home/SearchYoutube.tsx index 0ef4b36..8ac6ff1 100644 --- a/src/components/Home/SearchYoutube.tsx +++ b/src/components/Home/SearchYoutube.tsx @@ -15,7 +15,8 @@ import { SearchContainer, } from '@/styles/HomepageStyle'; -import { recommendationModalState } from '@/stores/modal'; +import { recommendationModalState, errorModalState } from '@/stores/modal'; + import { modelingDataState, modelingProgressState, @@ -37,6 +38,7 @@ const SearchYoutube = ({ searchRef }: Props) => { const userToken = useRecoilValue(userTokenState); const setIsOpenModal = useSetRecoilState(recommendationModalState); + const setIsOpenErrorModal = useSetRecoilState(errorModalState); const setVideoLink = useSetRecoilState(videoLinkState); const setProgress = useSetRecoilState(modelingProgressState); const [status, setStatus] = useRecoilState(modelingStatusState); @@ -102,6 +104,7 @@ const SearchYoutube = ({ searchRef }: Props) => { setModelingData(null); } catch (e) { console.error(e); + setIsOpenErrorModal(true); } } else { navigate('/summary/guest'); diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 00d149e..b828841 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -1,5 +1,5 @@ import { Outlet, useLocation } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { isSideBarOpenState } from '@/stores/ui'; @@ -7,13 +7,17 @@ import Footer from './footer/Footer'; import Header from './header'; import SideBar from './sideBar'; import NicknameModal from '@/components/NicknameModal'; +import ErrorModal from '../modals/ErrorModal'; + import { useMemo, useEffect } from 'react'; import { userInfoState } from '@/stores/user'; +import { errorModalState } from '@/stores/modal'; const Layout = () => { const { pathname } = useLocation(); const isSideBarOpen = useRecoilValue(isSideBarOpenState); const userInfo = useRecoilValue(userInfoState); + const isErrorModalOpen = useRecoilValue(errorModalState) const isShowFooter = useMemo( () => pathname === '/' || /^(\/category)/g.test(pathname), @@ -38,6 +42,7 @@ const Layout = () => { {isShowFooter &&
} {userInfo && userInfo.nick_name === '' && } + {isErrorModalOpen && } ); }; diff --git a/src/components/layout/sideBar/ConvertVideo.tsx b/src/components/layout/sideBar/ConvertVideo.tsx index aa5de8d..2b8cdca 100644 --- a/src/components/layout/sideBar/ConvertVideo.tsx +++ b/src/components/layout/sideBar/ConvertVideo.tsx @@ -11,7 +11,7 @@ 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 { recommendationModalState } from '@/stores/modal'; +import { errorModalState, recommendationModalState } from '@/stores/modal'; import { modelingDataState, modelingProgressState, @@ -31,6 +31,7 @@ const ConvertVideo = () => { 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); @@ -51,6 +52,7 @@ const ConvertVideo = () => { navigate(`/summary/${video_id}`); setModelingData(null); } catch (e) { + setIsOpenErrorModal(true) console.error(e); } } else { diff --git a/src/components/modals/ErrorModal.tsx b/src/components/modals/ErrorModal.tsx new file mode 100644 index 0000000..90182ab --- /dev/null +++ b/src/components/modals/ErrorModal.tsx @@ -0,0 +1,39 @@ +import { useSetRecoilState } from 'recoil'; +import { errorModalState } from '@/stores/modal'; +import useOutsideClick from '@/hooks/useOutsideClick'; + +import CloseIcon from '@/assets/icons/close.svg?react'; +import errorImg from '@/assets/Error.png'; +import { ErrorModalContainer } from '@/styles/modals/ErrorModal.style'; + +const ErrorModal = () => { + const setIsOpenModal = useSetRecoilState(errorModalState); + const [modalRef] = useOutsideClick(() => + setIsOpenModal(false), + ); + + return ( + +
+
+
setIsOpenModal(false)}> + +
+ +
+
+ errorImg +

영상 업로드 중 오류

+
+

업로드 중 알 수 없는 오류가 발생했어요
다시 시도해주세요

+
+
+ +
+
+ ); +}; + +export default ErrorModal; \ No newline at end of file diff --git a/src/stores/modal.ts b/src/stores/modal.ts index ad21ac9..5adae7b 100644 --- a/src/stores/modal.ts +++ b/src/stores/modal.ts @@ -14,3 +14,8 @@ export const recommendationModalState = atom({ key: 'recommendation-modal', default: false, }); + +export const errorModalState = atom({ + key: 'error-modal', + default: false, +}); diff --git a/src/styles/modals/ErrorModal.style.ts b/src/styles/modals/ErrorModal.style.ts new file mode 100644 index 0000000..26f2543 --- /dev/null +++ b/src/styles/modals/ErrorModal.style.ts @@ -0,0 +1,85 @@ +import styled from 'styled-components'; +import theme from '../theme'; + +export const ErrorModalContainer = styled.div` + position: fixed; + z-index: 100; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + + .container { + width: 700px; + height: 384px; + background-color: ${theme.color.white}; + padding: 40px 50px; + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .wrapper { + width: 600px; + height: 198px; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + } + + .close-btn { + align-self: flex-end; + cursor: pointer; + } + + .main { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } + + .modal-main { + width: 245px; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + } + + .modal-main h2 { + color: ${theme.color.gray500}; + font-style: ${theme.typography.Header6}; + font-size: 24px; + line-height: 1.6em; + margin-top: 12px; + margin-bottom: 12px; + } + + .main h4 { + color: ${theme.color.gray300}; + font-style: ${theme.typography.Body1}; + font-size: 16px; + line-height: 1.6em; + } + + .restart-btn { + width: 600px; + height: 58px; + font-style: ${theme.typography.Body1}; + font-size: 16px; + padding: 16px 24px; + color: ${theme.color.white}; + background-color: ${theme.color.gray500}; + border: none; + border-radius: 12px; + cursor: pointer; + } +`; \ No newline at end of file From 0811eeb9a39d24ec9d630e56f170c6d14f248603 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: Tue, 20 Feb 2024 03:42:50 +0900 Subject: [PATCH 10/31] =?UTF-8?q?feature-074:=20=EC=98=81=EC=83=81=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=ED=8E=98=EC=9D=B4=EC=A7=80=20>=20?= =?UTF-8?q?=EB=8D=94=EB=AF=B8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/videos.ts | 6 +++- src/models/video.ts | 4 +++ src/pages/SummaryPage.tsx | 72 ++++++++++++++++++++++++--------------- src/styles/SummaryPage.ts | 7 ++-- 4 files changed, 58 insertions(+), 31 deletions(-) diff --git a/src/apis/videos.ts b/src/apis/videos.ts index c2e5092..e309cb1 100644 --- a/src/apis/videos.ts +++ b/src/apis/videos.ts @@ -1,6 +1,7 @@ import { APIBaseResponse, APIResponse } from '@/models/config/axios'; import { ModelingFinalData } from '@/models/modeling'; import { + CreateVideoResponse, IVideo, UpdateVideoCategoryRequest, UpdateVideoRequest, @@ -15,7 +16,10 @@ import { IVideoProps } from 'types/videos'; const PREFIX = '/videos'; export const createVideoAPI = (data: ModelingFinalData) => { - return axios.post>(PREFIX + `/new-video`, data); + return axios.post>( + PREFIX + `/new-video`, + data, + ); }; export const getVideoAPI = ( diff --git a/src/models/video.ts b/src/models/video.ts index c7e2074..ebc0b31 100644 --- a/src/models/video.ts +++ b/src/models/video.ts @@ -53,3 +53,7 @@ export interface UpdateVideoRequest { export interface UpdateVideoCategoryRequest { video_id: (string | number)[]; } + +export interface CreateVideoResponse { + video_id: number; +} diff --git a/src/pages/SummaryPage.tsx b/src/pages/SummaryPage.tsx index 4abc89c..34d7141 100644 --- a/src/pages/SummaryPage.tsx +++ b/src/pages/SummaryPage.tsx @@ -1,14 +1,12 @@ -import { useCallback, useEffect } from 'react'; +import { useEffect } from 'react'; import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { getDummyVideoAPI, getVideoAPI } from '@/apis/videos'; +import { createVideoAPI, getDummyVideoAPI, getVideoAPI } from '@/apis/videos'; import { SummaryDetailBox } from '@/components/SummaryPage'; import { SummaryScriptBox } from '@/components/SummaryPage'; -import { IVideo } from '@/models/video'; - import { modelingDataState } from '@/stores/model-controller'; import { summaryVideoState } from '@/stores/summary'; import { userTokenState } from '@/stores/user'; @@ -21,42 +19,55 @@ const SummaryPage = () => { const { search } = useLocation(); const userToken = useRecoilValue(userTokenState); - const [modelingData, setModelingData] = useRecoilState(modelingDataState); + const modelingData = useRecoilValue(modelingDataState); const [summaryVideo, setSummaryVideo] = useRecoilState(summaryVideoState); - const callAPI = useCallback(async () => { + const callVideoAPI = async () => { if (!videoId) return; - const searchParam = new URLSearchParams(search); - const isInsight = searchParam.get('insight') === 'true'; + try { + const { isSuccess, result } = (await getVideoAPI(videoId)).data; - let isSuccess = false; - let result: IVideo | null = null; + if (!isSuccess) { + navigate('/'); + return; + } - try { - if (isInsight) { - const { data } = await getDummyVideoAPI(videoId); + setSummaryVideo(result); + } catch (e) { + console.error(e); + navigate('/'); + } + }; - isSuccess = data.isSuccess; - result = data.result; - } else { - const { data } = await getVideoAPI(videoId); + const callDummyAPI = async () => { + if (!videoId) return; - isSuccess = data.isSuccess; - result = data.result; - } + try { + const { isSuccess, result } = (await getDummyVideoAPI(videoId)).data; if (!isSuccess) { navigate('/'); return; } - setSummaryVideo(result); + if (userToken) { + const { video_id } = ( + await createVideoAPI({ + subheading: result.subHeading, + ...result, + }) + ).data.result; + + navigate(`/summary/${video_id}`); + } else { + setSummaryVideo(result); + } } catch (e) { console.error(e); navigate('/'); } - }, [search, videoId, navigate, setSummaryVideo]); + }; const setGuestSummaryVideo = () => { if (!modelingData) { @@ -83,13 +94,20 @@ const SummaryPage = () => { }; useEffect(() => { + const searchParam = new URLSearchParams(search); + const isInsight = searchParam.get('insight') === 'true'; + if (userToken) { - callAPI(); + if (isInsight) { + callDummyAPI(); + } else { + callVideoAPI(); + } } else { if (videoId === 'guest') { setGuestSummaryVideo(); } else { - callAPI(); + callDummyAPI(); } } @@ -98,14 +116,14 @@ const SummaryPage = () => { // setModelingData(null); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userToken]); + }, [search]); return ( {summaryVideo && ( <> - - + + )} diff --git a/src/styles/SummaryPage.ts b/src/styles/SummaryPage.ts index b93ee7c..c8ff6b1 100644 --- a/src/styles/SummaryPage.ts +++ b/src/styles/SummaryPage.ts @@ -249,17 +249,18 @@ export const ScriptBox = styled.div` position: relative; display: flex; flex-direction: column; - gap: 40px; min-width: 555px; max-width: 865px; box-shadow: 0 4px 40px 0 rgba(0, 0, 0, 0.05); & div.tools { - padding: 20px 100px 0 60px; + z-index: 1; + padding: 20px 100px 20px 60px; display: flex; align-items: center; justify-content: space-between; width: 100%; + box-shadow: 0 4px 40px 0 rgba(0, 0, 0, 0.05); & button.edit-button { padding: 8px 20px; @@ -325,7 +326,7 @@ export const ScriptBox = styled.div` } & div.script-container { - padding: 0 100px 20px 60px; + padding: 20px 100px 20px 60px; display: flex; flex-direction: column; gap: 60px; From 1630ad399553b004ed724f52f4817f2f573ef776 Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 04:08:18 +0900 Subject: [PATCH 11/31] =?UTF-8?q?feature-081:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=B0=BD=EC=97=90=20=EC=98=81=EC=83=81=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/user.ts | 13 +++--- src/components/Home/SearchYoutube.tsx | 36 ++------------- .../ModelController/ModelController.tsx | 10 +++++ .../layout/header/alarm/AlarmItem.tsx | 43 +++++++++++++++++- .../layout/header/alarm/AlarmList.tsx | 1 + src/components/layout/header/alarm/index.tsx | 20 ++++++++- .../layout/sideBar/ConvertVideo.tsx | 41 +++-------------- src/hooks/useCreateVideo.ts | 44 +++++++++++++++++++ .../layout/header/alarm/AlarmItem.style.ts | 27 ++++++++++++ 9 files changed, 159 insertions(+), 76 deletions(-) create mode 100644 src/hooks/useCreateVideo.ts 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/SearchYoutube.tsx b/src/components/Home/SearchYoutube.tsx index 779cb9b..f883f88 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'; @@ -17,30 +14,26 @@ import { import { recommendationModalState } from '@/stores/modal'; import { - modelingDataState, modelingProgressState, 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'; type Props = { searchRef: React.RefObject; }; const SearchYoutube = ({ searchRef }: Props) => { - const navigate = useNavigate(); - - const userToken = useRecoilValue(userTokenState); const setIsOpenModal = useSetRecoilState(recommendationModalState); 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(''); @@ -98,27 +91,6 @@ const SearchYoutube = ({ searchRef }: Props) => { setProgress(0); }; - 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); - } - } else { - navigate('/summary/guest'); - } - - setVideoLink(null); - setStatus('NONE'); - setProgress(0); - }; - return ( <> @@ -172,7 +144,7 @@ const SearchYoutube = ({ searchRef }: Props) => { color: theme.color.gray500, backgroundColor: theme.color.green400, }} - onClick={handleClickCreateVideoButton} + onClick={createVideo} > 영상 읽기 diff --git a/src/components/common/ModelController/ModelController.tsx b/src/components/common/ModelController/ModelController.tsx index 90de4ea..03c1ca3 100644 --- a/src/components/common/ModelController/ModelController.tsx +++ b/src/components/common/ModelController/ModelController.tsx @@ -49,6 +49,15 @@ const ModelController = () => { setVideoLink(null); }; + const handleUpdateAlarm = async (title: string) => { + await createVideoAlarmAPI(0, 'success', { + title: `[${title}]`, + content: + '영상이 모두 변환되었어요!\n이제 정리 된 영상을 확인하러 가볼까요?', + is_confirm: false, + }); + }; + useEffect(() => { if (!videoLink) return; @@ -89,6 +98,7 @@ const ModelController = () => { setModelingData(finalData); setModelingProgress(100); + handleUpdateAlarm(finalData.title); setModelingStatus('COMPLETE'); } catch (e) { console.error(e); diff --git a/src/components/layout/header/alarm/AlarmItem.tsx b/src/components/layout/header/alarm/AlarmItem.tsx index d026ac1..d7f8b06 100644 --- a/src/components/layout/header/alarm/AlarmItem.tsx +++ b/src/components/layout/header/alarm/AlarmItem.tsx @@ -11,12 +11,21 @@ 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 useCreateVideo from '@/hooks/useCreateVideo'; +import { confirmSelectAlarmAPI } from '@/apis/user'; type Props = { alarm: IAlarm; selectIdList: number[]; onUpdateSelectIdList: (list: number[]) => void; onClose: () => void; + onRefresh: () => void; }; const AlarmItem = ({ @@ -24,8 +33,12 @@ const AlarmItem = ({ selectIdList, onUpdateSelectIdList, onClose, + onRefresh, }: Props) => { const navigate = useNavigate(); + const status = useRecoilValue(modelingStatusState); + const progress = useRecoilValue(modelingProgressState); + const { createVideo } = useCreateVideo(); const isSelected = selectIdList.indexOf(alarm.alarm_id) > -1; const type = () => { @@ -58,11 +71,22 @@ const AlarmItem = ({ return `${second}초`; }; - const handleClick = () => { + const handleClick = async () => { if (alarm.type === 'notice') { navigate('/guide'); onClose(); } + if (alarm.type === 'video' && !alarm.is_confirm) { + try { + await confirmSelectAlarmAPI({ alarms: [alarm.alarm_id] }); + + onRefresh(); + createVideo(); + onClose(); + } catch (e) { + console.error(e); + } + } }; const handleClickRemoveButton: React.MouseEventHandler = ( @@ -110,6 +134,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..66ae127 100644 --- a/src/components/layout/header/alarm/index.tsx +++ b/src/components/layout/header/alarm/index.tsx @@ -40,9 +40,27 @@ const Alarm = ({ isDark }: Props) => { }, []); useEffect(() => { - if (status === 'ERROR') { + if (status === 'ERROR' || status === 'COMPLETE') { + console.log(status, 'dd'); callAPI(); } + + 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 ( diff --git a/src/components/layout/sideBar/ConvertVideo.tsx b/src/components/layout/sideBar/ConvertVideo.tsx index aa5de8d..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'; @@ -13,27 +11,23 @@ import { CommonTitle } from '@/styles/layout/sideBar/UserMode.style'; 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 [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); @@ -41,27 +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) { - console.error(e); - } - } else { - navigate('/summary/guest'); - } - - setVideoLink(null); - setStatus('NONE'); - setProgress(0); - }; - const handleClickStartConvertButton: React.MouseEventHandler< HTMLButtonElement > = (e) => { @@ -112,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..7c93c7b --- /dev/null +++ b/src/hooks/useCreateVideo.ts @@ -0,0 +1,44 @@ +import { createVideoAPI } from '@/apis/videos'; +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'; + +const useCreateVideo = () => { + const [modelingData, setModelingData] = useRecoilState(modelingDataState); + const userToken = useRecoilValue(userTokenState); + const setVideoLink = useSetRecoilState(videoLinkState); + const setStatus = useSetRecoilState(modelingStatusState); + const setProgress = useSetRecoilState(modelingProgressState); + const navigate = useNavigate(); + + const createVideo = 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); + } + } else { + navigate('/summary/guest'); + } + + setVideoLink(null); + setStatus('NONE'); + setProgress(0); + }; + + return { createVideo }; +}; + +export default useCreateVideo; diff --git a/src/styles/layout/header/alarm/AlarmItem.style.ts b/src/styles/layout/header/alarm/AlarmItem.style.ts index 6b9496d..b1eeb0d 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` @@ -122,4 +123,30 @@ export const Container = styled.div` } } } + + & 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}; + } `; From 331ec061058735a261165d29208da0dfb67f9ef5 Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 04:20:58 +0900 Subject: [PATCH 12/31] =?UTF-8?q?feature-081:=20=EB=B9=84=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=9D=BC=20=EB=95=8C=20=EC=9D=B4=EB=8F=99=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/header/alarm/index.tsx | 32 +++++++++++--------- src/hooks/useCreateVideo.ts | 16 ++++------ 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/components/layout/header/alarm/index.tsx b/src/components/layout/header/alarm/index.tsx index 66ae127..27fba10 100644 --- a/src/components/layout/header/alarm/index.tsx +++ b/src/components/layout/header/alarm/index.tsx @@ -15,6 +15,7 @@ import { modelingStatusState } from '@/stores/model-controller'; import * as HeaderStyle from '@/styles/layout/header'; import AlarmList from './AlarmList'; +import { userTokenState } from '@/stores/user'; type Props = { isDark: boolean; @@ -22,6 +23,7 @@ type Props = { const Alarm = ({ isDark }: Props) => { const status = useRecoilValue(modelingStatusState); + const userToken = useRecoilValue(userTokenState); const [isOpen, setIsOpen] = useState(false); const [alarmList, setAlarmList] = useState([]); @@ -41,24 +43,24 @@ const Alarm = ({ isDark }: Props) => { useEffect(() => { if (status === 'ERROR' || status === 'COMPLETE') { - console.log(status, 'dd'); - callAPI(); + userToken && callAPI(); } 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, - ]); + userToken && + 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]); diff --git a/src/hooks/useCreateVideo.ts b/src/hooks/useCreateVideo.ts index 7c93c7b..8df7376 100644 --- a/src/hooks/useCreateVideo.ts +++ b/src/hooks/useCreateVideo.ts @@ -20,17 +20,13 @@ const useCreateVideo = () => { const createVideo = async () => { if (!modelingData) return; - if (userToken) { - try { - const { video_id } = (await createVideoAPI(modelingData)).data.result; + try { + const { video_id } = (await createVideoAPI(modelingData)).data.result; - navigate(`/summary/${video_id}`); - setModelingData(null); - } catch (e) { - console.error(e); - } - } else { - navigate('/summary/guest'); + navigate(`/summary/${video_id}${!userToken && '?insight=true'}`); + setModelingData(null); + } catch (e) { + console.error(e); } setVideoLink(null); From 4532ce08760fa718e0d2910f8ff792bed85e2317 Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 04:08:18 +0900 Subject: [PATCH 13/31] =?UTF-8?q?feature-081:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=B0=BD=EC=97=90=20=EC=98=81=EC=83=81=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/user.ts | 13 ++--- src/components/Home/SearchYoutube.tsx | 45 +++++------------- .../ModelController/ModelController.tsx | 10 ++++ .../layout/header/alarm/AlarmItem.tsx | 43 ++++++++++++++++- .../layout/header/alarm/AlarmList.tsx | 1 + src/components/layout/header/alarm/index.tsx | 20 +++++++- .../layout/sideBar/ConvertVideo.tsx | 45 ++++-------------- src/hooks/useCreateVideo.ts | 47 +++++++++++++++++++ src/pages/HomePage.tsx | 5 +- .../layout/header/alarm/AlarmItem.style.ts | 27 +++++++++++ 10 files changed, 173 insertions(+), 83 deletions(-) create mode 100644 src/hooks/useCreateVideo.ts 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/SearchYoutube.tsx b/src/components/Home/SearchYoutube.tsx index 78a24b9..127bad2 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/..." + searchRef={searchRef} />
@@ -171,7 +148,7 @@ const SearchYoutube = () => { color: theme.color.gray500, backgroundColor: theme.color.green400, }} - onClick={handleClickCreateVideoButton} + onClick={createVideo} > 영상 읽기 diff --git a/src/components/common/ModelController/ModelController.tsx b/src/components/common/ModelController/ModelController.tsx index f20dc83..c0acf7a 100644 --- a/src/components/common/ModelController/ModelController.tsx +++ b/src/components/common/ModelController/ModelController.tsx @@ -55,6 +55,15 @@ const ModelController = () => { setVideoLink(null); }; + const handleUpdateAlarm = async (title: string) => { + await createVideoAlarmAPI(0, 'success', { + title: `[${title}]`, + content: + '영상이 모두 변환되었어요!\n이제 정리 된 영상을 확인하러 가볼까요?', + is_confirm: false, + }); + }; + useEffect(() => { if (!videoLink) return; @@ -100,6 +109,7 @@ const ModelController = () => { updated_at: new Date().toString(), }); setModelingProgress(100); + handleUpdateAlarm(finalData.title); setModelingStatus('COMPLETE'); } catch (e) { console.error(e); diff --git a/src/components/layout/header/alarm/AlarmItem.tsx b/src/components/layout/header/alarm/AlarmItem.tsx index d026ac1..d7f8b06 100644 --- a/src/components/layout/header/alarm/AlarmItem.tsx +++ b/src/components/layout/header/alarm/AlarmItem.tsx @@ -11,12 +11,21 @@ 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 useCreateVideo from '@/hooks/useCreateVideo'; +import { confirmSelectAlarmAPI } from '@/apis/user'; type Props = { alarm: IAlarm; selectIdList: number[]; onUpdateSelectIdList: (list: number[]) => void; onClose: () => void; + onRefresh: () => void; }; const AlarmItem = ({ @@ -24,8 +33,12 @@ const AlarmItem = ({ selectIdList, onUpdateSelectIdList, onClose, + onRefresh, }: Props) => { const navigate = useNavigate(); + const status = useRecoilValue(modelingStatusState); + const progress = useRecoilValue(modelingProgressState); + const { createVideo } = useCreateVideo(); const isSelected = selectIdList.indexOf(alarm.alarm_id) > -1; const type = () => { @@ -58,11 +71,22 @@ const AlarmItem = ({ return `${second}초`; }; - const handleClick = () => { + const handleClick = async () => { if (alarm.type === 'notice') { navigate('/guide'); onClose(); } + if (alarm.type === 'video' && !alarm.is_confirm) { + try { + await confirmSelectAlarmAPI({ alarms: [alarm.alarm_id] }); + + onRefresh(); + createVideo(); + onClose(); + } catch (e) { + console.error(e); + } + } }; const handleClickRemoveButton: React.MouseEventHandler = ( @@ -110,6 +134,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..66ae127 100644 --- a/src/components/layout/header/alarm/index.tsx +++ b/src/components/layout/header/alarm/index.tsx @@ -40,9 +40,27 @@ const Alarm = ({ isDark }: Props) => { }, []); useEffect(() => { - if (status === 'ERROR') { + if (status === 'ERROR' || status === 'COMPLETE') { + console.log(status, 'dd'); callAPI(); } + + 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 ( 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..789a596 --- /dev/null +++ b/src/hooks/useCreateVideo.ts @@ -0,0 +1,47 @@ +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'; + +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 navigate = useNavigate(); + + const createVideo = 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); + }; + + return { createVideo }; +}; + +export default useCreateVideo; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 3e77d6e..bc1fa74 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'; @@ -34,6 +34,7 @@ const HomePage: React.FC = () => { const isOpenModal = useRecoilValue(recommendationModalState); const [recentVideos, setRecentVideos] = useState([]); const [dummyVideos, setDummyVideos] = useState([]); + const searchRef = useRef(); const { createToast } = useCreateToast(); const onFileClick = async ( @@ -71,7 +72,7 @@ const HomePage: React.FC = () => { return ( <> - +
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}; + } `; From 757da2876452a3da30261fa95105b440b36930bf Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 04:20:58 +0900 Subject: [PATCH 14/31] =?UTF-8?q?feature-081:=20=EB=B9=84=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=9D=BC=20=EB=95=8C=20=EC=9D=B4=EB=8F=99=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/header/alarm/index.tsx | 32 +++++++++++--------- src/hooks/useCreateVideo.ts | 18 +++++------ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/components/layout/header/alarm/index.tsx b/src/components/layout/header/alarm/index.tsx index 66ae127..27fba10 100644 --- a/src/components/layout/header/alarm/index.tsx +++ b/src/components/layout/header/alarm/index.tsx @@ -15,6 +15,7 @@ import { modelingStatusState } from '@/stores/model-controller'; import * as HeaderStyle from '@/styles/layout/header'; import AlarmList from './AlarmList'; +import { userTokenState } from '@/stores/user'; type Props = { isDark: boolean; @@ -22,6 +23,7 @@ type Props = { const Alarm = ({ isDark }: Props) => { const status = useRecoilValue(modelingStatusState); + const userToken = useRecoilValue(userTokenState); const [isOpen, setIsOpen] = useState(false); const [alarmList, setAlarmList] = useState([]); @@ -41,24 +43,24 @@ const Alarm = ({ isDark }: Props) => { useEffect(() => { if (status === 'ERROR' || status === 'COMPLETE') { - console.log(status, 'dd'); - callAPI(); + userToken && callAPI(); } 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, - ]); + userToken && + 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]); diff --git a/src/hooks/useCreateVideo.ts b/src/hooks/useCreateVideo.ts index 789a596..bcfeeb2 100644 --- a/src/hooks/useCreateVideo.ts +++ b/src/hooks/useCreateVideo.ts @@ -22,18 +22,14 @@ const useCreateVideo = () => { const createVideo = async () => { if (!modelingData) return; - if (userToken) { - try { - const { video_id } = (await createVideoAPI(modelingData)).data.result; + 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'); + navigate(`/summary/${video_id}${!userToken && '?insight=true'}`); + setModelingData(null); + } catch (e) { + setIsOpenErrorModal(true); + console.error(e); } setVideoLink(null); From e409d9fa649490368fb3001bef80dfad6682663e Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 04:33:45 +0900 Subject: [PATCH 15/31] =?UTF-8?q?feature-081:=20rebase=20=ED=9B=84=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Home/RecentVideos.tsx | 10 ++++++++-- src/components/Home/SearchYoutube.tsx | 2 +- src/pages/HomePage.tsx | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) 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 f670393..679f7f6 100644 --- a/src/components/Home/SearchYoutube.tsx +++ b/src/components/Home/SearchYoutube.tsx @@ -137,7 +137,7 @@ const SearchYoutube = ({ searchRef }: Props) => { disabled={status === 'CONTINUE'} onChange={handleChangeInput} placeholder="https://youtube.com/..." - // searchRef={searchRef} + ref={searchRef} />
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index bc1fa74..7feffd7 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -34,8 +34,8 @@ const HomePage: React.FC = () => { const isOpenModal = useRecoilValue(recommendationModalState); const [recentVideos, setRecentVideos] = useState([]); const [dummyVideos, setDummyVideos] = useState([]); - const searchRef = useRef(); const { createToast } = useCreateToast(); + const searchRef = useRef(null); const onFileClick = async ( videoId: number, @@ -84,7 +84,7 @@ const HomePage: React.FC = () => { backgroundColor: 'white', }} > - + Date: Tue, 20 Feb 2024 04:41:22 +0900 Subject: [PATCH 16/31] =?UTF-8?q?feature-081:=20=EB=B9=84=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=9D=BC=20=EB=95=8C=20=EC=98=81=EC=83=81=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=20=ED=9B=84=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/header/alarm/index.tsx | 31 +++++++++----------- src/hooks/useCreateVideo.ts | 6 ++-- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/components/layout/header/alarm/index.tsx b/src/components/layout/header/alarm/index.tsx index 27fba10..8c5023b 100644 --- a/src/components/layout/header/alarm/index.tsx +++ b/src/components/layout/header/alarm/index.tsx @@ -15,7 +15,6 @@ import { modelingStatusState } from '@/stores/model-controller'; import * as HeaderStyle from '@/styles/layout/header'; import AlarmList from './AlarmList'; -import { userTokenState } from '@/stores/user'; type Props = { isDark: boolean; @@ -23,7 +22,6 @@ type Props = { const Alarm = ({ isDark }: Props) => { const status = useRecoilValue(modelingStatusState); - const userToken = useRecoilValue(userTokenState); const [isOpen, setIsOpen] = useState(false); const [alarmList, setAlarmList] = useState([]); @@ -43,24 +41,23 @@ const Alarm = ({ isDark }: Props) => { useEffect(() => { if (status === 'ERROR' || status === 'COMPLETE') { - userToken && callAPI(); + callAPI(); } if (status === 'CONTINUE') { - userToken && - setAlarmList([ - { - state: 'success', - type: 'video', - alarm_id: 999, - title: '열심히 영상을 변환하는 중이에요!', - content: '잠시후 멋진 글을 만날 수 있어요:)', - is_confirm: 0, - created_at: new Date().toString(), - updated_at: new Date().toString(), - }, - ...alarmList, - ]); + 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]); diff --git a/src/hooks/useCreateVideo.ts b/src/hooks/useCreateVideo.ts index bcfeeb2..62db094 100644 --- a/src/hooks/useCreateVideo.ts +++ b/src/hooks/useCreateVideo.ts @@ -6,13 +6,11 @@ import { modelingStatusState, videoLinkState, } from '@/stores/model-controller'; -import { userTokenState } from '@/stores/user'; import { useNavigate } from 'react-router-dom'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilState, useSetRecoilState } from 'recoil'; const useCreateVideo = () => { const [modelingData, setModelingData] = useRecoilState(modelingDataState); - const userToken = useRecoilValue(userTokenState); const setIsOpenErrorModal = useSetRecoilState(errorModalState); const setVideoLink = useSetRecoilState(videoLinkState); const setStatus = useSetRecoilState(modelingStatusState); @@ -25,7 +23,7 @@ const useCreateVideo = () => { try { const { video_id } = (await createVideoAPI(modelingData)).data.result; - navigate(`/summary/${video_id}${!userToken && '?insight=true'}`); + navigate(`/summary/${video_id}`); setModelingData(null); } catch (e) { setIsOpenErrorModal(true); From 9e158be6c070f07b44bba041b29c5a907c73f5c2 Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 04:45:09 +0900 Subject: [PATCH 17/31] =?UTF-8?q?feature-081:=20=EB=B9=84=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=9D=BC=20=EB=95=8C=20=EC=98=81=EC=83=81=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=20=ED=9B=84=20=EC=9D=B4=EB=8F=99=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCreateVideo.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/hooks/useCreateVideo.ts b/src/hooks/useCreateVideo.ts index 62db094..a0f4323 100644 --- a/src/hooks/useCreateVideo.ts +++ b/src/hooks/useCreateVideo.ts @@ -6,11 +6,13 @@ import { modelingStatusState, videoLinkState, } from '@/stores/model-controller'; +import { userTokenState } from '@/stores/user'; import { useNavigate } from 'react-router-dom'; -import { useRecoilState, useSetRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; const useCreateVideo = () => { const [modelingData, setModelingData] = useRecoilState(modelingDataState); + const userToken = useRecoilValue(userTokenState); const setIsOpenErrorModal = useSetRecoilState(errorModalState); const setVideoLink = useSetRecoilState(videoLinkState); const setStatus = useSetRecoilState(modelingStatusState); @@ -20,14 +22,18 @@ const useCreateVideo = () => { const createVideo = async () => { if (!modelingData) return; - try { - const { video_id } = (await createVideoAPI(modelingData)).data.result; + if (userToken) { + try { + const { video_id } = (await createVideoAPI(modelingData)).data.result; - navigate(`/summary/${video_id}`); - setModelingData(null); - } catch (e) { - setIsOpenErrorModal(true); - console.error(e); + navigate(`/summary/${video_id}`); + setModelingData(null); + } catch (e) { + console.error(e); + setIsOpenErrorModal(true); + } + } else { + navigate('/summary/guest'); } setVideoLink(null); From d136727e3b0d167d364a99c94c594f34d617efa2 Mon Sep 17 00:00:00 2001 From: KimSehyeoun Date: Tue, 20 Feb 2024 11:08:21 +0900 Subject: [PATCH 18/31] =?UTF-8?q?feature-022=20:=20loading=20Spinner=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/spinner.gif | Bin 0 -> 464887 bytes .../loadingSpinner/loadingSpinner.tsx | 25 ++++++++++ src/styles/loadingSpinner.ts | 44 ++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/assets/spinner.gif create mode 100644 src/components/loadingSpinner/loadingSpinner.tsx create mode 100644 src/styles/loadingSpinner.ts diff --git a/src/assets/spinner.gif b/src/assets/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..6b2ca4bbc7f79694e0647e689a98e82716499be6 GIT binary patch literal 464887 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

3{&F}!y+Ww4e!78tP&mPV}WL@-%pxyPZP)|B! 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 Xut(d}|0EEHo{_-0nfAYr>=pkD-b%%@ literal 0 HcmV?d00001 diff --git a/src/components/loadingSpinner/loadingSpinner.tsx b/src/components/loadingSpinner/loadingSpinner.tsx new file mode 100644 index 0000000..c2d4f57 --- /dev/null +++ b/src/components/loadingSpinner/loadingSpinner.tsx @@ -0,0 +1,25 @@ +import Spinner from '@/assets/spinner.gif'; +import Container from '@/styles/loadingSpinner'; + +const LoadingSpinner = () => { + const randMsg = ['지금 이 서비스는 한 달 만에 개발된 서비스랍니다!', + '바꾸기 버튼으로 한 번에 원하는 단어를 바꿀 수 있어요!', '해시태그는 한번에 3개까지 섞어 검색할 수 있어요!', + '진행바에 마우스를 올리면 몇 %까지 진행됐는지 알 수 있어요!', '카테고리는 당겨서 이동시킬 수 있어요!']; + const randomElement = randMsg[Math.floor(Math.random() * randMsg.length)]; + + return ( + +
+
+ spinner-img +
+ 그거 아셨나요? + {randomElement} +
+
+
+
+ ); +} + +export default LoadingSpinner; \ No newline at end of file diff --git a/src/styles/loadingSpinner.ts b/src/styles/loadingSpinner.ts new file mode 100644 index 0000000..75c0f0f --- /dev/null +++ b/src/styles/loadingSpinner.ts @@ -0,0 +1,44 @@ +import styled from "styled-components"; +import theme from "./theme"; + + +const Container = styled.div` + width : 100%; + height : 100vh; + + display : flex; + align-items : center; + justify-content: center; + + & div.wrap { + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + } + & div.tip { + display : flex; + gap : 10px; + flex-direction : column; + } + & div.center-box { + display : flex; + flex-direction : column; + gap : 30px; + justify-content: center; + align-items: center; + } + & span.header { + text-align : center; + ${theme.typography.Subheader3}; + color : ${theme.color.gray500}; + } + & span.body { + text-align : center; + ${theme.typography.Body4}; + color : ${theme.color.gray400}; + } +`; + + +export default Container; \ No newline at end of file From 7ff5510185b62401296a0574ce2fba78ce3781ec Mon Sep 17 00:00:00 2001 From: KimSehyeoun Date: Tue, 20 Feb 2024 11:08:59 +0900 Subject: [PATCH 19/31] =?UTF-8?q?feature-022=20:=20loading=20spinner=202?= =?UTF-8?q?=EC=B4=88=20=EC=84=A4=EC=A0=95,=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/SearchResultPage.tsx | 43 ++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/pages/SearchResultPage.tsx b/src/pages/SearchResultPage.tsx index c6dd4d9..3ff59f5 100644 --- a/src/pages/SearchResultPage.tsx +++ b/src/pages/SearchResultPage.tsx @@ -1,9 +1,12 @@ -import { escapeHTML } from '@/utils/string'; import { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import Styled from '@/styles/SearchResult'; + import TagInput from '@/components/SearchPage/SearchComponent'; import SearchNotFound from '@/components/SearchPage/SearchNotFound'; +import LoadingSpinner from '@/components/loadingSpinner/loadingSpinner'; +import { escapeHTML } from '@/utils/string'; + import SearchIcon from '@/assets/icons/search.svg?react'; import { IVideo } from '@/models/search'; import { searchAPI } from '@/apis/search'; @@ -21,7 +24,6 @@ const SearchResult = () => { useEffect(() => { const searchParams = new URLSearchParams(location.search); - setLoading(true); switch (searchParams.get('type')) { @@ -32,9 +34,6 @@ const SearchResult = () => { setInput(inputValues); handleSearchAPI(inputValues, keywordtype, ' '); - if (data.length === 0) { - setErrormsg(inputValues); - } break; case 'hashtag': const tagValues = searchParams.get('value') as string; @@ -43,15 +42,19 @@ const SearchResult = () => { setTags(tagValues.replace(/\s+/g, '').split('&')); setSearchType(false); handleSearchAPI(tagValues, tagtype, '&'); - if (data.length === 0) { - setErrormsg(tagValues.replace(/&/g, ' ')); - } break; default: // 기타 에러 } }, [location.search]); + + useEffect(() => { + const timer = setTimeout(() => { + setLoading(false); + }, 2000); + return () => clearTimeout(timer); + }, [loading]); const handleSearchAPI = async ( inputValues: string, @@ -68,14 +71,25 @@ const SearchResult = () => { return searchData.then((value) => value.data.result); }); const responses = await Promise.all(requests); + let responseArr = [] as IVideo[]; responses.forEach((response) => { const ivideos = response.videos as IVideo[]; - dataDuplicateHandler(ivideos, inputValues); + ivideos.forEach((val) => { + responseArr.push(val); + }) }); + if(responseArr.length === 0){ + setData([]); + if(type === 'hashtag') + setErrormsg(inputValues.replace(/\&/g, ' ')); + else { + setErrormsg(inputValues); + } + } else { + dataDuplicateHandler(responseArr, inputValues); + } } catch (error) { - } finally { - setLoading(false); - } + } }; const formatContent = (content: string, keyword: string) => { @@ -99,7 +113,6 @@ const SearchResult = () => { const uniqueData = videos.filter((v, index, arr) => arr.findIndex(t => t.video_id === v.video_id) === index ); - const mappingData = uniqueData.map((video) => { return { ...video, @@ -108,12 +121,12 @@ const SearchResult = () => { content: formatContent(video.content, check), }; }); - setData([...data, ...mappingData]); + setData([...mappingData]); }; if (loading) { return ( -
스켈레톤 페이지
+ ); } return ( From 071019a167998fdb42773cab0bdf954ebd5c666b Mon Sep 17 00:00:00 2001 From: KimSehyeoun Date: Tue, 20 Feb 2024 13:15:27 +0900 Subject: [PATCH 20/31] =?UTF-8?q?feature-022=20:=20timer=202=EC=B4=88=20?= =?UTF-8?q?=EA=B0=95=EC=A0=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loadingSpinner/loadingSpinner.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/loadingSpinner/loadingSpinner.tsx b/src/components/loadingSpinner/loadingSpinner.tsx index c2d4f57..2d2f60e 100644 --- a/src/components/loadingSpinner/loadingSpinner.tsx +++ b/src/components/loadingSpinner/loadingSpinner.tsx @@ -1,7 +1,23 @@ +import { useEffect } from 'react'; import Spinner from '@/assets/spinner.gif'; import Container from '@/styles/loadingSpinner'; -const LoadingSpinner = () => { +interface LoadingSpinnerProps { + isCroll : boolean; + setLoading : (value : boolean) => void; + time : number; +} + +const LoadingSpinner : React.FC = ({isCroll, setLoading, time}) => { + useEffect(() => { + if(isCroll){ + const timer = setTimeout(() => { + setLoading(false); + }, time); + return () => clearTimeout(timer); + } + }, []); + const randMsg = ['지금 이 서비스는 한 달 만에 개발된 서비스랍니다!', '바꾸기 버튼으로 한 번에 원하는 단어를 바꿀 수 있어요!', '해시태그는 한번에 3개까지 섞어 검색할 수 있어요!', '진행바에 마우스를 올리면 몇 %까지 진행됐는지 알 수 있어요!', '카테고리는 당겨서 이동시킬 수 있어요!']; From e564d08f32828b3f1e2c34ac2c430a375efefc7d Mon Sep 17 00:00:00 2001 From: KimSehyeoun Date: Tue, 20 Feb 2024 13:15:57 +0900 Subject: [PATCH 21/31] =?UTF-8?q?feature-022=20:=20loading=20spinner=20pro?= =?UTF-8?q?p=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/SearchResultPage.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/pages/SearchResultPage.tsx b/src/pages/SearchResultPage.tsx index 3ff59f5..d64c370 100644 --- a/src/pages/SearchResultPage.tsx +++ b/src/pages/SearchResultPage.tsx @@ -18,6 +18,7 @@ const SearchResult = () => { const [input, setInput] = useState(''); const [searchType, setSearchType] = useState(true); // True : keyword | False : hashTag const [loading, setLoading] = useState(false); + const [isCroll, setIsCroll] = useState(false); const [errormsg, setErrormsg] = useState(''); const [data, setData] = useState([]); const location = useLocation(); @@ -49,12 +50,6 @@ const SearchResult = () => { } }, [location.search]); - useEffect(() => { - const timer = setTimeout(() => { - setLoading(false); - }, 2000); - return () => clearTimeout(timer); - }, [loading]); const handleSearchAPI = async ( inputValues: string, @@ -62,6 +57,7 @@ const SearchResult = () => { splittype: string, ) => { try { + setIsCroll(true); const keywords = inputValues.split(splittype); const requests = keywords.map((value) => { if (type === 'hashtag') { @@ -126,7 +122,7 @@ const SearchResult = () => { if (loading) { return ( - + ); } return ( From 779e20b869bbf82a67c567148b3e497a7189ad6c Mon Sep 17 00:00:00 2001 From: KimSehyeoun Date: Tue, 20 Feb 2024 13:35:02 +0900 Subject: [PATCH 22/31] =?UTF-8?q?feature-022=20:=20insight=20true=20false?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SearchPage/SearchResultBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SearchPage/SearchResultBox.tsx b/src/components/SearchPage/SearchResultBox.tsx index cc2a41d..1bbf1b2 100644 --- a/src/components/SearchPage/SearchResultBox.tsx +++ b/src/components/SearchPage/SearchResultBox.tsx @@ -19,7 +19,7 @@ const SearchResultBox: React.FC = ({ video, tags }) => { target.style.display = 'none'; }; const handleOnclick = () => { - nav(`/summary/${video.video_id}?insight=${userName?.name === video.user}`); + nav(`/summary/${video.video_id}?insight=${userName?.name !== video.user}`); }; return ( From d6dafac6a8cdca7506692f735fc4ae45927b549c Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 15:47:31 +0900 Subject: [PATCH 23/31] =?UTF-8?q?feature-081:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModelController/ModelController.tsx | 10 -------- .../layout/header/alarm/AlarmItem.tsx | 7 ++---- src/components/layout/header/alarm/index.tsx | 24 +++++++------------ src/hooks/useCreateVideo.ts | 10 ++++++++ src/hooks/useGetAlarm.ts | 16 +++++++++++++ src/stores/user.ts | 6 +++++ 6 files changed, 43 insertions(+), 30 deletions(-) create mode 100644 src/hooks/useGetAlarm.ts diff --git a/src/components/common/ModelController/ModelController.tsx b/src/components/common/ModelController/ModelController.tsx index c0acf7a..f20dc83 100644 --- a/src/components/common/ModelController/ModelController.tsx +++ b/src/components/common/ModelController/ModelController.tsx @@ -55,15 +55,6 @@ const ModelController = () => { setVideoLink(null); }; - const handleUpdateAlarm = async (title: string) => { - await createVideoAlarmAPI(0, 'success', { - title: `[${title}]`, - content: - '영상이 모두 변환되었어요!\n이제 정리 된 영상을 확인하러 가볼까요?', - is_confirm: false, - }); - }; - useEffect(() => { if (!videoLink) return; @@ -109,7 +100,6 @@ const ModelController = () => { updated_at: new Date().toString(), }); setModelingProgress(100); - handleUpdateAlarm(finalData.title); setModelingStatus('COMPLETE'); } catch (e) { console.error(e); diff --git a/src/components/layout/header/alarm/AlarmItem.tsx b/src/components/layout/header/alarm/AlarmItem.tsx index d7f8b06..f59d184 100644 --- a/src/components/layout/header/alarm/AlarmItem.tsx +++ b/src/components/layout/header/alarm/AlarmItem.tsx @@ -17,7 +17,6 @@ import { modelingStatusState, } from '@/stores/model-controller'; import theme from '@/styles/theme'; -import useCreateVideo from '@/hooks/useCreateVideo'; import { confirmSelectAlarmAPI } from '@/apis/user'; type Props = { @@ -38,7 +37,6 @@ const AlarmItem = ({ const navigate = useNavigate(); const status = useRecoilValue(modelingStatusState); const progress = useRecoilValue(modelingProgressState); - const { createVideo } = useCreateVideo(); const isSelected = selectIdList.indexOf(alarm.alarm_id) > -1; const type = () => { @@ -76,12 +74,11 @@ const AlarmItem = ({ navigate('/guide'); onClose(); } - if (alarm.type === 'video' && !alarm.is_confirm) { + if (alarm.type === 'video' && !alarm.is_confirm && alarm.alarm_id !== 999) { try { await confirmSelectAlarmAPI({ alarms: [alarm.alarm_id] }); - onRefresh(); - createVideo(); + navigate(`/summary/${alarm.video_id}`); onClose(); } catch (e) { console.error(e); diff --git a/src/components/layout/header/alarm/index.tsx b/src/components/layout/header/alarm/index.tsx index 8c5023b..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,25 +21,21 @@ 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' || status === 'COMPLETE') { - callAPI(); + getAlarm(); } if (status === 'CONTINUE') { @@ -78,7 +72,7 @@ const Alarm = ({ isDark }: Props) => { {isOpen && ( setIsOpen(false)} /> )} diff --git a/src/hooks/useCreateVideo.ts b/src/hooks/useCreateVideo.ts index a0f4323..75e6a6e 100644 --- a/src/hooks/useCreateVideo.ts +++ b/src/hooks/useCreateVideo.ts @@ -1,3 +1,4 @@ +import { createVideoAlarmAPI } from '@/apis/user'; import { createVideoAPI } from '@/apis/videos'; import { errorModalState } from '@/stores/modal'; import { @@ -9,6 +10,7 @@ import { 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); @@ -17,6 +19,7 @@ const useCreateVideo = () => { const setVideoLink = useSetRecoilState(videoLinkState); const setStatus = useSetRecoilState(modelingStatusState); const setProgress = useSetRecoilState(modelingProgressState); + const { getAlarm } = useGetAlarm(); const navigate = useNavigate(); const createVideo = async () => { @@ -26,6 +29,13 @@ const useCreateVideo = () => { 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) { 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/stores/user.ts b/src/stores/user.ts index b9a6f60..6e26e83 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -3,6 +3,7 @@ import { atom } from 'recoil'; import { MyInfoResponse } from '@/models/user'; import localStorageEffect from './effects/localStorageEffect'; +import { IAlarm } from '@/models/alarm'; export const userInfoState = atom({ key: 'user-info', @@ -14,3 +15,8 @@ export const userTokenState = atom({ default: null, effects_UNSTABLE: [localStorageEffect], }); + +export const userAlarmState = atom({ + key: 'user-alarm', + default: [], +}); From 276353fd46675a0b2560282fea4b8c815f11c271 Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 16:22:33 +0900 Subject: [PATCH 24/31] =?UTF-8?q?feature-081:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=202=EC=A4=84=EB=A1=9C=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/layout/header/alarm/AlarmItem.style.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/styles/layout/header/alarm/AlarmItem.style.ts b/src/styles/layout/header/alarm/AlarmItem.style.ts index b1eeb0d..eb4e232 100644 --- a/src/styles/layout/header/alarm/AlarmItem.style.ts +++ b/src/styles/layout/header/alarm/AlarmItem.style.ts @@ -120,6 +120,10 @@ export const Container = styled.div` & h1 { font-weight: bold; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + -webkit-line-clamp: 2; } } } From 660c26a265a6d7427b7fbe81fba02ff61e17eda9 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: Tue, 20 Feb 2024 19:34:06 +0900 Subject: [PATCH 25/31] =?UTF-8?q?feature-074:=20=EA=B0=81=EC=A2=85=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- public/{assets/favicon.png => favicon.ico} | Bin src/assets/icons/dark-logo.svg | 15 ++ src/assets/icons/light-logo.svg | 15 ++ src/components/Home/SearchYoutube.tsx | 12 +- .../SummaryScriptBox/ToolBox/Indicator.tsx | 30 ++- .../ModelController/ModelController.tsx | 2 +- src/components/layout/header/index.tsx | 9 +- src/pages/FindEmailPage.tsx | 179 ++++++++------ src/pages/SignInPage.tsx | 14 +- src/pages/SignUpPage.tsx | 26 +- src/styles/SummaryPage.ts | 2 +- src/styles/signup/SignuppageStyle.ts | 234 +++++++++--------- 13 files changed, 308 insertions(+), 232 deletions(-) rename public/{assets/favicon.png => favicon.ico} (100%) create mode 100644 src/assets/icons/dark-logo.svg create mode 100644 src/assets/icons/light-logo.svg diff --git a/index.html b/index.html index 31fcf3b..a2dccdd 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Vi.No diff --git a/public/assets/favicon.png b/public/favicon.ico similarity index 100% rename from public/assets/favicon.png rename to public/favicon.ico diff --git a/src/assets/icons/dark-logo.svg b/src/assets/icons/dark-logo.svg new file mode 100644 index 0000000..5bbbe4e --- /dev/null +++ b/src/assets/icons/dark-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/icons/light-logo.svg b/src/assets/icons/light-logo.svg new file mode 100644 index 0000000..0bf9383 --- /dev/null +++ b/src/assets/icons/light-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/components/Home/SearchYoutube.tsx b/src/components/Home/SearchYoutube.tsx index 11ee27f..b578bf4 100644 --- a/src/components/Home/SearchYoutube.tsx +++ b/src/components/Home/SearchYoutube.tsx @@ -121,7 +121,17 @@ const SearchYoutube = () => {
-

{getTitle()}

+

+ {getTitle()} +

{(status === 'ERROR' || (!isValidate && inputLink !== '')) && ( diff --git a/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx b/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx index 0a0fb23..c6d608c 100644 --- a/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx +++ b/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx @@ -14,7 +14,7 @@ const Indicator = () => { summaryPlaySubHeadingIdState, ); - const [focusId, setFocusId] = useState(summaryVideo.subHeading[0].id); + const [focusId, setFocusId] = useState(-1); useEffect(() => { const handleScroll = () => { @@ -23,11 +23,21 @@ const Indicator = () => { .map((el) => el.getBoundingClientRect().top - containerTop) .filter((top) => top < 100); // window.innerHeight * 0.3 - const { id } = summaryVideo.subHeading[list.length - 1]; - setFocusId(id); + (document.querySelector('.tools') as HTMLDivElement).style.boxShadow = + `0 4px 40px 0 rgba(0, 0, 0, ${container.scrollTop > 0 ? 0.05 : 0})`; + + if (list.length) { + const { id } = summaryVideo.subHeading[list.length - 1]; + + setFocusId(id); + } }; + if (summaryVideo.subHeading.length) { + setFocusId(summaryVideo.subHeading[0].id); + } + const container = document.querySelector('#script-box') as HTMLDivElement; container.addEventListener('scroll', handleScroll); @@ -47,13 +57,15 @@ const Indicator = () => { const container = document.querySelector('#script-box') as HTMLDivElement; const element = document.querySelectorAll('.script-box')[findIdx]; - const { top: containerTop } = container.getBoundingClientRect(); - const { top } = element.getBoundingClientRect(); + if (element) { + const { top: containerTop } = container.getBoundingClientRect(); + const { top } = element.getBoundingClientRect(); - container.scrollTo({ - top: container.scrollTop + top - containerTop, - behavior: 'smooth', - }); + container.scrollTo({ + top: container.scrollTop + top - containerTop - 20, + behavior: 'smooth', + }); + } } setFocusId(playSubHeadingId); diff --git a/src/components/common/ModelController/ModelController.tsx b/src/components/common/ModelController/ModelController.tsx index f20dc83..6dab468 100644 --- a/src/components/common/ModelController/ModelController.tsx +++ b/src/components/common/ModelController/ModelController.tsx @@ -59,7 +59,7 @@ const ModelController = () => { if (!videoLink) return; const callProcess1API = async () => { - setModelingProgress(Math.ceil(Math.random() * 5)); + setModelingProgress(0); try { const { videoId } = (await modelingProcess1(videoLink)).data.result; diff --git a/src/components/layout/header/index.tsx b/src/components/layout/header/index.tsx index d2bf1f4..44daa8d 100644 --- a/src/components/layout/header/index.tsx +++ b/src/components/layout/header/index.tsx @@ -5,8 +5,8 @@ import { useRecoilState, useRecoilValue } from 'recoil'; import CloseIcon from '@/assets/icons/close.svg?react'; import MenuIcon from '@/assets/icons/menu.svg?react'; import SearchIcon from '@/assets/icons/search-light.svg?react'; -import DarkLogoImage from '@/assets/logo-dark.png'; -import LightLogoImage from '@/assets/logo-light.png'; +import DarkLogoIcon from '@/assets/icons/dark-logo.svg?react'; +import LightLogoIcon from '@/assets/icons/light-logo.svg?react'; import * as HeaderStyle from '@/styles/layout/header'; @@ -71,10 +71,7 @@ const Header = () => { - Logo + {isDarkSection ? : } diff --git a/src/pages/FindEmailPage.tsx b/src/pages/FindEmailPage.tsx index 34346db..07411e5 100644 --- a/src/pages/FindEmailPage.tsx +++ b/src/pages/FindEmailPage.tsx @@ -1,16 +1,20 @@ -import React, { useState} from 'react'; -import styled from 'styled-components'; +import React, { useState } from 'react'; import { Link } from 'react-router-dom'; -import smallLogo from "../assets/logo.png"; -import theme from '@/styles/theme'; +import styled from 'styled-components'; + +import { findEmailAPI } from '@/apis/user'; + +import LogoIcon from '@/assets/icons/dark-logo.svg?react'; + import NotFindUserModal from '@/components/modals/NotFindUserModal'; import PhoneCheck from '@/components/PhoneCheck'; -import { findEmailAPI } from '@/apis/user'; import FindEmail from '@/components/FindEmail'; import ImageSlider from '@/components/ImageSlider'; +import theme from '@/styles/theme'; + const FindEmailPage = () => { - const [name, setName] = useState(""); + const [name, setName] = useState(''); const [tel, setTel] = useState(''); const [email, setEmail] = useState(''); const [isModal, setIsModal] = useState(false); @@ -19,82 +23,100 @@ const FindEmailPage = () => { const onChangeName = (e: React.ChangeEvent) => { setName(e.target.value); - } + }; const findBtnHandler = async () => { try { - const {data} = await findEmailAPI({ + const { data } = await findEmailAPI({ name: name, phone_number: tel, }); - if(data.success){ + if (data.success) { setIsFind(true); setEmail(data.email); } } catch (error) { - setIsModal(true); - } -} + setIsModal(true); + } + }; -if(isFind){ + if (isFind) { + return ( + + + + ); + } return ( - - - ); -} -return ( - - - - - - 로고 이미지 -

이메일 찾기

-

이메일이 기억나지 않으시나요?

-
- - - - - - - 찾아보기 - - - + + + + + +

이메일 찾기

+

이메일이 기억나지 않으시나요?

+
+ + + + + + + 찾아보기 + + + 계정이 기억나시나요? 로그인 - - - - 아직 계정이 없으신가요? + + + + 아직 계정이 없으신가요? 이메일로 회원가입 - - -
-
- {isModal && } -
+ + + + + {isModal && } + ); }; export default FindEmailPage; - const Container = styled.div` display: flex; `; @@ -109,14 +131,13 @@ const Wrapper = styled.div` gap: 124px; `; - const MainSection = styled.div` display: flex; flex-direction: column; align-items: center; + justify-content: center; width: auto; height: 840px; - margin-top: 300px; `; const Intro = styled.div` @@ -154,7 +175,6 @@ const InputSection = styled.div` height: auto; `; - const Label = styled.label` span { font-size: 16px; @@ -164,11 +184,15 @@ const Label = styled.label` font-weight: 500; line-height: 160%; } + + &:not(:first-of-type) { + margin-top: 20px; + } `; const TwoLabel = styled.label` - display : flex; - flex-direction : column; + display: flex; + flex-direction: column; margin-bottom: 8px; span { font-size: 16px; @@ -177,12 +201,15 @@ const TwoLabel = styled.label` font-weight: 500; line-height: 160%; } + + &:not(:first-of-type) { + margin-top: 20px; + } `; const InputBox = styled.input` display: flex; align-items: center; - margin-bottom : 8px; justify-content: center; width: 494px; height: 56px; @@ -191,14 +218,14 @@ const InputBox = styled.input` flex: 1 0 0; font-size: 16px; font-style: normal; - color: var(--Main, #1E1E1E); + color: var(--Main, #1e1e1e); font-family: Pretendard; font-weight: 500; line-height: 160%; border-radius: 12px; border: 1.5px solid var(--gray-200, #e8e8e8); outline: none; - + &:hover { border: 1.5px solid #1e1e1e; } @@ -216,8 +243,8 @@ const InputBox = styled.input` const FindButton = styled.button` width: 494px; height: 56px; - background: #1E1E1E; - color: #EEEEEE; + background: #1e1e1e; + color: #eeeeee; font-size: 16px; font-weight: 500; line-height: 160%; @@ -228,32 +255,32 @@ const FindButton = styled.button` cursor: pointer; } &:disabled { - background-color : #F3F3F3; - color : #BBBBBB; + background-color: #f3f3f3; + color: #bbbbbb; } `; const TextTotalComponent = styled.div` display: flex; flex-direction: row; - margin: "0px"; + margin: '0px'; `; const TextDiv = styled.div` - color: ${(props) => props.color || "#1e1e1e"}; + color: ${(props) => props.color || '#1e1e1e'}; text-transform: capitalize; font-size: 36px; font-weight: bold; font-style: normal; line-height: 160%; /* 57.6px */ font-family: Pretendard; - margin: "0px"; + margin: '0px'; `; const StyledLink = styled(Link)` color: ${({ theme }) => theme.color.gray500}; - ${ theme.typography.Body3 }; + ${theme.typography.Body3}; text-align: center; text-decoration: none; - margin : 0px 0px 0px 10px; + margin: 0px 0px 0px 10px; `; diff --git a/src/pages/SignInPage.tsx b/src/pages/SignInPage.tsx index 1563696..2d2aa59 100644 --- a/src/pages/SignInPage.tsx +++ b/src/pages/SignInPage.tsx @@ -8,7 +8,7 @@ import styled from 'styled-components'; import { loginAPI } from '@/apis/user'; import CloseIcon from '@/assets/icons/close.svg?react'; -import smallLogo from '@/assets/logo-dark.png'; +import LogoIcon from '@/assets/icons/dark-logo.svg?react'; import lineImg from '@/assets/line_img.png'; import errorImg from '@/assets/Error.png'; import signupImg from '@/assets/before-login.png'; @@ -69,11 +69,11 @@ const SignInPage: React.FC = () => { } } catch (error) { if (error instanceof AxiosError) { - const { message } = error.response?.data as APIBaseResponse; + const { code } = error.response?.data as APIBaseResponse; - if (message.indexOf('비밀번호') > -1) { + if (code === 'WRONG_PASSWORD') { setIsOpenErrorModal(true); - } else if (message.indexOf('이메일') > -1) { + } else if (code === 'NOT_FOUND_EMAIL') { setIsOpenSignUpModal(true); } } @@ -88,7 +88,7 @@ const SignInPage: React.FC = () => { const handleOnClick = () => { handleClickLoginButton(); - } + }; const redirect_uri = `${location.origin}/social-account`; //Redirect URI const KAKAO_KEY = '77ddf1baeb87f4a9752ed437db43cd96'; //kakao REST API KEY @@ -104,7 +104,7 @@ const SignInPage: React.FC = () => { return ( - logo + 로그인 { @@ -165,7 +168,7 @@ const SignUp = () => { const handleOnClick = () => { onApply(); - } + }; return ( @@ -175,7 +178,7 @@ const SignUp = () => { - 로고 이미지 +

회원가입

새로운 계정을 생성하고 나만의 영상 아카이빙을 시작해요

@@ -333,10 +336,7 @@ const SignUp = () => { isPassword && passwordCheck && !mismatchError ? ( - + 가입하기 ) : ( diff --git a/src/styles/SummaryPage.ts b/src/styles/SummaryPage.ts index c8ff6b1..ce5213b 100644 --- a/src/styles/SummaryPage.ts +++ b/src/styles/SummaryPage.ts @@ -260,7 +260,7 @@ export const ScriptBox = styled.div` align-items: center; justify-content: space-between; width: 100%; - box-shadow: 0 4px 40px 0 rgba(0, 0, 0, 0.05); + transition: box-shadow 0.1s; & button.edit-button { padding: 8px 20px; diff --git a/src/styles/signup/SignuppageStyle.ts b/src/styles/signup/SignuppageStyle.ts index d302af7..15178e1 100644 --- a/src/styles/signup/SignuppageStyle.ts +++ b/src/styles/signup/SignuppageStyle.ts @@ -12,7 +12,7 @@ export const Wrapper = styled.div` `; export const LogoSection = styled.div` - img{ + img { display: flex; width: auto; height: 840px; @@ -26,8 +26,9 @@ export const MainSection = styled.div` justify-content: center; width: 580px; height: 896px; - margin-top: 128px; + margin-top: 95px; `; + export const InputSection = styled.div` display: flex; flex-direction: column; @@ -38,13 +39,13 @@ export const InputSection = styled.div` &::-webkit-scrollbar { width: 8px; } - &::-webkit-scrollbar-thumb{ + &::-webkit-scrollbar-thumb { height: 200px; - background-color:#f3f3f3; + background-color: #f3f3f3; border-radius: 100px; } - &::-webkit-scrollbar-track{ - background-color:#ffffff; + &::-webkit-scrollbar-track { + background-color: #ffffff; } `; @@ -101,8 +102,8 @@ export const TwoLabel = styled.label` `; export const ThreeLabel = styled.label` - display : flex; - flex-direction : column; + display: flex; + flex-direction: column; margin-bottom: 8px; `; @@ -117,12 +118,12 @@ export const PhoneInputBox = styled.input` flex: 1 0 0; font-size: 16px; font-style: normal; - color: var(--Main, #1E1E1E); + color: var(--Main, #1e1e1e); font-family: Pretendard; font-weight: 500; line-height: 160%; border-radius: 12px; - border: 1.5px solid var(--gray-200, #e8e8e8) ; + border: 1.5px solid var(--gray-200, #e8e8e8); margin-top: 8px; outline: none; &:hover { @@ -187,7 +188,7 @@ export const EmailInputBox = styled.input` flex: 1 0 0; font-size: 16px; font-style: normal; - color: var(--Main, #1E1E1E); + color: var(--Main, #1e1e1e); font-family: Pretendard; font-weight: 500; line-height: 160%; @@ -235,14 +236,14 @@ export const BirthInputBox = styled.input` flex: 1 0 0; font-size: 16px; font-style: normal; - color: var(--Main, #1E1E1E); + color: var(--Main, #1e1e1e); font-family: Pretendard; font-weight: 500; line-height: 160%; margin-right: 8px; border-radius: 12px; border: 1.5px solid #e8e8e8; - color: var(--Main, #1E1E1E); + color: var(--Main, #1e1e1e); background: #fff; &::placeholder { color: #bbb; @@ -256,7 +257,6 @@ export const BirthInputBox = styled.input` } `; - export const SexSelectBox = styled.div` display: flex; flex-direction: row; @@ -280,7 +280,8 @@ export const SexButton = styled.button<{ selected: boolean }>` line-height: 160%; margin-right: 10px; cursor: pointer; - ${(props) => props.selected && + ${(props) => + props.selected && ` background: #1e1e1e; color: #fff; @@ -298,7 +299,7 @@ export const Error = styled.p` `; export const Avail = styled.p` - color: #3681FE; + color: #3681fe; font-size: 14px; font-weight: 500; margin-top: 8px; @@ -334,7 +335,7 @@ export const Button = styled.button` export const SucButton = styled.button` border-radius: 12px; - background: #1E1E1E; + background: #1e1e1e; color: #fff; display: flex; width: 494px; @@ -352,8 +353,8 @@ export const SucButton = styled.button` export const DupSucButton = styled.button` border-radius: 12px; - background: #E9FF3F; - color: #1E1E1E; + background: #e9ff3f; + color: #1e1e1e; display: flex; margin-top: 8px; margin-left: 8px; @@ -369,8 +370,8 @@ export const DupSucButton = styled.button` border: none; cursor: pointer; &:disabled { - background-color : ${theme.color.gray100}; - color : ${theme.color.gray300}; + background-color: ${theme.color.gray100}; + color: ${theme.color.gray300}; cursor: default; } `; @@ -431,15 +432,14 @@ export const RetryButton = styled.button((props) => ({ })); export const PwDiv = styled.div` - font-size: 14px; - margin-top: 8px; - color:#3681FE; - font-weight: 500; - line-height: 160%; - padding-left: 16px; + font-size: 14px; + margin-top: 8px; + color: #3681fe; + font-weight: 500; + line-height: 160%; + padding-left: 16px; `; - export const TextTotalComponent = styled.div` display: flex; flex-direction: row; @@ -447,118 +447,118 @@ export const TextTotalComponent = styled.div` `; export const TextDiv = styled.div` - font-size: 14px; - color: #BBB; - font-weight: 500; - line-height: 160%; + font-size: 14px; + color: #bbb; + font-weight: 500; + line-height: 160%; `; export const UserButton = styled.button` - width : 160px; - height : 56px; - color : #1E1E1E; - background-color : ${theme.color.green400}; - border : none; - border-radius : 12px; + width: 160px; + height: 56px; + color: #1e1e1e; + background-color: ${theme.color.green400}; + border: none; + border-radius: 12px; margin-top: 8px; ${theme.typography.Body1}; &:disabled { - background-color : ${theme.color.gray100}; - color : ${theme.color.gray300}; + background-color: ${theme.color.gray100}; + color: ${theme.color.gray300}; } -` +`; export const UserDiv = styled.div` - display : flex; - flex-direction : row; - gap : 10px; -` + display: flex; + flex-direction: row; + gap: 10px; +`; export const SendMsg = styled.div` - margin-left : 16px; - margin-top : 8px; - ${theme.typography.Body3}; - color : ${theme.color.red}; -` + margin-left: 16px; + margin-top: 8px; + ${theme.typography.Body3}; + color: ${theme.color.red}; +`; export const CustomButton = styled.button` - width : 54px; - height : 54px; - display : flex; - align-items: center; - justify-content: center; - background : #1E1E1E; - border : none; - border-radius : 12px; -` -export const CalendarContainer = styled.div` + width: 54px; + height: 54px; + display: flex; + align-items: center; + justify-content: center; + background: #1e1e1e; + border: none; + border-radius: 12px; +`; +export const CalendarContainer = styled.div` .custom-inputSelected { - background : #BBBBBB !important; + background: #bbbbbb !important; } .react-datepicker { & select { - border : none; - color : #1E1E1E; - font-weight: bold; + border: none; + color: #1e1e1e; + font-weight: bold; } & button { - border : none; - border-radius : 8px; + border: none; + border-radius: 8px; } .react-datepicker__month-container { - .react-datepicker__header { - background-color: white; - border: none; - } - .react-datepicker__day-name { - margin: 0px 7px 0px 7px; - } - .react-datepicker__month { - .react-datepicker__day { - margin: 5px 7px 5px 7px; - &:hover { - border-radius: 18px; - background-color: #FBFFCC - } - } - .react-datepicker__day--today, - .react-datepicker__day--keyboard-selected { - border-radius: 18px; - background-color: #E9FF3F; - font-weight: 400; - } - .react-datepicker__day--selected, - .react-datepicker__day--in-range, - .react-datepicker__day--in-selecting-range { - border-radius: 18px; - background-color: #E9FF3F; - color: black; - } + .react-datepicker__header { + background-color: white; + border: none; + } + .react-datepicker__day-name { + margin: 0px 7px 0px 7px; + } + .react-datepicker__month { + .react-datepicker__day { + margin: 5px 7px 5px 7px; + &:hover { + border-radius: 18px; + background-color: #fbffcc; } + } + .react-datepicker__day--today, + .react-datepicker__day--keyboard-selected { + border-radius: 18px; + background-color: #e9ff3f; + font-weight: 400; + } + .react-datepicker__day--selected, + .react-datepicker__day--in-range, + .react-datepicker__day--in-selecting-range { + border-radius: 18px; + background-color: #e9ff3f; + color: black; + } } + } } - .react-datepicker__aria-live, - .react-datepicker__time-list-item—disabled, - .react-datepicker-time__header { - display: none; - } + .react-datepicker__aria-live, + .react-datepicker__time-list-item—disabled, + .react-datepicker-time__header { + display: none; + } - .react-datepicker__time-container { - overflow-y: scroll; - height: 100px; - cursor: pointer; - } - .react-datepicker__input-container > input, - .react-datepicker__time-container { - width: 80px; - background-color: #f9f9f9; - outline: none; - text-align: center; - overflow-x: hidden; - } - .react-datepicker__time-list-item—selected { - background-color: #fff2b4 !important; - color: black !important; - } -` \ No newline at end of file + .react-datepicker__time-container { + overflow-y: scroll; + height: 100px; + cursor: pointer; + } + .react-datepicker__input-container > input, + .react-datepicker__time-container { + width: 80px; + background-color: #f9f9f9; + outline: none; + text-align: center; + overflow-x: hidden; + } + .react-datepicker__time-list-item—selected { + background-color: #fff2b4 !important; + color: black !important; + } +`; From 6bc075780dddc65e72ba4d30841d5e537400c31f Mon Sep 17 00:00:00 2001 From: KimSehyeoun Date: Tue, 20 Feb 2024 20:08:30 +0900 Subject: [PATCH 26/31] =?UTF-8?q?feature-076=20:=20400=EB=B2=88=EB=8C=80?= =?UTF-8?q?=EC=97=90=EB=A7=8C=20=EC=A4=91=EB=B3=B5=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PhoneCheck.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/PhoneCheck.tsx b/src/components/PhoneCheck.tsx index 51edf6b..1b13755 100644 --- a/src/components/PhoneCheck.tsx +++ b/src/components/PhoneCheck.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { sendSMSAPI, checkSMSAPI, sendSMSFindAPI } from '@/apis/sms'; import Container from '@/styles/PhoneCheck'; +import { AxiosError } from "axios"; interface PhoneCheckProps { @@ -100,7 +101,9 @@ const PhoneCheck : React.FC = ({setCheck, tel, setTel, type}) = setToken(data.result.token); } } catch(e){ - setPhoneCertify(true); + const err = e as AxiosError; + if(err.response?.status === 400) + setPhoneCertify(true); } } From d7714c2484a359b51fdefb6307773538edc24080 Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 22:40:48 +0900 Subject: [PATCH 27/31] =?UTF-8?q?hotfix-082:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EC=95=88=20=EB=9C=A8=EB=8A=94=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/modals/AddCategoryModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/modals/AddCategoryModal.tsx b/src/components/modals/AddCategoryModal.tsx index fccc42f..3353c08 100644 --- a/src/components/modals/AddCategoryModal.tsx +++ b/src/components/modals/AddCategoryModal.tsx @@ -47,7 +47,6 @@ const AddCategoryModal = ({ isTopCategoryModalOpen ? setIsTopCategoryModalOpen(false) : setIsSubCategoryModalOpen(false); - setCategoryName(''); }; const [topCategoryModalRef] = useOutsideClick(onCloseModal); From eb629317e5dc9b6630b17a072fa7c8a3d34b530a Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 22:44:09 +0900 Subject: [PATCH 28/31] =?UTF-8?q?hotfix-082:=20=ED=99=88=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=20=EC=83=89=EC=83=81=20=EB=B3=80=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/category/Card.style.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/styles/category/Card.style.ts b/src/styles/category/Card.style.ts index eba2049..39802a5 100644 --- a/src/styles/category/Card.style.ts +++ b/src/styles/category/Card.style.ts @@ -47,6 +47,10 @@ export const DropdownWrap = styled.div` fill: ${(props) => props.theme.color.gray400}; } } + + &.changed { + background-color: ${theme.color.green400}; + } } `; From 0bd2807be1a7cc680e84d68582fc892a7b81a88c Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 22:50:36 +0900 Subject: [PATCH 29/31] =?UTF-8?q?hotfix-082:=20=EC=95=8C=EB=9E=8C=EC=9D=84?= =?UTF-8?q?=20=EC=9D=BD=EC=97=88=EB=8D=94=EB=9D=BC=EB=8F=84=20=EC=9A=94?= =?UTF-8?q?=EC=95=BD=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/header/alarm/AlarmItem.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/layout/header/alarm/AlarmItem.tsx b/src/components/layout/header/alarm/AlarmItem.tsx index f59d184..0e122f4 100644 --- a/src/components/layout/header/alarm/AlarmItem.tsx +++ b/src/components/layout/header/alarm/AlarmItem.tsx @@ -72,18 +72,23 @@ const AlarmItem = ({ 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); + if ( + alarm.type === 'video' && + alarm.state !== 'fail' && + alarm.alarm_id !== 999 + ) { + if (!alarm.is_confirm) { + try { + await confirmSelectAlarmAPI({ alarms: [alarm.alarm_id] }); + onRefresh(); + } catch (e) { + console.error(e); + } } + navigate(`/summary/${alarm.video_id}`); } + onClose(); }; const handleClickRemoveButton: React.MouseEventHandler = ( From e802532f8038caaeb951798f1fd281aba8466944 Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 22:59:57 +0900 Subject: [PATCH 30/31] =?UTF-8?q?hotfix-082:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EC=9D=BD=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/header/alarm/AlarmItem.tsx | 18 +++++++++--------- .../layout/header/alarm/AlarmItem.style.ts | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/layout/header/alarm/AlarmItem.tsx b/src/components/layout/header/alarm/AlarmItem.tsx index 0e122f4..83b66dd 100644 --- a/src/components/layout/header/alarm/AlarmItem.tsx +++ b/src/components/layout/header/alarm/AlarmItem.tsx @@ -70,22 +70,22 @@ const AlarmItem = ({ }; const handleClick = async () => { + if (!alarm.is_confirm) { + try { + await confirmSelectAlarmAPI({ alarms: [alarm.alarm_id] }); + onRefresh(); + } catch (e) { + console.error(e); + } + } if (alarm.type === 'notice') { navigate('/guide'); } if ( alarm.type === 'video' && - alarm.state !== 'fail' && + alarm.state === 'success' && alarm.alarm_id !== 999 ) { - if (!alarm.is_confirm) { - try { - await confirmSelectAlarmAPI({ alarms: [alarm.alarm_id] }); - onRefresh(); - } catch (e) { - console.error(e); - } - } navigate(`/summary/${alarm.video_id}`); } onClose(); diff --git a/src/styles/layout/header/alarm/AlarmItem.style.ts b/src/styles/layout/header/alarm/AlarmItem.style.ts index eb4e232..7b31746 100644 --- a/src/styles/layout/header/alarm/AlarmItem.style.ts +++ b/src/styles/layout/header/alarm/AlarmItem.style.ts @@ -2,6 +2,7 @@ import theme from '@/styles/theme'; import styled from 'styled-components'; export const Container = styled.div` + cursor: pointer !important; padding: 28px 0; display: flex; flex-direction: column; From 2b799355c154973a61cb08bdf927def9c6494925 Mon Sep 17 00:00:00 2001 From: gs0428 Date: Tue, 20 Feb 2024 23:52:34 +0900 Subject: [PATCH 31/31] =?UTF-8?q?hotfix-083:=20=ED=95=98=EC=9C=84=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/sideBar/AddCategory.tsx | 10 ++++-- src/components/layout/sideBar/SubCategory.tsx | 8 ++--- src/components/layout/sideBar/TopCategory.tsx | 25 +++++++++----- src/components/layout/sideBar/UserMode.tsx | 16 ++++----- src/components/modals/AddCategoryModal.tsx | 34 +++++++++---------- src/stores/modal.ts | 10 +++--- types/category.ts | 11 ++++++ 7 files changed, 66 insertions(+), 48 deletions(-) diff --git a/src/components/layout/sideBar/AddCategory.tsx b/src/components/layout/sideBar/AddCategory.tsx index 210aa73..909c334 100644 --- a/src/components/layout/sideBar/AddCategory.tsx +++ b/src/components/layout/sideBar/AddCategory.tsx @@ -1,18 +1,22 @@ import * as AddCategoryStyle from '@/styles/layout/sideBar/AddCategory.style'; import PlusSvg from '@/assets/icons/plus.svg?react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { topCategoryModalState } from '@/stores/modal'; +import { addCategoryModalState } from '@/stores/modal'; import { userTokenState } from '@/stores/user'; import { useState } from 'react'; import NoticeModal from '@/components/modals/NoticeModal'; const AddCategory = () => { const isUser = useRecoilValue(userTokenState); - const setTopCategoryModal = useSetRecoilState(topCategoryModalState); + const setIsAddCategoryModalOpen = useSetRecoilState(addCategoryModalState); const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false); const openAddModal = (e: React.MouseEvent) => { - setTopCategoryModal(true); + setIsAddCategoryModalOpen({ + location: 'top', + isOpen: true, + categoryId: -1, + }); e.stopPropagation(); }; diff --git a/src/components/layout/sideBar/SubCategory.tsx b/src/components/layout/sideBar/SubCategory.tsx index dfcca2c..c248f90 100644 --- a/src/components/layout/sideBar/SubCategory.tsx +++ b/src/components/layout/sideBar/SubCategory.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import * as SubCategoryStyles from '@/styles/layout/sideBar/SubCategory.style'; import Option from './Option'; import handleDrag from '@/utils/handleDrag'; -import { ISubFolderProps } from 'types/category'; +import { IEditProps, ISubFolderProps } from 'types/category'; import EditCategoryName from '@/components/category/EditCategoryName'; import useCreateToast from '@/hooks/useCreateToast'; @@ -13,10 +13,8 @@ interface ISubCategoryProps { subId: number; subFolder: ISubFolderProps; grabedCategory: React.MutableRefObject; - isEditing: { activated: boolean; categoryId: number }; - setIsEditing: React.Dispatch< - React.SetStateAction<{ activated: boolean; categoryId: number }> - >; + isEditing: IEditProps; + setIsEditing: React.Dispatch>; setIsDeleteModalOpen: React.Dispatch>; putCategoryFolder: () => void; setCategoryId: React.Dispatch>; diff --git a/src/components/layout/sideBar/TopCategory.tsx b/src/components/layout/sideBar/TopCategory.tsx index d717eba..9493cfe 100644 --- a/src/components/layout/sideBar/TopCategory.tsx +++ b/src/components/layout/sideBar/TopCategory.tsx @@ -7,7 +7,12 @@ import useOutsideClick from '@/hooks/useOutsideClick'; import SubCategory from './SubCategory'; import Option from './Option'; import handleDrag from '@/utils/handleDrag'; -import { IFolderProps, ISubFolderProps } from 'types/category'; +import { + IAddCategoryModalProps, + IEditProps, + IFolderProps, + ISubFolderProps, +} from 'types/category'; import EditCategoryName from '@/components/category/EditCategoryName'; interface ITopCategoryProps { @@ -17,11 +22,11 @@ interface ITopCategoryProps { grabedCategory: React.MutableRefObject; dropedCategory: React.MutableRefObject; category: IFolderProps; - isEditing: { activated: boolean; categoryId: number }; - setIsEditing: React.Dispatch< - React.SetStateAction<{ activated: boolean; categoryId: number }> + isEditing: IEditProps; + setIsEditing: React.Dispatch>; + setIsAddCategoryModalOpen: React.Dispatch< + React.SetStateAction >; - setIsSubCategoryModalOpen: React.Dispatch>; setIsDeleteModalOpen: React.Dispatch>; setCategoryId: React.Dispatch>; putCategoryFolder: () => void; @@ -36,7 +41,7 @@ const TopCategory = ({ category, isEditing, setIsEditing, - setIsSubCategoryModalOpen, + setIsAddCategoryModalOpen, setIsDeleteModalOpen, putCategoryFolder, setCategoryId, @@ -53,7 +58,12 @@ const TopCategory = ({ const handleOptionClick = (e: React.MouseEvent, option: string) => { e.stopPropagation(); if (option === '추가') { - setIsSubCategoryModalOpen(true); + console.log(category.categoryId); + setIsAddCategoryModalOpen({ + location: 'sub', + isOpen: true, + categoryId: category.categoryId, + }); } else if (option === '수정') { setIsEditing({ activated: true, categoryId: category.categoryId }); setBeforeEdit(edit); @@ -101,7 +111,6 @@ const TopCategory = ({ return ( <> { - const isTopCategoryModalOpen = useRecoilValue(topCategoryModalState); + const [isAddCategoryModalOpen, setIsAddCategoryModalOpen] = useRecoilState( + addCategoryModalState, + ); const [isSuccessAddCategoryModalOpen, setIsSuccessAddCategoryModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -24,7 +26,6 @@ const UserMode = () => { const [categoryId, setCategoryId] = useState(null); const [to, setTo] = useState(''); const categories = useRecoilValue(categoryState); - const [isSubCategoryModalOpen, setIsSubCategoryModalOpen] = useState(false); const grabedCategory = useRef(undefined); const dropedCategory = useRef(undefined); const { createToast } = useCreateToast(); @@ -96,7 +97,7 @@ const UserMode = () => { category={category} isEditing={isEditing} setIsEditing={setIsEditing} - setIsSubCategoryModalOpen={setIsSubCategoryModalOpen} + setIsAddCategoryModalOpen={setIsAddCategoryModalOpen} setIsDeleteModalOpen={setIsDeleteModalOpen} putCategoryFolder={putCategoryFolder} setCategoryId={setCategoryId} @@ -104,14 +105,11 @@ const UserMode = () => { /> ))} - {(isTopCategoryModalOpen || isSubCategoryModalOpen) && ( + {isAddCategoryModalOpen.isOpen && ( )} diff --git a/src/components/modals/AddCategoryModal.tsx b/src/components/modals/AddCategoryModal.tsx index 3353c08..78f8c6d 100644 --- a/src/components/modals/AddCategoryModal.tsx +++ b/src/components/modals/AddCategoryModal.tsx @@ -1,6 +1,6 @@ import useOutsideClick from '@/hooks/useOutsideClick'; -import { topCategoryModalState } from '@/stores/modal'; -import { useSetRecoilState } from 'recoil'; +import { addCategoryModalState } from '@/stores/modal'; +import { useRecoilState } from 'recoil'; import OpenFileSvg from '@/assets/icons/open-file.svg?react'; import CloseSvg from '@/assets/icons/close.svg?react'; import * as AddTopCategoryModalStyles from '@/styles/modals/AddCategoryModal.style'; @@ -17,22 +17,18 @@ import useUpdateCategories from '@/hooks/useUpdateCategories'; import useCreateToast from '@/hooks/useCreateToast'; interface IAddTopCategoryModalProps extends ICommonModalProps { - isTopCategoryModalOpen: boolean; - setIsSubCategoryModalOpen: React.Dispatch>; - topCategoryId: number; setTo: React.Dispatch>; } const AddCategoryModal = ({ - isTopCategoryModalOpen, - setIsSubCategoryModalOpen, categoryName, setCategoryName, setIsSuccessAddCategoryModalOpen, - topCategoryId, setTo, }: IAddTopCategoryModalProps) => { - const setIsTopCategoryModalOpen = useSetRecoilState(topCategoryModalState); + const [isAddCategoryModalOpen, setIsAddCategoryModalOpen] = useRecoilState( + addCategoryModalState, + ); const { createToast } = useCreateToast(); const { updateCategories } = useUpdateCategories(); const { editText } = handleEdit(); @@ -42,12 +38,14 @@ const AddCategoryModal = ({ const categoryNameRegex = /^[a-zA-Z0-9가-힣\s]*$/; const checkCategoryNameRegex = categoryNameRegex.test(categoryName); const addEnabled = categoryName.length > 0 && checkCategoryNameRegex; + const isTopAdd = isAddCategoryModalOpen.location === 'top'; - const onCloseModal = () => { - isTopCategoryModalOpen - ? setIsTopCategoryModalOpen(false) - : setIsSubCategoryModalOpen(false); - }; + const onCloseModal = () => + setIsAddCategoryModalOpen({ + location: '', + isOpen: false, + categoryId: -1, + }); const [topCategoryModalRef] = useOutsideClick(onCloseModal); @@ -59,13 +57,13 @@ const AddCategoryModal = ({ createToast(`'기타' 이름은 사용하실 수 없어요`); return; } - const response = isTopCategoryModalOpen + const response = isTopAdd ? await postTopCategroy(categoryName) - : await postSubCategroy(categoryName, topCategoryId); + : await postSubCategroy(categoryName, isAddCategoryModalOpen.categoryId); if (response.isSuccess) { updateCategories(); setTo( - isTopCategoryModalOpen + isTopAdd ? `${response.result.categoryId}` : `${response.result.topCategoryId}/${response.result.categoryId}`, ); @@ -82,7 +80,7 @@ const AddCategoryModal = ({ - {isTopCategoryModalOpen ? '상위' : '하위'} 카테고리 추가 + {isTopAdd ? '상위' : '하위'} 카테고리 추가 만들고 싶은 카테고리의 이름을 작성해주세요 diff --git a/src/stores/modal.ts b/src/stores/modal.ts index 5adae7b..5570113 100644 --- a/src/stores/modal.ts +++ b/src/stores/modal.ts @@ -1,8 +1,8 @@ import { atom } from 'recoil'; -export const topCategoryModalState = atom({ - key: 'topCategoryModal', - default: false, +export const addCategoryModalState = atom({ + key: 'addCategoryModal', + default: { location: '', isOpen: false, categoryId: -1 }, }); export const summaryTransformModalState = atom({ @@ -16,6 +16,6 @@ export const recommendationModalState = atom({ }); export const errorModalState = atom({ - key: 'error-modal', - default: false, + key: 'error-modal', + default: false, }); diff --git a/types/category.ts b/types/category.ts index ac6a12b..4dbe752 100644 --- a/types/category.ts +++ b/types/category.ts @@ -20,3 +20,14 @@ export interface ITagProps { tag_id: number; name: string; } + +export interface IEditProps { + activated: boolean; + categoryId: number; +} + +export interface IAddCategoryModalProps { + location: string; + isOpen: boolean; + categoryId: number; +}