From 4a3fa9ceec6b0ca1435414ef875e8d4f2fa4dbd9 Mon Sep 17 00:00:00 2001 From: whistleJs Date: Fri, 16 Feb 2024 02:34:24 +0900 Subject: [PATCH] =?UTF-8?q?feature-057:=20=EC=98=81=EC=83=81=20=EC=9A=94?= =?UTF-8?q?=EC=95=BD=20API=20=EC=97=B0=EA=B2=B0=20=EC=A7=84=ED=96=89?= =?UTF-8?q?=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChangeKeyword/ChangeKeywordModal.tsx | 87 +++++++++++++++---- .../SummaryScriptBox/SummaryScriptBox.tsx | 31 +++++-- src/components/common/ToastList/ToastItem.tsx | 13 +-- src/components/common/ToastList/style.ts | 3 + src/styles/SummaryPage.ts | 39 ++++++--- 5 files changed, 129 insertions(+), 44 deletions(-) diff --git a/src/components/SummaryPage/SummaryScriptBox/ChangeKeyword/ChangeKeywordModal.tsx b/src/components/SummaryPage/SummaryScriptBox/ChangeKeyword/ChangeKeywordModal.tsx index 24c8c4e..4de590d 100644 --- a/src/components/SummaryPage/SummaryScriptBox/ChangeKeyword/ChangeKeywordModal.tsx +++ b/src/components/SummaryPage/SummaryScriptBox/ChangeKeyword/ChangeKeywordModal.tsx @@ -14,11 +14,13 @@ import { IVideoSubHeading } from '@/models/video'; import { ModalContainer } from '@/styles/SummaryPage'; +import { toastListState } from '@/stores/toast'; import { summaryFindKeywordCountState, summarySearchIndexState, summaryVideoState, } from '@/stores/summary'; + import { getSearchIndex } from '@/utils/summary'; type Props = { @@ -32,6 +34,7 @@ const ChangeKeywordModal = ({ onChange, onClose }: Props) => { const [summaryVideo, setSummaryVideo] = useRecoilState(summaryVideoState); const findKeywordCount = useRecoilValue(summaryFindKeywordCountState); const [searchIndex, setSearchIndex] = useRecoilState(summarySearchIndexState); + const [toastList, setToastList] = useRecoilState(toastListState); const [holdPosition, setHoldPosition] = useState({ x: -1, y: -1 }); const [position, setPosition] = useState({ x: 0, y: 0 }); @@ -45,6 +48,11 @@ const ChangeKeywordModal = ({ onChange, onClose }: Props) => { const { result } = (await getVideoAPI(summaryVideo.video_id)).data; setSummaryVideo(result); + + setToastList([ + ...toastList, + { id: Date.now(), content: '단어 변경이 완료되었어요!' }, + ]); } catch (e) { console.error(e); } @@ -101,37 +109,72 @@ const ChangeKeywordModal = ({ onChange, onClose }: Props) => { setPosition({ x, y }); }; - const handleClickChangeAllButton = () => { + const handleClickChangeAllButton = async () => { if (!summaryVideo) return; - const subHeading = summaryVideo.subHeading.map(({ content, ...others }) => { - return { - content: content.replace(new RegExp(keyword, 'g'), replaceKeyword), - ...others, - }; - }); + const regex = new RegExp(keyword, 'g'); + + const subHeading = summaryVideo.subHeading.map( + ({ name, content, ...others }) => { + return { + name: name.replace(regex, replaceKeyword), + content: content.replace(regex, replaceKeyword), + ...others, + }; + }, + ); + + await updateSubHeading(subHeading); - updateSubHeading(subHeading); + onClose(); }; - const handleClickChangeButton = () => { + const handleClickChangeButton = async () => { if (!summaryVideo) return; - const regex = new RegExp(keyword, 'g'); - const subHeadingList = [...summaryVideo.subHeading]; + const subHeadingList: IVideoSubHeading[] = []; let index = -1; - for (const i in summaryVideo.subHeading) { - const { content } = summaryVideo.subHeading[i]; + for (const subHeading of summaryVideo.subHeading) { + const splittedName = subHeading.name.split(keyword); + const splittedContent = subHeading.content.split(keyword); + let name = ''; + let content = ''; + + // 제목 탐색 (탐색 로직 같음) + for (let i = 0; i < splittedName.length; i++) { + if (i === splittedName.length - 1) { + name += splittedName[splittedName.length - 1]; + break; + } + index++; + + if (index === searchIndex) { + name += splittedName[i] + replaceKeyword; + } else { + name += splittedName[i] + keyword; + } + } - if (regex.test(content)) index++; - if (index === searchIndex) { - subHeadingList[i].content = content.replace(regex, replaceKeyword); - break; + // 컨텐츠 탐색 (탐색 로직 같음) + for (let i = 0; i < splittedContent.length; i++) { + if (i === splittedContent.length - 1) { + content += splittedContent[splittedContent.length - 1]; + break; + } + index++; + + if (index === searchIndex) { + content += splittedContent[i] + replaceKeyword; + } else { + content += splittedContent[i] + keyword; + } } + + subHeadingList.push({ ...subHeading, name, content }); } - updateSubHeading(subHeadingList); + await updateSubHeading(subHeadingList); }; useEffect(() => { @@ -233,11 +276,17 @@ const ChangeKeywordModal = ({ onChange, onClose }: Props) => {
-
diff --git a/src/components/SummaryPage/SummaryScriptBox/SummaryScriptBox.tsx b/src/components/SummaryPage/SummaryScriptBox/SummaryScriptBox.tsx index 8b67257..24a95a8 100644 --- a/src/components/SummaryPage/SummaryScriptBox/SummaryScriptBox.tsx +++ b/src/components/SummaryPage/SummaryScriptBox/SummaryScriptBox.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import ModifyIcon from '@/assets/icons/modify.svg?react'; @@ -41,36 +41,50 @@ const SummaryScriptBox = () => { const [focusId, setFocusId] = useState(1); const [keyword, setKeyword] = useState(''); - const handleChangeKeyword = (keyword: string) => { + const updateFindKeywordCount = useCallback(() => { if (keyword === '') { setFindKeywordCount(0); } else { setFindKeywordCount( summaryVideo.subHeading.reduce( - (total, { content }) => total + (content.split(keyword).length - 1), + (total, { name, content }) => + total + + (name.split(keyword).length - 1) + + (content.split(keyword).length - 1), 0, ), ); } + }, [summaryVideo, keyword, setFindKeywordCount]); + const handleChangeKeyword = (keyword: string) => { setKeyword(keyword); + updateFindKeywordCount(); }; const formattedScriptList = useMemo(() => { - return summaryVideo.subHeading.map(({ content, ...others }) => { + return summaryVideo.subHeading.map(({ name, content, ...others }) => { if ((searchIsOpen || transformModalIsOpen) && keyword !== '') { + name = name + .split(keyword) + .map((s) => escapeHTML(s)) + .join(`${escapeHTML(keyword)}`); + content = content .split(keyword) .map((s) => escapeHTML(s)) .join(`${escapeHTML(keyword)}`); } else { + name = escapeHTML(name); content = escapeHTML(content); } + name = name.replace(/\n/g, '
'); content = content.replace(/\n/g, '
'); return { content, + name, ...others, }; }); @@ -101,6 +115,10 @@ const SummaryScriptBox = () => { }); }, [searchIsOpen, transformModalIsOpen, keyword, searchIndex]); + useEffect(() => { + updateFindKeywordCount(); + }, [updateFindKeywordCount]); + return (
@@ -137,7 +155,10 @@ const SummaryScriptBox = () => { - {script.name} +
diff --git a/src/components/common/ToastList/ToastItem.tsx b/src/components/common/ToastList/ToastItem.tsx index 3c856b7..ab673ac 100644 --- a/src/components/common/ToastList/ToastItem.tsx +++ b/src/components/common/ToastList/ToastItem.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; import { IToast, toastListState } from '@/stores/toast'; @@ -8,23 +8,24 @@ type Props = { }; const ToastItem = ({ toast }: Props) => { - const [list, setList] = useRecoilState(toastListState); + const setList = useSetRecoilState(toastListState); const [isShow, setIsShow] = useState(true); useEffect(() => { const removeTimer = setTimeout(() => { - setList(list.filter((item) => item.id !== toast.id)); - }, 1000 * 4); + setList((list) => list.filter((item) => item.id !== toast.id)); + }, 1000 * 3); const hideTimer = setTimeout(() => { setIsShow(false); - }, 1000 * 3.5); + }, 1000 * 2.5); return () => { clearTimeout(hideTimer); clearTimeout(removeTimer); }; - }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return (
{toast.content}
diff --git a/src/components/common/ToastList/style.ts b/src/components/common/ToastList/style.ts index c880bc7..3197dd9 100644 --- a/src/components/common/ToastList/style.ts +++ b/src/components/common/ToastList/style.ts @@ -30,6 +30,9 @@ export const Container = styled.div` left: 50%; bottom: 60px; transform: translateX(-50%); + display: flex; + flex-direction: column; + gap: 8px; & > .toast { display: flex; diff --git a/src/styles/SummaryPage.ts b/src/styles/SummaryPage.ts index 0c9e379..6eec058 100644 --- a/src/styles/SummaryPage.ts +++ b/src/styles/SummaryPage.ts @@ -242,7 +242,7 @@ export const ScriptBox = styled.div` & span.script-title { color: ${(props) => props.theme.color.gray500}; - ${(props) => props.theme.typography.Subheader1} + ${(props) => props.theme.typography.Subheader1}; } & span.play-button { @@ -263,18 +263,6 @@ export const ScriptBox = styled.div` font-weight: 600; line-height: 2; color: #5d5b5b; - - & > mark { - padding: 4px 8px; - border-radius: 4px; - background-color: #d2f1b4; - line-height: 1.6; - color: ${(props) => props.theme.color.gray500}; - - &.active { - background-color: #a4de6b; - } - } } & div.resize-thumb { @@ -285,6 +273,18 @@ export const ScriptBox = styled.div` height: 100%; cursor: ew-resize; } + + & mark { + padding: 4px 8px; + border-radius: 4px; + background-color: #d2f1b4; + line-height: 1.6; + color: ${(props) => props.theme.color.gray500}; + + &.active { + background-color: #a4de6b; + } + } `; export const Dropdown = styled.div` @@ -487,14 +487,25 @@ export const ModalContainer = styled(BlurBackground)` border-radius: 12px; background-color: ${(props) => props.theme.color.gray500}; color: white; + transition: 0.1s; cursor: pointer; ${(props) => props.theme.typography.Body1}; &.all { - background-color: white; + background-color: white !important; border: solid 1.5px ${(props) => props.theme.color.gray200}; color: ${(props) => props.theme.color.gray400}; } + + &:disabled { + background-color: ${(props) => props.theme.color.gray200}; + color: ${(props) => props.theme.color.gray300}; + cursor: not-allowed; + + &.all { + color: ${(props) => props.theme.color.gray300}; + } + } } `;