What to do?
포스팅 검색기능 만들기
제목, 본문, 해쉬태그, 글쓴이 검색기능 구현하기
검색유형 정의
- 검색유형
- NONE : 검색사용 안함 → 전체 포스팅 조회
- TITLE: 제목검색
- HASHTAG : 단일 해쉬태그 (Exact Match)
- CONTENT : 본문 검색
- NICKNAME : 닉네임 (Exact Match)
SearchType.java
@Getter
@AllArgsConstructor
public enum SearchType {
NONE("검색사용 안함"),
TITLE("포스팅 제목"),
HASHTAG("단일 해쉬태그"),
CONTENT("포스팅 본문"),
NICKNAME("포스팅 작성자 닉네임");
private String description;
}
Repository
- findAll : 검색유형 NONE
- findAllByTitleContaining : 검색유형 TITLE
- findAllByHashtag : 검색유형 HASHTAG
- findAllByContentContaining : 검색유형 CONTENT
postRepository.java
@Repository
public interface PostRepository extends JpaRepository<PostEntity, Long> {
Page<PostEntity> findAll(Pageable pageable);
Page<PostEntity> findAllByUser(Pageable pageable, UserEntity userEntity);
Page<PostEntity> findAllByHashtags(Pageable pageable, String hashtag);
Page<PostEntity> findAllByTitleContaining(Pageable pageable, String title);
Page<PostEntity> findAllByContentContaining(Pageable pageable, String content);
}
Service
- 기존에 있던 포스팅을 조회하던 method들을 getPostBySearch라는 하나의 method로 만들었다. 훨씬 짧고 간단해졌다.
PostService.java
public class PostService {
...
/**
* 포스팅 검색
* @param pageable
* @param searchType : 검색타입 - none, title, hashtag, content, user
* @param searchValue : 검색어
* @return Page<PostDto>
*/
@Transactional(readOnly = true)
public Page<PostDto> getPostBySearch(Pageable pageable, SearchType searchType, String searchValue){
Page<PostEntity> searched = switch (searchType){
case NICKNAME -> postRepository.findAllByUser(pageable, findByNicknameOrElseThrow(searchValue));
case TITLE -> postRepository.findAllByTitleContaining(pageable, searchValue);
case HASHTAG -> postRepository.findAllByHashtags(pageable, searchValue);
case CONTENT -> postRepository.findAllByContentContaining(pageable, searchValue);
case NONE -> postRepository.findAll(pageable);
};
return searched.map(PostEntity::dto);
}
@Transactional(readOnly = true)
private UserEntity findByNicknameOrElseThrow(String nickname){
return userRepository.findByNickname(nickname).orElseThrow(()->{
throw CustomException.of(CustomErrorCode.USERNAME_NOT_FOUND);
});
}
...
}
Controller
- 포스팅 多건 조회시 api/v1/post 라는 경로로 GET요청을 쏘도록 함
- parameter는 SearchType(검색유형)과 SearchValue(검색어)를 받도록 함
PostController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1")
public class PostController {
...
/**
* 검색기능
* @param searchType : 검색타입 - none, title, hashtag, content, user
* @param searchValue : 검색어
* @param pageable
*/
@GetMapping("/post")
public CustomResponse<Page<GetPostResponse>> getPostBySearch(
@RequestParam("searchType") SearchType searchType,
@RequestParam("searchValue") String searchValue,
@PageableDefault(size = 20, sort = "createdAt",direction = Sort.Direction.DESC) Pageable pageable
){
return CustomResponse.success(postService.getPostBySearch(pageable, searchType, searchValue).map(GetPostResponse::from));
}
...
}
Front-End (React)
유저가 입력한 검색유형과 검색어가 변화될 때 마다 Get요청을 쏴서 페이지를 재렌더링 해야한다.
두가지 방법을 고민했다.
- 현재 Url 주소가 변경될 때마다 재런데링
- 검색유형, 검색어를 State에 저장하고, 변경될 때마다 재렌더링
구글링을 해보니 1번의 방법의 경우에 useLocation이나 useParams와 같은 hook을 사용해 처리해야 했다.
useState, useEffect, useNavigate 외에는 다른 hook들을 사용해본적이 없어서 생소했다.
그래도 구글링한 따라 테스트를 해봤는데, 생각치 못한 버그들이 나왔다.
그래서 2번 방법을 택했다.
ⓐ 유저가 입력한 검색유형과 검색어를 currentType, currentValue라는 state에 담기
ⓑ 검색버튼을 누르는 순간 searchType, searchValue라는 state를 업데이트
ⓒ useEffect를 사용해 searchType, searchValue가 변경되는 순간 Get요청을 보내 재렌더링하도록 하였다.
- PostList.js
import { Box, Container } from "@mui/system";
import axios from "axios";
import { useEffect, useState } from "react";
import { Link, useLocation} from "react-router-dom";
import { Button, FormControl, MenuItem, Pagination, Select, Tooltip, Typography } from "@mui/material";
import CreateIcon from '@mui/icons-material/Create';
import PostCard from './PostCard';
import { useRecoilState } from "recoil";
import { userState } from "../../recoil/user";
import Paper from '@mui/material/Paper';
import InputBase from '@mui/material/InputBase';
import SearchIcon from '@mui/icons-material/Search';
const PostList = ()=>{
/**
* States
* MAX_LENGTH_SEARCH_VALUE : 검색값의 최대길이
* searchType : 현재 페이지에 조회된 포스팅의 검색유형 (← 검색요청을 하는 순간 변경)
* currentType : 검색유형
* searchValue : 현재 페이지에 조회된 포스팅의 검색유형 (← 검색요청을 하는 순간 변경)
* currentValue : 검색값
* totalPage : 전체 페이지수
* isLoading : 포스팅을 가져오는지 여부
* user : 현재 로그인한 유저(Global State)
*/
const MAX_LENGTH_SEARCH_VALUE = 30;
const [searchType, setSearchType] = useState("NONE");
const [currentType, setCurrentType] = useState("NONE");
const [searchValue, setSearchValue] = useState("");
const [currentValue, setCurrentValue] = useState("");
const [currentPage, setCurrentPage] = useState(0);
const [totalPage, setTotalPage] = useState(0);
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [user, setUser] = useRecoilState(userState);
// ------- hooks ------- //
useEffect(()=>{
setIsLoading(true);
// 서버요청
const endPoint = `/api/v1/post?page=${currentPage}&searchType=${searchType??'NONE'}&searchValue=${searchValue??'NONE'}`;
axios.get(endPoint, {
headers:{
Authorization:user.token??localStorage.getItem("token")
}
}).then((res)=>{
return res.data.result
}).then((res)=>{
setCurrentPage(res.pageable.pageNumber); // 현재 페이지수
setTotalPage(res.totalPages); // 전체 페이지수
setPosts([...res.content]); // 포스팅 정보
})
.catch((err)=>{
console.log(err);
})
.finally(()=>{
setIsLoading(false);
})
}, [currentPage, searchType, searchValue]);
// ------- handler ------- //
const handlePage = (e) => {
setCurrentPage((parseInt(e.target.outerText)??1)-1);
}
const handleCurrentType = (e) =>{
setCurrentType(e.target.value);
}
const handleCurrentValue = (e) => {
let input = e.target.value;
if (currentType === "HASHTAG") input=input.replace("#", "");
input = input.slice(0, MAX_LENGTH_SEARCH_VALUE);
setCurrentValue(input);
}
const handleSearch = (e) => {
setSearchType(currentType);
setSearchValue(currentValue);
}
return (
<>
<Container>
<Paper component="form" sx={{display: 'flex', width: '100%',alignItems: 'center', marginTop:'3vh', padding:'10px'}}>
{/* 검색유형 드롭박스 */}
<FormControl variant="standard" sx={{ m: 1, minWidth: 120 }}>
<Select value={currentType} onChange={handleCurrentType} label="searchType">
<MenuItem value={"NONE"}>NONE</MenuItem>
<MenuItem value={"TITLE"}>제목</MenuItem>
<MenuItem value={"NICKNAME"}>작성자</MenuItem>
<MenuItem value={"HASHTAG"}>해쉬태그</MenuItem>
<MenuItem value={"CONTENT"}>본문</MenuItem>
</Select>
</FormControl>
{/* 입력창 */}
<InputBase sx={{ ml: 1, flex: 1 }} placeholder="검색어를 입력하세요" onChange={handleCurrentValue} value={currentValue} disabled={currentType==="NONE"}/>
{/* 검색하기 */}
<Tooltip title={`검색어는 최대 ${MAX_LENGTH_SEARCH_VALUE}자`}>
<Button type="button" sx={{ p: '10px' }} variant="contained" disabled={isLoading} onClick={handleSearch}>
<SearchIcon sx={{marginRight:'5px'}}/><Typography>검색하기</Typography>
</Button>
</Tooltip>
{/* 포스팅 작성페이지로 */}
<Link to="/post/write">
<Button variant="contained" color="success" sx={{ p: '10px', marginLeft:'10px' }}>
<CreateIcon sx={{marginRight:'5px'}}/><Typography>포스팅 작성</Typography>
</Button>
</Link>
</Paper>
{/* helper Text */}
{
currentType === "HASHTAG"
?
<Box>
<Typography sx={{marginLeft:"1vw", marginTop:"1vh"}} color="text.secondary">
해쉬 태그 검색시에는 하나의 해쉬태그를 검색해주세요
</Typography>
</Box>
:null
}
{/* 포스팅 */}
<Paper>
{
posts.map((p, i)=>{
return (
<Box sx={{marginTop:'5vh'}} key={i}>
<PostCard post={p}/>
</Box>
)
})
}
</Paper>
{/* 페이지 */}
<Box sx={{justifyContent:"center", display:"flex", marginTop:"5vh"}}>
<Pagination count={totalPage} defaultPage={1} boundaryCount={10} color="primary" size="large" sx={{margin: '2vh'}} onChange={handlePage}/>
</Box>
</Container>
</>
)
}
export default PostList;
'Java > Spring' 카테고리의 다른 글
[Spring] 간단한 SNS 만들기 #9 (0) | 2023.01.22 |
---|---|
[Spring] 간단한 SNS 만들기 #8 (0) | 2023.01.21 |
[Spring] 간단한 SNS 만들기 #6 (0) | 2023.01.15 |
[Spring] 간단한 SNS 만들기 #5 (0) | 2023.01.15 |
[Spring] 간단한 SNS 만들기 #4 (3) | 2023.01.01 |