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

검색에 관한 설정과 전체 게시글 초기 설정 변경 #547

Merged
merged 10 commits into from
Sep 12, 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
46 changes: 0 additions & 46 deletions frontend/__test__/getSelectedState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ describe('getSelectedState 사용했을 때 현재 유저에게 어떤 게시글
const state: SelectedState = {
postType: 'category',
categoryId,
keyword: '',
categoryList: MOCK_CATEGORY_LIST,
};

Expand All @@ -18,53 +17,10 @@ describe('getSelectedState 사용했을 때 현재 유저에게 어떤 게시글
expect(result).toBe(MOCK_CATEGORY_LIST[0].name);
});

test('현재 검색을 한 상태이면, 검색 키워드를 반환한다.', () => {
const keyword = '갤럭시';
const state: SelectedState = {
postType: 'search',
categoryId: 0,
keyword,
categoryList: MOCK_CATEGORY_LIST,
};

const result = getSelectedState(state);

expect(result).toBe(keyword);
});

test('검색어를 길게 설정한 경우 10자만 보여주고 ...으로 표시해서 보여준다.', () => {
const keyword = '아이폰갤럭시뉴진스아이브세븐틴슈퍼주니어임';
const state: SelectedState = {
postType: 'search',
categoryId: 0,
keyword,
categoryList: MOCK_CATEGORY_LIST,
};

const result = getSelectedState(state);

expect(result).toBe('아이폰갤럭시뉴진스아...');
});

test('검색어를 10글자인 경우 10글자 전부 표시해서 보여준다.', () => {
const keyword = '아이폰갤럭시뉴진스아';
const state: SelectedState = {
postType: 'search',
categoryId: 0,
keyword,
categoryList: MOCK_CATEGORY_LIST,
};

const result = getSelectedState(state);

expect(result).toBe('아이폰갤럭시뉴진스아');
});

