Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

게시글 작성 시 본문 링크 삽입을 하지 않더라도 사용자에게 링크 클릭이 가능하도록 변경 #706

Merged
merged 8 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 isLinkEnabled가 false라면 모든 문자열은 < span >으로 감싸지게 되는데,

그렇다면 서두에 분기처리해서 early return 해버리는건 어떠세요?

export const convertTextToElement = (text: string, isLinkEnabled = true) => {
  const convertedUrlText = convertTextToUrl(text);

  if (!isLinkEnabled) return  <span >{convertedUrlText.join(""}</span> ;

  ...기존 코드
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수아가 좋은 제안을 해주셔서 아래와 같이 바꿔볼 수 있었어요 !!👍👍👍

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

  const parts = convertedUrlText.split(linkPattern);

  if (!isLinkEnabled) {
    return parts.join('');
  }


...

});

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] 는 공백이 아닌 문자인지 여부를 확인한다.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상세한 주석 정말 좋군요👍

const httpsOrHttpRegex = /(?<!\[\[)(https?:\/\/[^\s]+)(?!\]\])/g;

/**
* www.naver.com
* www.tistory.com
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 tistory.com 도 링크로 인식되는지 궁금해요!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

www.tistory.com
흠 깃허브도 tistory.com 은 링크로 인식이 안되네요 ..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러게요! 근데 www. 을 안붙히고 뒤에 .com이나 net을 인식하면 가능은 할 것 같아요

* (?<!\[\[) 는 앞에 [[로 시작하는 지 여부를 확인한다
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 [[ 로 시작하는지 판별하는 이유는 기존의 링크가 [[ 로 시작하기 때문인가요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 기존의 링크가 [[으로 시작하기 때문에 중복해서 [[[[ 기존링크 ]]]]와 같이 변환되지 않게 하기 위해서 [[로 시작한다면 매칭되지 않게 했습니다

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

세심하시네요

* (?<!\/)는 앞에 /로 시작하는 지 여부를 확인한다. 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}]]`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 되면
[[www.---]] 문자열은 변경되지 않고,
www 문자열은 [[www]]로 변경되는 것 맞나요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[[www.---]]와 같이 이미 감싸져 있는 문자열이라면 변경되지 않구요

[[ ]] 으로 감싸져 있지 않은 www라면 [[www.---]]으로 변경됩니당


return wwwConvertedText;
};
Loading