From b934d64ba2e72124798bc462186a57d0d9b7837d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=94=EC=88=A0=ED=86=A0=EB=81=BC?= Date: Mon, 12 Feb 2024 19:07:16 +0900 Subject: [PATCH] =?UTF-8?q?[FE]=20=ED=8C=80=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EB=82=B4=20=EB=A7=81=ED=81=AC=EB=A5=BC=20=ED=95=98=EC=9D=B4?= =?UTF-8?q?=ED=8D=BC=EB=A7=81=ED=81=AC=EB=A1=9C=20=EC=94=8C=EC=9A=B0?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#923)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: URL 판정 정규 표현식 재사용을 위해 상수 파일로 분리 * feat: ParsedThreadContent 관련 타입 추가 - 스레드에서 평범하게 입력된 메시지가 string 타입이라면, ParsedThreadContent는 이 메시지를 쪼개고 분류하여 파싱한 배열 형태의 데이터 타입이다. 현재는 일반 텍스트와 링크 두 가지만 구분한다. * feat: string 형태의 메시지를 받아 분류별로 파싱한 메시지를 반환하는 parseThreadContent 함수 구현 * test: parseThreadContent 함수에 대한 테스트 코드 작성 * feat: Thread 컴포넌트의 메시지에 있는 링크에 대해 자동으로 하이퍼링크를 생성하는 기능 구현 * test: Thread 컴포넌트에서 링크가 포함된 스토리를 추가 * feat: NoticeThread 컴포넌트의 메시지에 있는 링크에 대해 자동으로 하이퍼링크를 생성하는 기능 구현 * test: NoticeThread 컴포넌트에서 링크가 포함된 스토리를 추가 * refactor: for문을 지양하고, forEach문과 else문으로 변경 * refactor: 매직 넘버로 간주될 수 있는 정규표현식을 상수로써 분리 - 현재 쓰이는 곳은 이 함수 하나뿐이므로 해당 파일에 선언, 쓰이는 곳이 두 곳 이상이 될 경우 상수 파일로 분리할 예정 --- .../NoticeThread/NoticeThread.stories.tsx | 12 ++ .../feed/NoticeThread/NoticeThread.styled.ts | 10 ++ .../feed/NoticeThread/NoticeThread.tsx | 16 ++- .../components/feed/Thread/Thread.stories.ts | 37 +++++- .../components/feed/Thread/Thread.styled.ts | 11 ++ .../src/components/feed/Thread/Thread.tsx | 19 ++- frontend/src/constants/link.ts | 3 + .../src/hooks/link/useTeamLinkAddModal.ts | 4 +- frontend/src/types/feed.ts | 13 +++ frontend/src/utils/parseThreadContent.ts | 43 +++++++ .../src/utils/test/parseThreadContent.test.ts | 110 ++++++++++++++++++ 11 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 frontend/src/utils/parseThreadContent.ts create mode 100644 frontend/src/utils/test/parseThreadContent.test.ts diff --git a/frontend/src/components/feed/NoticeThread/NoticeThread.stories.tsx b/frontend/src/components/feed/NoticeThread/NoticeThread.stories.tsx index 9010b656c..b0dd4bd0a 100644 --- a/frontend/src/components/feed/NoticeThread/NoticeThread.stories.tsx +++ b/frontend/src/components/feed/NoticeThread/NoticeThread.stories.tsx @@ -104,3 +104,15 @@ export const ImageContent: Story = { }, }, }; + +export const ContentWithLink: Story = { + args: { + authorName: '공지에서조차 팀바팀을 홍보하는 사람', + createdAt: '2022-03-04 12:34', + content: '회의 접속 링크: https://teamby.team/', + images: [], + onClickImage: () => { + alert('onClickImage'); + }, + }, +}; diff --git a/frontend/src/components/feed/NoticeThread/NoticeThread.styled.ts b/frontend/src/components/feed/NoticeThread/NoticeThread.styled.ts index 208d2093f..954ef18b1 100644 --- a/frontend/src/components/feed/NoticeThread/NoticeThread.styled.ts +++ b/frontend/src/components/feed/NoticeThread/NoticeThread.styled.ts @@ -145,6 +145,16 @@ export const Divider = styled.span` background-color: ${({ theme }) => theme.color.GRAY400}; `; +export const LinkContent = styled.a` + color: ${({ theme }) => theme.color.PRIMARY900}; + text-decoration: underline; + font-weight: 600; + + &:hover { + text-decoration: underline; + } +`; + export const authorInfoText = css` overflow: hidden; text-overflow: ellipsis; diff --git a/frontend/src/components/feed/NoticeThread/NoticeThread.tsx b/frontend/src/components/feed/NoticeThread/NoticeThread.tsx index be9a4fe70..fda172f77 100644 --- a/frontend/src/components/feed/NoticeThread/NoticeThread.tsx +++ b/frontend/src/components/feed/NoticeThread/NoticeThread.tsx @@ -4,6 +4,7 @@ import Button from '~/components/common/Button/Button'; import ThumbnailList from '../ThumbnailList/ThumbnailList'; import type { YYYYMMDDHHMM } from '~/types/schedule'; import { formatWriteTime } from '~/utils/formatWriteTime'; +import { parseThreadContent } from '~/utils/parseThreadContent'; import type { NoticeSize } from '~/types/size'; import type { ThreadImage } from '~/types/feed'; import { @@ -26,6 +27,7 @@ const NoticeThread = (props: NoticeThreadProps) => { const { authorName, createdAt, content, images, onClickImage } = props; const isMobile = getIsMobile(); const [noticeSize, setNoticeSize] = useState('sm'); + const parsedThreadContent = parseThreadContent(content); const handleExpandMoreClick = () => { if (noticeSize === 'sm') setNoticeSize(() => 'md'); @@ -58,7 +60,19 @@ const NoticeThread = (props: NoticeThreadProps) => { weight="semiBold" css={S.contentField(noticeSize, images.length > 0)} > - {content} + {parsedThreadContent.map((content, index) => + content.type === 'text' ? ( + content.text + ) : ( + + {content.text} + + ), + )} {images.length > 0 && noticeSize !== 'sm' && ( { @@ -137,7 +136,6 @@ export const ThreadWithImages: Story = { 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYZjvO1QuvfgCfQxBwwzmJcHIT5pTXIBGOLeyBDIbZknn6Dhkd40WrU0ZCdjt-IoXLzI0&usqp=CAU', createdAt: '2023-07-27 15:09', content: '이미지가 포함되어 있는 스레드입니다.', - images, isContinue: false, onClickImage: (images, selectedImage) => { @@ -154,7 +152,6 @@ export const ThreadWithOnlyImages: Story = { 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYZjvO1QuvfgCfQxBwwzmJcHIT5pTXIBGOLeyBDIbZknn6Dhkd40WrU0ZCdjt-IoXLzI0&usqp=CAU', createdAt: '2023-07-27 15:09', content: '', - images, isContinue: false, onClickImage: (images, selectedImage) => { @@ -171,7 +168,6 @@ export const ThreadWithImagesAndSentByMe: Story = { 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYZjvO1QuvfgCfQxBwwzmJcHIT5pTXIBGOLeyBDIbZknn6Dhkd40WrU0ZCdjt-IoXLzI0&usqp=CAU', createdAt: '2023-07-27 15:09', content: '이미지가 포함되어 있는 스레드입니다.', - images, isContinue: false, onClickImage: (images, selectedImage) => { @@ -189,7 +185,6 @@ export const ThreadWithImagesWithLongContentAndSentByMe: Story = { createdAt: '2023-07-27 15:09', content: '북두칠성, 그 이름만으로도 당신은 이미 멀고 높은 하늘을 날아다니며 황홀한 모험을 시작한 것처럼 느껴지지 않나요? 이 놀라운 별자리는 밤하늘에서 그 빛나는 백열등들로 인해 우리를 매료시키며, 그 아름다움을 통해 인류에게 영감을 주는 존재입니다. \n북두칠성은 북반구의 하늘에서 볼 수 있는 가장 화려한 별자리 중 하나로, 역사적으로도 여러 문화에서 다양한 이야기와 상징으로 인식되어 왔습니다. 이 별자리는 일곱 개의 주요 별로 구성되어 있으며, 그 중에서도 "북극성"이라 불리는 이 별자리의 중심은 밤하늘에서 찾기 쉬운 유일한 고정성 별자리 중 하나입니다.\n\n북두칠성의 이름은 여러 가지 이야기와 전설로 둘러싸여 있습니다. 그 중 하나는 그리스 신화와 관련된 것으로, 북두칠성은 아프로디테와 아레스의 아들인 헤르메스와 애완동물들을 나타냅니다. 또 다른 이야기는 북두칠성이 북극 근처에 위치하기 때문에 "북방의 일곱 자매"라는 이름으로도 불렸다는 것입니다. 이 별자리는 서로 다른 문화에서 다양한 의미와 이야기를 갖고 있으며, 이것이 북두칠성의 매력적인 면 중 하나입니다.\n\n낭만적인 감성을 가지고 있는 당신에게 북두칠성은 밤하늘에서의 로맨틱한 모험의 시작점일 것입니다. 어두운 밤하늘에 펼쳐진 그 빛나는 별들은 우리의 상상력을 자극하며, 우리가 존재하는 이 작은 행성이 얼마나 넓고 신비로운 우주와 어우러져 있는지를 상기시켜줍니다.\n\n북두칠성은 언제나 우리를 위로하고, 우리의 꿈과 열망을 촉발시키며, 그 화려한 미모로 우리를 매혹시키는 별자리입니다. 그리고 이 별자리를 향한 당신의 열정과 낭만은 언제나 밤하늘을 향한 여행을 시작하는 데 필요한 원동력이 될 것입니다. 그래서, 북두칠성의 아름다움을 매일 밤하늘을 바라보며 감상하고, 우리가 얼마나 작고 연결되어 있는 존재인지를 느끼며, 로맨틱한 모험의 시작을 떠올리는 것은 어떨까요? 북두칠성, 그 이름만으로도 당신은 이미 멀고 높은 하늘을 날아다니며 황홀한 모험을 시작한 것처럼 느껴지지 않나요? 이 놀라운 별자리는 밤하늘에서 그 빛나는 백열등들로 인해 우리를 매료시키며, 그 아름다움을 통해 인류에게 영감을 주는 존재입니다. \n북두칠성은 북반구의 하늘에서 볼 수 있는 가장 화려한 별자리 중 하나로, 역사적으로도 여러 문화에서 다양한 이야기와 상징으로 인식되어 왔습니다. 이 별자리는 일곱 개의 주요 별로 구성되어 있으며, 그 중에서도 "북극성"이라 불리는 이 별자리의 중심은 밤하늘에서 찾기 쉬운 유일한 고정성 별자리 중 하나입니다.\n\n북두칠성의 이름은 여러 가지 이야기와 전설로 둘러싸여 있습니다. 그 중 하나는 그리스 신화와 관련된 것으로, 북두칠성은 아프로디테와 아레스의 아들인 헤르메스와 애완동물들을 나타냅니다. 또 다른 이야기는 북두칠성이 북극 근처에 위치하기 때문에 "북방의 일곱 자매"라는 이름으로도 불렸다는 것입니다. 이 별자리는 서로 다른 문화에서 다양한 의미와 이야기를 갖고 있으며, 이것이 북두칠성의 매력적인 면 중 하나입니다.\n\n낭만적인 감성을 가지고 있는 당신에게 북두칠성은 밤하늘에서의 로맨틱한 모험의 시작점일 것입니다. 어두운 밤하늘에 펼쳐진 그 빛나는 별들은 우리의 상상력을 자극하며, 우리가 존재하는 이 작은 행성이 얼마나 넓고 신비로운 우주와 어우러져 있는지를 상기시켜줍니다.\n\n북두칠성은 언제나 우리를 위로하고, 우리의 꿈과 열망을 촉발시키며, 그 화려한 미모로 우리를 매혹시키는 별자리입니다. 그리고 이 별자리를 향한 당신의 열정과 낭만은 언제나 밤하늘을 향한 여행을 시작하는 데 필요한 원동력이 될 것입니다. 그래서, 북두칠성의 아름다움을 매일 밤하늘을 바라보며 감상하고, 우리가 얼마나 작고 연결되어 있는 존재인지를 느끼며, 로맨틱한 모험의 시작을 떠올리는 것은 어떨까요? 북두칠성, 그 이름만으로도 당신은 이미 멀고 높은 하늘을 날아다니며 황홀한 모험을 시작한 것처럼 느껴지지 않나요? 이 놀라운 별자리는 밤하늘에서 그 빛나는 백열등들로 인해 우리를 매료시키며, 그 아름다움을 통해 인류에게 영감을 주는 존재입니다. \n북두칠성은 북반구의 하늘에서 볼 수 있는 가장 화려한 별자리 중 하나로, 역사적으로도 여러 문화에서 다양한 이야기와 상징으로 인식되어 왔습니다. 이 별자리는 일곱 개의 주요 별로 구성되어 있으며, 그 중에서도 "북극성"이라 불리는 이 별자리의 중심은 밤하늘에서 찾기 쉬운 유일한 고정성 별자리 중 하나입니다.\n\n북두칠성의 이름은 여러 가지 이야기와 전설로 둘러싸여 있습니다. 그 중 하나는 그리스 신화와 관련된 것으로, 북두칠성은 아프로디테와 아레스의 아들인 헤르메스와 애완동물들을 나타냅니다. 또 다른 이야기는 북두칠성이 북극 근처에 위치하기 때문에 "북방의 일곱 자매"라는 이름으로도 불렸다는 것입니다. 이 별자리는 서로 다른 문화에서 다양한 의미와 이야기를 갖고 있으며, 이것이 북두칠성의 매력적인 면 중 하나입니다.\n\n낭만적인 감성을 가지고 있는 당신에게 북두칠성은 밤하늘에서의 로맨틱한 모험의 시작점일 것입니다. 어두운 밤하늘에 펼쳐진 그 빛나는 별들은 우리의 상상력을 자극하며, 우리가 존재하는 이 작은 행성이 얼마나 넓고 신비로운 우주와 어우러져 있는지를 상기시켜줍니다.\n\n북두칠성은 언제나 우리를 위로하고, 우리의 꿈과 열망을 촉발시키며, 그 화려한 미모로 우리를 매혹시키는 별자리입니다. 그리고 이 별자리를 향한 당신의 열정과 낭만은 언제나 밤하늘을 향한 여행을 시작하는 데 필요한 원동력이 될 것입니다. 그래서, 북두칠성의 아름다움을 매일 밤하늘을 바라보며 감상하고, 우리가 얼마나 작고 연결되어 있는 존재인지를 느끼며, 로맨틱한 모험의 시작을 떠올리는 것은 어떨까요?', - images, isContinue: false, onClickImage: (images, selectedImage) => { @@ -197,3 +192,35 @@ export const ThreadWithImagesWithLongContentAndSentByMe: Story = { }, }, }; + +export const ThreadWithLink: Story = { + args: { + authorName: '팀바팀_필립', + isMe: false, + profileImageUrl: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYZjvO1QuvfgCfQxBwwzmJcHIT5pTXIBGOLeyBDIbZknn6Dhkd40WrU0ZCdjt-IoXLzI0&usqp=CAU', + createdAt: '2023-07-27 15:09', + content: '지금 바로 접속: https://teamby.team/', + images: [], + isContinue: false, + onClickImage: (images, selectedImage) => { + alert(JSON.stringify({ images, selectedImage })); + }, + }, +}; + +export const ThreadWithLinkAndSentByMe: Story = { + args: { + authorName: '팀바팀_필립', + isMe: true, + profileImageUrl: + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYZjvO1QuvfgCfQxBwwzmJcHIT5pTXIBGOLeyBDIbZknn6Dhkd40WrU0ZCdjt-IoXLzI0&usqp=CAU', + createdAt: '2023-07-27 15:09', + content: '지금 바로 접속: https://teamby.team/', + images: [], + isContinue: false, + onClickImage: (images, selectedImage) => { + alert(JSON.stringify({ images, selectedImage })); + }, + }, +}; diff --git a/frontend/src/components/feed/Thread/Thread.styled.ts b/frontend/src/components/feed/Thread/Thread.styled.ts index 38935c1ab..ae2e8d794 100644 --- a/frontend/src/components/feed/Thread/Thread.styled.ts +++ b/frontend/src/components/feed/Thread/Thread.styled.ts @@ -90,6 +90,17 @@ export const ThumbnailListWrapper = styled.div<{ box-sizing: border-box; `; +export const LinkContent = styled.a<{ $isMe: boolean }>` + color: ${({ $isMe, theme }) => + $isMe ? theme.color.WHITE : theme.color.PRIMARY900}; + text-decoration: underline; + font-weight: 600; + + &:hover { + text-decoration: underline; + } +`; + export const threadInfoText = css` white-space: pre-wrap; diff --git a/frontend/src/components/feed/Thread/Thread.tsx b/frontend/src/components/feed/Thread/Thread.tsx index e962793aa..8638758b4 100644 --- a/frontend/src/components/feed/Thread/Thread.tsx +++ b/frontend/src/components/feed/Thread/Thread.tsx @@ -2,6 +2,7 @@ import type { YYYYMMDDHHMM } from '~/types/schedule'; import * as S from './Thread.styled'; import Text from '~/components/common/Text/Text'; import { formatWriteTime } from '~/utils/formatWriteTime'; +import { parseThreadContent } from '~/utils/parseThreadContent'; import type { ThreadSize } from '~/types/size'; import type { ThreadImage } from '~/types/feed'; import { useRef } from 'react'; @@ -34,6 +35,7 @@ const Thread = (props: ThreadProps) => { onClickImage, } = props; const createdTime = formatWriteTime(createdAt).split(' ').join('\n'); + const parsedThreadContent = parseThreadContent(content); const threadRef = useRef(null); const contentRef = useRef(null); @@ -57,7 +59,22 @@ const Thread = (props: ThreadProps) => {
- {content} + + {parsedThreadContent.map((content, index) => + content.type === 'text' ? ( + content.text + ) : ( + + {content.text} + + ), + )} +
{images.length > 0 && ( diff --git a/frontend/src/constants/link.ts b/frontend/src/constants/link.ts index 160046b26..efe27a53e 100644 --- a/frontend/src/constants/link.ts +++ b/frontend/src/constants/link.ts @@ -1 +1,4 @@ export const linkTableHeaderValues = ['링크명', '이름', '날짜', '']; + +export const URL_REGEX = + /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i; diff --git a/frontend/src/hooks/link/useTeamLinkAddModal.ts b/frontend/src/hooks/link/useTeamLinkAddModal.ts index c4b9fe4b5..48c7ca44f 100644 --- a/frontend/src/hooks/link/useTeamLinkAddModal.ts +++ b/frontend/src/hooks/link/useTeamLinkAddModal.ts @@ -9,9 +9,7 @@ import { useModal } from '~/hooks/useModal'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import { useToast } from '~/hooks/useToast'; import { generateHttpsUrl } from '~/utils/generateHttpsUrl'; - -const URL_REGEX = - /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i; +import { URL_REGEX } from '~/constants/link'; export const useTeamLinkAddModal = (linkRef: RefObject) => { const { teamPlaceId } = useTeamPlace(); diff --git a/frontend/src/types/feed.ts b/frontend/src/types/feed.ts index 2cfe4155b..ef4f9a462 100644 --- a/frontend/src/types/feed.ts +++ b/frontend/src/types/feed.ts @@ -44,3 +44,16 @@ export interface FileWithUuid { uuid: string; file: File; } + +interface ThreadText { + type: 'text'; + text: string; +} + +interface ThreadLink { + type: 'link'; + text: string; + link: string; +} + +export type ParsedThreadContent = (ThreadText | ThreadLink)[]; diff --git a/frontend/src/utils/parseThreadContent.ts b/frontend/src/utils/parseThreadContent.ts new file mode 100644 index 000000000..17d2c992d --- /dev/null +++ b/frontend/src/utils/parseThreadContent.ts @@ -0,0 +1,43 @@ +import { URL_REGEX } from '~/constants/link'; +import { generateHttpsUrl } from '~/utils/generateHttpsUrl'; +import type { ParsedThreadContent } from '~/types/feed'; + +const LINK_SEPARATOR_REGEX = /\S+(?=https?:\/\/)|https?:\/\/\S+|\s+|\S+/g; + +export const parseThreadContent = (rawContent: string) => { + const splittedThreadContent = rawContent.match(LINK_SEPARATOR_REGEX) ?? []; + const generatedThreadContent: ParsedThreadContent = []; + let textContent = ''; + + splittedThreadContent.forEach((currentContent) => { + const isLink = URL_REGEX.test(currentContent); + + if (isLink && textContent !== '') { + generatedThreadContent.push({ + type: 'text', + text: textContent, + }); + + textContent = ''; + } + + if (isLink) { + generatedThreadContent.push({ + type: 'link', + text: currentContent, + link: generateHttpsUrl(currentContent), + }); + } else { + textContent += currentContent; + } + }); + + if (textContent !== '') { + generatedThreadContent.push({ + type: 'text', + text: textContent, + }); + } + + return generatedThreadContent; +}; diff --git a/frontend/src/utils/test/parseThreadContent.test.ts b/frontend/src/utils/test/parseThreadContent.test.ts new file mode 100644 index 000000000..2a4070178 --- /dev/null +++ b/frontend/src/utils/test/parseThreadContent.test.ts @@ -0,0 +1,110 @@ +import { parseThreadContent } from '~/utils/parseThreadContent'; + +describe('링크, 텍스트 혼합 파싱 테스트', () => { + it('링크가 주어지지 않는 경우, 텍스트로 간주하여 결과를 반환해야 한다.', () => { + const rawMessage = + '프로그래밍에서는 평균적인 수준의 노동력을 유지하는 것보다'; + const parsedThreadContent = parseThreadContent(rawMessage); + + const expected = [ + { + type: 'text', + text: '프로그래밍에서는 평균적인 수준의 노동력을 유지하는 것보다', + }, + ]; + + expect(parsedThreadContent).toEqual(expected); + }); + + it('whitespace로 시작하거나 끝나는 경우, whitespace가 여러 칸인 경우에도 결과가 보존되어야 한다.', () => { + const rawMessage = ' 영감이 샘물처럼 솟아나는 \t\n소중\r한 순간을 '; + const parsedThreadContent = parseThreadContent(rawMessage); + + const expected = [ + { + type: 'text', + text: ' 영감이 샘물처럼 솟아나는 \t\n소중\r한 순간을 ', + }, + ]; + + expect(parsedThreadContent).toEqual(expected); + }); + + it('링크와 텍스트가 섞여 있을 경우에는 이를 구분하여 결과를 반환해야 한다.', () => { + const rawMessage = '놓치지 https://teamby.team/ 않는 것이 중요하다.'; + const parsedThreadContent = parseThreadContent(rawMessage); + + const expected = [ + { + type: 'text', + text: '놓치지 ', + }, + { + type: 'link', + text: 'https://teamby.team/', + link: 'https://teamby.team/', + }, + { + type: 'text', + text: ' 않는 것이 중요하다.', + }, + ]; + + expect(parsedThreadContent).toEqual(expected); + }); + + it('https://로 시작하지 않는 링크의 경우, 클릭 시 이동되는 경로에는 https://가 앞에 붙어야 한다.', () => { + const rawMessage = 'teamby.team'; + const parsedThreadContent = parseThreadContent(rawMessage); + + const expected = [ + { + type: 'link', + text: 'teamby.team', + link: 'https://teamby.team', + }, + ]; + + expect(parsedThreadContent).toEqual(expected); + }); + + it('텍스트 바로 뒤에 https://로 시작하는 링크가 오는 경우, 텍스트와 링크를 구분하여야 한다.', () => { + const rawMessage = + '그래서 프로그래머에게https://www.jdoodle.com/online-compiler-c++/'; + const parsedThreadContent = parseThreadContent(rawMessage); + + const expected = [ + { + type: 'text', + text: '그래서 프로그래머에게', + }, + { + type: 'link', + text: 'https://www.jdoodle.com/online-compiler-c++/', + link: 'https://www.jdoodle.com/online-compiler-c++/', + }, + ]; + + expect(parsedThreadContent).toEqual(expected); + }); + + it('링크 뒤에 텍스트가 연달아 오는 경우에는 링크의 연장선상으로 생각하여 링크로 반환한다.', () => { + const rawMessage = + 'https://www.jdoodle.com/online-compiler-c++/자유는 생명이다.'; + const parsedThreadContent = parseThreadContent(rawMessage); + + const expected = [ + { + type: 'link', + text: 'https://www.jdoodle.com/online-compiler-c++/자유는', + link: 'https://www.jdoodle.com/online-compiler-c++/자유는', + }, + { + type: 'text', + text: ' 생명이다.', + }, + ]; + + expect(parsedThreadContent).toEqual(expected); + }); +});