Skip to content

Commit

Permalink
게시글 작성 시 본문 링크 삽입을 하지 않더라도 사용자에게 링크 클릭이 가능하도록 변경 (#706)
Browse files Browse the repository at this point in the history
* feat: (#703) https | http | www을 판별하는 정규 표현식 구현

* test: (#703) 링크가 포함된 문자를 입력했을 때 문자에서 링크는 [[]]로 감싸서 반환하는 함수 테스트 및 정규표현식 수정

* feat: (#703) 게시글 작성 시 [[ ]] 를 따로 해주지 않고, 렌더링 시 링크에 [[]]로 감싸지도록 적용

* refactor: (#703) 게시글 작성, 댓글 작성 시 링크 넣기 버튼 삭제

* feat: (#703) 링크를 지원할 지 여부를 매개변수로 추가하여 홈에서는 링크를 span으로 렌더되도록 구현

* refactor: (#703) isLinkEnabled early return으로 코드 가독성 향상

* refactor: (#703) 홈에서도 링크에 접속 가능하도록 수정
웹 접근성을 위해 link로 들어가는 태그를 button으로 하여 스페이스바로 진입 가능하도록 함
  • Loading branch information
Gilpop8663 authored Oct 11, 2023
1 parent 1be7c74 commit 89089fb
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 41 deletions.
36 changes: 36 additions & 0 deletions frontend/__test__/convertTextToUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { convertTextToUrl } from '@utils/post/convertTextToUrl';

test.each([
['www.naver.com 이걸 어째', '[[www.naver.com]] 이걸 어째'],
[
'반갑다 https://github.com/woowacourse-teams/2023-votogether/issues/703 임',
'반갑다 [[https://github.com/woowacourse-teams/2023-votogether/issues/703]] 임',
],
['안녕 wwwww.naver.com', '안녕 wwwww.naver.com'],
['http://localhost:3000/ 피카츄', '[[http://localhost:3000/]] 피카츄'],
[
'http://localhost:3000/http://localhost:3000/ 피카츄',
'[[http://localhost:3000/http://localhost:3000/]] 피카츄',
],
['www.naver.com', '[[www.naver.com]]'],
['[[www.naver.com]] www.naver.com', '[[www.naver.com]] [[www.naver.com]]'],
[
'[[http://localhost:3000/]] http://localhost:3000/',
'[[http://localhost:3000/]] [[http://localhost:3000/]]',
],
[
'[[https://votogether.com/ranking]] https://www.naver.com/',
'[[https://votogether.com/ranking]] [[https://www.naver.com/]]',
],
[
'www.naver.com www.naver.com www.naver.com https://www.npmjs.com/package/dotenv-webpack',
'[[www.naver.com]] [[www.naver.com]] [[www.naver.com]] [[https://www.npmjs.com/package/dotenv-webpack]]',
],
])(
'convertTextToUrl 함수에서 링크가 포함된 문자를 입력했을 때 문자에서 링크는 [[]]로 감싸서 반환한다.',
(word, expectedWord) => {
const result = convertTextToUrl(word);

expect(result).toBe(expectedWord);
}
);
15 changes: 1 addition & 14 deletions frontend/src/components/PostForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,7 @@ export default function PostForm({ data, mutate, isSubmitting }: PostFormProps)
};

const { text: writingTitle, handleTextChange: handleTitleChange } = useText(title ?? '');
const {
text: writingContent,
handleTextChange: handleContentChange,
addText: addContent,
} = useText(content ?? '');
const { text: writingContent, handleTextChange: handleContentChange } = useText(content ?? '');
const multiSelectHook = useMultiSelect(categoryIds ?? [], POST_CATEGORY.MAX_AMOUNT);

const handleDeadlineButtonClick = (option: DeadlineOptionInfo) => {
Expand All @@ -128,10 +124,6 @@ export default function PostForm({ data, mutate, isSubmitting }: PostFormProps)
}
};

const handleInsertContentLink = () => {
addContent('[[이 괄호 안에 링크를 작성해주세요]] ');
};

const handlePostFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData();
Expand Down Expand Up @@ -223,11 +215,6 @@ export default function PostForm({ data, mutate, isSubmitting }: PostFormProps)
onPaste={handlePasteImage}
required
/>
<S.ContentLinkButtonWrapper>
<S.Button onClick={handleInsertContentLink} type="button">
본문에 링크 넣기
</S.Button>
</S.ContentLinkButtonWrapper>
<S.ContentImagePartWrapper $hasImage={!!contentImageHook.contentImage}>
<ContentImagePart size="lg" contentImageHook={contentImageHook} />
</S.ContentImagePartWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,7 @@ export default function CommentTextForm({
initialComment,
handleCancelClick,
}: CommentTextFormProps) {
const {
text: content,
handleTextChange,
resetText,
addText: addContent,
} = useText(initialComment.content);
const { text: content, handleTextChange, resetText } = useText(initialComment.content);
const { isToastOpen, openToast, toastMessage } = useToast();

const params = useParams() as { postId: string };
Expand Down Expand Up @@ -111,16 +106,6 @@ export default function CommentTextForm({
</SquareButton>
</S.ButtonWrapper>
)}
<S.ButtonWrapper>
<SquareButton
aria-label="댓글에 링크 넣기"
onClick={() => addContent('[[괄호 안에 링크 작성]] ')}
theme="blank"
type="button"
>
링크 넣기
</SquareButton>
</S.ButtonWrapper>
<S.ButtonWrapper>
<SquareButton
aria-label="댓글 저장"
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/post/Post/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ForwardedRef, forwardRef, memo, useContext, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

import { PostInfo } from '@type/post';

Expand Down Expand Up @@ -33,6 +34,7 @@ const Post = forwardRef(function Post(
{ postInfo, isPreview }: PostProps,
ref: ForwardedRef<HTMLLIElement>
) {
const navigate = useNavigate();
const {
postId,
category,
Expand Down Expand Up @@ -115,8 +117,14 @@ const Post = forwardRef(function Post(
return (
<S.Container as={isPreview ? 'li' : 'div'} ref={ref} $isPreview={isPreview}>
<S.DetailLink
role={isPreview ? 'link' : 'none'}
tabIndex={0}
as={isPreview ? '' : 'main'}
to={isPreview ? `${PATH.POST}/${postId}` : '#'}
onClick={() => {
if (!isPreview) return;

navigate(`${PATH.POST}/${postId}`);
}}
$isPreview={isPreview}
aria-describedby={
isPreview
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/post/Post/style.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Link } from 'react-router-dom';

import { styled } from 'styled-components';

import { theme } from '@styles/theme';
Expand Down Expand Up @@ -83,6 +81,7 @@ export const Content = styled.div<{ $isPreview: boolean }>`
text-overflow: ellipsis;
word-break: break-word;
white-space: pre-wrap;
text-align: start;
overflow: hidden;
Expand All @@ -94,10 +93,12 @@ export const Content = styled.div<{ $isPreview: boolean }>`
}
`;

export const DetailLink = styled(Link)<{ $isPreview: boolean }>`
export const DetailLink = styled.button<{ $isPreview: boolean }>`
display: flex;
flex-direction: column;
gap: 10px;
cursor: ${({ $isPreview }) => $isPreview && 'pointer'};
`;

export const Image = styled.img`
Expand Down
6 changes: 1 addition & 5 deletions frontend/src/hooks/useText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,5 @@ export const useText = (originalText: string) => {
setText('');
};

const addText = (newTextToAdd: string) => {
setText(text + newTextToAdd);
};

return { text, setText, handleTextChange, resetText, addText };
return { text, setText, handleTextChange, resetText };
};
12 changes: 10 additions & 2 deletions frontend/src/utils/post/convertTextToElement.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { MouseEvent } from 'react';

import { convertTextToUrl } from './convertTextToUrl';

export const convertTextToElement = (text: string) => {
const convertedUrlText = convertTextToUrl(text);
const linkPattern = /\[\[([^[\]]+)\]\]/g;

const parts = text.split(linkPattern);
const parts = convertedUrlText.split(linkPattern);

const elementList = parts.map((part, index) => {
if (index % 2 === 1) {
Expand All @@ -10,6 +15,9 @@ export const convertTextToElement = (text: string) => {
const linkUrl = linkText.startsWith('http' || 'https') ? linkText : `https://${linkText}`;
return (
<a
onClick={(event: MouseEvent<HTMLAnchorElement>) => {
event.stopPropagation();
}}
key={index}
href={linkUrl}
target="_blank"
Expand All @@ -22,7 +30,7 @@ export const convertTextToElement = (text: string) => {
}

// 링크가 아닌 문자열
return <span>{part}</span>;
return <span key={index}>{part}</span>;
});

return elementList;
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/utils/post/convertTextToUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* https://abc.co.kr/@abc/4
* https://votogether.com/
* http://localhost:3000/posts/100035
* http://votogether.com/
* (?<!\[\[) 는 앞에 [[로 시작하는 지 여부를 확인한다
* https?:\/\/는 http:// 혹은 https:// 로 시작하는 지 여부를 확인한다.
* (?!\]\]) 는 뒤에 ]]로 끝나는 지 여부를 확인한다.
* [^\s] 는 공백이 아닌 문자인지 여부를 확인한다.
*/
const httpsOrHttpRegex = /(?<!\[\[)(https?:\/\/[^\s]+)(?!\]\])/g;

/**
* www.naver.com
* www.tistory.com
* (?<!\[\[) 는 앞에 [[로 시작하는 지 여부를 확인한다
* (?<!\/)는 앞에 /로 시작하는 지 여부를 확인한다. https://www 에서 www 앞에 /가 있기에 중복되어 확인하는 것을 방지하기 위함
* \b(w{3})\b 는 www로 시작하는 지 여부를 정확히 확인한다. w가 4개인 경우 판별하지 않음
* [^\s] 는 공백이 아닌 문자인지 여부를 확인한다.
* (?!\]\]) 는 뒤에 ]]로 끝나는 지 여부를 확인한다.
*/
const wwwRegex = /(?<!\[\[)(?<!\/)\b(w{3})\b[^\s]+(?!\]\])/g;

export const convertTextToUrl = (text: string) => {
const httpOrHttpsConvertedText = text.replace(httpsOrHttpRegex, url => `[[${url}]]`);
const wwwConvertedText = httpOrHttpsConvertedText.replace(wwwRegex, url => `[[${url}]]`);

return wwwConvertedText;
};

0 comments on commit 89089fb

Please sign in to comment.