test('현재 홈 화면에 있다면, "전체"를 반환한다', () => {
const state: SelectedState = {
postType: 'posts',
categoryId: 0,
keyword: '',
categoryList: MOCK_CATEGORY_LIST,
};

Expand All @@ -77,7 +33,6 @@ describe('getSelectedState 사용했을 때 현재 유저에게 어떤 게시글
const state: SelectedState = {
postType: 'myPost',
categoryId: 0,
keyword: '',
categoryList: MOCK_CATEGORY_LIST,
};

Expand All @@ -90,7 +45,6 @@ describe('getSelectedState 사용했을 때 현재 유저에게 어떤 게시글
const state: SelectedState = {
postType: 'myVote',
categoryId: 0,
keyword: '',
categoryList: MOCK_CATEGORY_LIST,
};

Expand Down
15 changes: 15 additions & 0 deletions frontend/__test__/getTrimmedWord.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getTrimmedWord } from '@utils/getTrimmedWord';

test.each([
['검색어 입니다', '검색어 입니다'],
[' 완전히 갤럭시 임 ', '완전히 갤럭시 임'],
[' ', ''],
['', ''],
])(
'getTrimmedWord 함수에서 단어를 입력했을 때 중복된 공백을 제거한 단어를 반환한다.',
(word, expectedWord) => {
const result = getTrimmedWord(word);

expect(result).toBe(expectedWord);
}
);
41 changes: 41 additions & 0 deletions frontend/__test__/hooks/useSearch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';

import { fireEvent, render, renderHook, screen } from '@testing-library/react';

import { useSearch } from '@hooks/useSearch';

describe('useSearch 훅이 검색을 하는지 확인한다.', () => {
test('초기 값이 없다면 keyword는 빈 문자열이다.', () => {
const { result } = renderHook(() => useSearch(), { wrapper: MemoryRouter });

const { keyword } = result.current;

expect(keyword).toBe('');
});

test('초기 값이 있다면 keyword 값에 설정된다.', () => {
const KEYWORD = '갤럭시';

const { result } = renderHook(() => useSearch(KEYWORD), { wrapper: MemoryRouter });

const { keyword } = result.current;

expect(keyword).toBe(KEYWORD);
});

test('onChange 이벤트를 한다면 keyword 값에 설정된다.', () => {
const KEYWORD = '갤럭시';

const { result } = renderHook(() => useSearch(KEYWORD), { wrapper: MemoryRouter });
const { keyword, handleKeywordChange } = result.current;

render(<input value={keyword} aria-label="search-input" onChange={handleKeywordChange} />);

const input = screen.getByLabelText('search-input');

fireEvent.change(input, { target: { value: KEYWORD } });

expect(result.current.keyword).toBe(KEYWORD);
});
});
2 changes: 1 addition & 1 deletion frontend/__test__/hooks/useSelect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { PostStatus } from '@components/post/PostListPage/types';
const INIT_SELECTED_OPTION = 'progress';
const CHANGE_SELECTED_OPTION = 'closed';

describe('usePostList 훅이 전체 게시글 목록을 불러오는지 확인한다', () => {
describe('useSelect 훅이 전체 게시글 목록을 불러오는지 확인한다', () => {
test('초기 값이 설정 되었는지 확인한다.', () => {
const { result } = renderHook(() => useSelect<PostStatus>(INIT_SELECTED_OPTION));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ export default function CategorySection() {
const categoryListFallback = categoryList ?? [];

const { postOptionalOption, postType } = usePostRequestInfo();
const { categoryId, keyword } = postOptionalOption;
const { categoryId } = postOptionalOption;

const selectedState = getSelectedState({
categoryId,
keyword,
categoryList: categoryListFallback,
postType,
});
Expand Down
16 changes: 14 additions & 2 deletions frontend/src/components/common/SearchBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { HTMLAttributes } from 'react';

import { Size } from '@type/style';

import { useCurrentKeyword } from '@hooks/useCurrentKeyword';
import { useSearch } from '@hooks/useSearch';

import { PATH } from '@constants/path';
import { SEARCH_KEYWORD } from '@constants/post';
import { SEARCH_KEYWORD, SEARCH_KEYWORD_MAX_LENGTH } from '@constants/post';

import searchIcon from '@assets/search_black.svg';

Expand All @@ -15,11 +18,20 @@ interface SearchBarProps extends HTMLAttributes<HTMLInputElement> {
}

export default function SearchBar({ size, isOpen, ...rest }: SearchBarProps) {
const { currentKeyword } = useCurrentKeyword();
const { keyword, handleKeywordChange, handleSearchSubmit, searchInputRef } =
useSearch(currentKeyword);

return (
<S.Form size={size} action={PATH.SEARCH}>
<S.Form size={size} action={PATH.SEARCH} onSubmit={handleSearchSubmit}>
<S.Input
ref={searchInputRef}
maxLength={SEARCH_KEYWORD_MAX_LENGTH + 1}
aria-label="게시글 제목 및 내용 검색창"
type="search"
value={keyword}
onChange={handleKeywordChange}
autoComplete="off"
Copy link
Collaborator

Choose a reason for hiding this comment

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

오오 새로운 속성이네요

name={SEARCH_KEYWORD}
{...rest}
/>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/context/postOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface PostOptionContextProps {
export default function PostOptionProvider({ children }: PropsWithChildren) {
const [postOption, setPostOption] = useState<PostOption>({
sorting: SORTING.LATEST,
status: STATUS.PROGRESS,
status: STATUS.ALL,
});
Copy link
Member

Choose a reason for hiding this comment

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

👍👍


return (
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/hooks/useCurrentKeyword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useSearchParams } from 'react-router-dom';

import { DEFAULT_KEYWORD, SEARCH_KEYWORD, SEARCH_KEYWORD_MAX_LENGTH } from '@constants/post';

import { getTrimmedWord } from '@utils/getTrimmedWord';

export const useCurrentKeyword = () => {
Copy link
Member

Choose a reason for hiding this comment

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

오 컴포넌트 뿐만 아니라 다른 hook에서도 쓰이는 hook이군요! 재사용성 굳입니다👍👍

const [searchParams] = useSearchParams();
const currentKeyword =
searchParams.get(SEARCH_KEYWORD)?.toString().slice(0, SEARCH_KEYWORD_MAX_LENGTH) ??
DEFAULT_KEYWORD;

return {
currentKeyword:
currentKeyword !== DEFAULT_KEYWORD ? getTrimmedWord(currentKeyword) : currentKeyword,
};
};
20 changes: 7 additions & 13 deletions frontend/src/hooks/usePostRequestInfo.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
import { useLocation, useParams } from 'react-router-dom';

import { PostRequestKind } from '@components/post/PostListPage/types';

import { PATH } from '@constants/path';
import {
DEFAULT_CATEGORY_ID,
DEFAULT_KEYWORD,
POST_TYPE,
SEARCH_KEYWORD,
SEARCH_KEYWORD_MAX_LENGTH,
} from '@constants/post';
import { DEFAULT_CATEGORY_ID, POST_TYPE } from '@constants/post';

import { getPathFragment } from '@utils/getPathFragment';

import { useCurrentKeyword } from './useCurrentKeyword';

const REQUEST_URL: Record<string, PostRequestKind> = {
[PATH.HOME]: POST_TYPE.ALL,
[PATH.POST_CATEGORY]: POST_TYPE.CATEGORY,
Expand All @@ -23,19 +19,17 @@ const REQUEST_URL: Record<string, PostRequestKind> = {

export const usePostRequestInfo = () => {
const params = useParams<{ categoryId?: string }>();
const [searchParams] = useSearchParams();
const { currentKeyword } = useCurrentKeyword();
const { pathname } = useLocation();

const categoryId = Number(params.categoryId ?? DEFAULT_CATEGORY_ID);
const keyword =
searchParams.get(SEARCH_KEYWORD)?.toString().slice(0, SEARCH_KEYWORD_MAX_LENGTH) ??
DEFAULT_KEYWORD;

const convertedPathname = getPathFragment(pathname);
const postType = REQUEST_URL[convertedPathname];

const postOptionalOption = {
categoryId,
keyword,
keyword: currentKeyword,
};

if (!postType) {
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/hooks/useSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ChangeEvent, FormEvent, useRef } from 'react';
import { useNavigate } from 'react-router-dom';

import { SEARCH_KEYWORD_MAX_LENGTH } from '@constants/post';

import { getTrimmedWord } from '@utils/getTrimmedWord';

import { useText } from './useText';

export const useSearch = (initialKeyword = '') => {
const navigate = useNavigate();
const searchInputRef = useRef<HTMLInputElement>(null);
const { text: keyword, setText: setKeyword, handleTextChange } = useText(initialKeyword);

const handleKeywordChange = (event: ChangeEvent<HTMLInputElement>) => {
if (!searchInputRef.current) return;

handleTextChange(event, { MAX_LENGTH: SEARCH_KEYWORD_MAX_LENGTH, MIN_LENGTH: 0 });
};

const handleSearchSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

if (!searchInputRef.current) return;

const trimmedKeyword = getTrimmedWord(keyword);

if (keyword !== trimmedKeyword) {
setKeyword(trimmedKeyword);
}

if (trimmedKeyword === '') {
searchInputRef.current.setCustomValidity('검색어를 입력해주세요');
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍👍👍

searchInputRef.current.reportValidity();
return;
}

navigate(`/search?keyword=${trimmedKeyword}`);
};

return { keyword, handleKeywordChange, handleSearchSubmit, searchInputRef };
};
2 changes: 1 addition & 1 deletion frontend/src/hooks/useText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ export const useText = (originalText: string) => {
setText('');
};

return { text, handleTextChange, resetText };
return { text, setText, handleTextChange, resetText };
};
7 changes: 7 additions & 0 deletions frontend/src/utils/getTrimmedWord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const getTrimmedWord = (word: string) => {
return word
.split(' ')
.map(word => word.trim())
.filter(word => word !== '')
.join(' ');
};
16 changes: 1 addition & 15 deletions frontend/src/utils/post/getSelectedState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,16 @@ import { PostRequestKind } from '@components/post/PostListPage/types';
export interface SelectedState {
postType: PostRequestKind;
categoryId: number;
keyword: string;
categoryList: Category[];
}

const SLICED_LENGTH_NUMBER = 10;

export const getSelectedState = ({
postType,
categoryId,
keyword,
categoryList,
}: SelectedState) => {
export const getSelectedState = ({ postType, categoryId, categoryList }: SelectedState) => {
if (postType === 'category') {
const selectedCategory = categoryList.find(category => category.id === categoryId);

return selectedCategory?.name ?? '전체';
}

if (postType === 'search') {
return keyword.length > SLICED_LENGTH_NUMBER
? `${keyword.slice(0, SLICED_LENGTH_NUMBER)}...`
: keyword;
}

if (postType === 'myPost') {
return '내가 작성한 글';
}
Expand Down