๐Ÿ’ป ์ฝ”๋”ฉ ์ผ๊ธฐ : [์Šคํ”„๋ง ๊ฒŒ์‹œํŒ with React] '๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ' ํŽธ

ybkยท2024๋…„ 5์›” 27์ผ

spring

๋ชฉ๋ก ๋ณด๊ธฐ
43/55
post-thumbnail

๐Ÿ”” '๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ'์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž!


๐Ÿ’Ÿ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ


BoardController.java

@GetMapping("list")
public Map<String, Object> list(@RequestParam(defaultValue = "1") Integer page,
                                @RequestParam(value = "type", required = false) String type,
                                @RequestParam(defaultValue = "", value = "keyword", required = false) String keyword) {
    return service.list(page, type, keyword);
}
  • type, keyword๋Š” ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ „๋‹ฌ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” null์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. keyword ๊ฒฝ์šฐ์—๋Š” null์ด ์•„๋‹Œ ๋นˆ ๋ฌธ์ž์—ด๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

BoardService.java

public Map<String, Object> list(Integer page, String type, String keyword) {

    // ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ
   	~~~~
    Integer countAll = mapper.countAllWithSearch(searchType, keyword);

    // ๊ฒ€์ƒ‰์–ด
    return Map.of("pageInfo", pageInfo,
            "boardList", mapper.selectAllPaging(offset, type, keyword));
}
  • select, input์— ๋งž๊ฒŒ ์กฐํšŒ๋˜๋Š” ๊ธ€์˜ ์ตœ์ข… ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ์•Œ๊ธฐ ์œ„ํ•ด mapper์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  • mapper์—์„œ DB์— ๊ฐ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Select("""
        <script>
        SELECT b.id, 
               b.title,
               m.nick_name writer
        FROM board b JOIN member m ON b.member_id = m.id
           <trim prefix="WHERE" prefixOverrides="OR">
               <if test="searchType != null">
                   <bind name="pattern" value="'%' + keyword + '%'" />
                   <if test="searchType == 'all' || searchType == 'text'">
                       OR b.title LIKE #{pattern}
                       OR b.content LIKE #{pattern}
                   </if>
                   <if test="searchType == 'all' || searchType == 'nickName'">
                       OR m.nick_name LIKE #{pattern}
                   </if>
               </if>
           </trim>
        ORDER BY b.id DESC
        LIMIT #{offset}, 10
        </script>
        """)
List<Board> selectAllPaging(Integer offset, String searchType, String keyword);

@Select("""
        <script>
         SELECT COUNT(b.id)
         FROM board b JOIN member m ON b.member_id = m.id
            <trim prefix="WHERE" prefixOverrides="OR">
                <if test="searchType != null">
                    <bind name="pattern" value="'%' + keyword + '%'" />
                    <if test="searchType == 'all' || searchType == 'text'">
                        OR b.title LIKE #{pattern}
                        OR b.content LIKE #{pattern}
                    </if>
                    <if test="searchType == 'all' || searchType == 'nickName'">
                        OR m.nick_name LIKE #{pattern}
                    </if>
                </if>
            </trim>
         </script>
         """)
Integer countAllWithSearch(String searchType, String keyword);

1. selectAllPaging :

  • board ํ…Œ์ด๋ธ”๊ณผ member ํ…Œ์ด๋ธ”์„ ์กฐ์ธํ•˜์—ฌ ๊ฒŒ์‹œ๋ฌผ์˜ ID, ์ œ๋ชฉ, ์ž‘์„ฑ์ž ๋‹‰๋„ค์ž„์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  • <trim> :
    - prefix : WHERE ์ ˆ์„ ์กฐ๊ฑด๋ถ€๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    - prefixOverrides : ๋ถˆํ•„์š”ํ•œ OR์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
  • <if> : searchType์ด null์ด ์•„๋‹ ๋•Œ keyword๋ฅผ LIKE ๊ตฌ๋ฌธ์— ๋งž๊ฒŒ ๋ณ€ํ™˜ํ•˜๊ณ  searchType์ด all(์ „์ฒด)์ด๊ฑฐ๋‚˜ text(๊ธ€(์ œ๋ชฉ+๋‚ด์šฉ))์ด๋ฉด ์ œ๋ชฉ ๋˜๋Š” ๋‚ด์šฉ์—์„œ ํ‚ค์›Œ๋“œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. searchType์ด all(์ „์ฒด)์ด๊ฑฐ๋‚˜ nickName(๋‹‰๋„ค์ž„)์ด๋ฉด ๋‹‰๋„ค์ž„์—์„œ ํ‚ค์›Œ๋“œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

2. countAllWithSearch :

  • ๊ฒ€์ƒ‰ type๊ณผ keyword์— ๋”ฐ๋ผ ๊ฒ€์ƒ‰๋œ ๊ฒฐ๊ณผ์˜ ์ด์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

BoardList.jsx

export function BoardList() {
  const navigate = useNavigate();
  const [boardList, setBoardList] = useState([]);
  const [pageInfo, setPageInfo] = useState({});
  const [searchParams] = useSearchParams();
  const [searchType, setSearchType] = useState("all");
  const [searchKeyword, setSearchKeyword] = useState("");

  useEffect(() => {
    axios.get(`/api/board/list?${searchParams}`).then((res) => {
      setBoardList(res.data.boardList);
      setPageInfo(res.data.pageInfo);
    });

    setSearchType("all");
    setSearchKeyword("");

    const typeParam = searchParams.get("type");
    const keywordParam = searchParams.get("keyword");
    if (typeParam) {
      setSearchType(typeParam);
    }

    if (keywordParam) {
      setSearchKeyword(keywordParam);
    }
  }, [searchParams]); //์˜์กด์„ฑ(dependency)๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ณ€๊ฒฝ๋ ๋•Œ๋งˆ๋‹ค ํ•จ์ˆ˜๋ฅผ trigger๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.

  // ์ด ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ
  const pageNumbers = [];
  for (let i = pageInfo.leftPageNumber; i <= pageInfo.rightPageNumber; i++) {
    pageNumbers.push(i);
  }

  function handleSearchClick() {
    navigate(`/?type=${searchType}&keyword=${searchKeyword}`);
  }

  function handlePageButtonClick(pageNumber) {
    searchParams.set("page", pageNumber);
    navigate(`/?${searchParams}`);
  }

  return (
    <Box mt={"30px"}>
      <Box>๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก</Box>
      <Box>
        {boardList.length === 0 && <Center>์กฐํšŒ๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</Center>}
        {boardList.length > 0 && (
          <Table>
            <Thead>
              <Tr>
                <Th>NO</Th>
                <Th>์ œ๋ชฉ</Th>
                <Th>
                  <FontAwesomeIcon icon={faUserPen} />
                </Th>
              </Tr>
            </Thead>
            <Tbody>
              {boardList.map((board) => (
                <Tr
                  cursor={"pointer"}
                  _hover={{ bgColor: "gray.200" }}
                  onClick={() => navigate(`/board/${board.id}`)}
                  key={board.id}
                >
                  <Td>{board.id}</Td>
                  <Td>{board.title}</Td>
                  <Td>{board.writer}</Td>
                </Tr>
              ))}
            </Tbody>
          </Table>
        )}
      </Box>

      {/*๊ฒ€์ƒ‰*/}
      <Center>
        <Box mt={"30px"}>
          <Flex>
            <Box>
              <Select
                value={searchType}
                onChange={(e) => setSearchType(e.target.value)}
              >
                <option value={"all"}>์ „์ฒด</option>
                <option value={"text"}>๊ธ€(์ œ๋ชฉ+๋‚ด์šฉ)</option>
                <option value={"nickName"}> ์ž‘์„ฑ์ž</option>
              </Select>
            </Box>
            <Box>
              <Input
                value={searchKeyword}
                onChange={(e) => setSearchKeyword(e.target.value)}
                placeholder={"๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”."}
              />
            </Box>
            <Box>
              <Button onClick={handleSearchClick}>
                <FontAwesomeIcon icon={faMagnifyingGlass} />
              </Button>
            </Box>
          </Flex>
        </Box>
      </Center>

      {/*ํŽ˜์ด์ง€๋„ค์ด์…˜*/}
      <Center>
        <Box mt={"30px"}>
          {/*๋งŒ์•ฝ ์ด์ „ ๋ฒ„ํŠผ์ด ๋ณด์ด๋ฉด ์ฒ˜์Œ ๋ฒ„ํŠผ๋„ ๋ณด์ธ๋‹ค. */}
          {pageInfo.prevPageNumber && (
            <>
              <Button onClick={() => handlePageButtonClick(1)}>
                <FontAwesomeIcon icon={faAnglesLeft} />
              </Button>
              <Button
                onClick={() => handlePageButtonClick(pageInfo.prevPageNumber)}
              >
                <FontAwesomeIcon icon={faAngleLeft} />
              </Button>
            </>
          )}

          {/*ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ*/}
          {pageNumbers.map((pageNumber) => (
            <Button
              mr={"10px"}
              onClick={() => handlePageButtonClick(pageNumber)}
              key={pageNumber}
              colorScheme={
                pageNumber == pageInfo.currentPageNumber ? "teal" : "gray"
              }
            >
              {pageNumber}
            </Button>
          ))}

          {/*๋งŒ์•ฝ ๋‹ค์Œ ๋ฒ„ํŠผ์ด ๋ณด์ด๋ฉด ๋งจ๋ ๋ฒ„ํŠผ๋„ ๋ณด์ธ๋‹ค. */}
          {pageInfo.nextPageNumber && (
            <>
              <Button
                onClick={() => handlePageButtonClick(pageInfo.nextPageNumber)}
              >
                <FontAwesomeIcon icon={faAngleRight} />
              </Button>
              <Button
                onClick={() => handlePageButtonClick(pageInfo.lastPageNumber)}
              >
                <FontAwesomeIcon icon={faAnglesRight} />
              </Button>
            </>
          )}
        </Box>
      </Center>
    </Box>
  );
}
  • ์กฐํšŒ ๊ฒฐ๊ณผ ์—†์„ ๋•Œ ์•ˆ๋‚ด ๋ฉ”์„ธ์ง€ : boardList์˜ ๊ธธ์ด๊ฐ€ 0๋ณด๋‹ค ํฌ๋ฉด ํ…Œ์ด๋ธ”์— ์กฐํšŒ๋œ ๋ชฉ๋ก์ด ๋‚˜์˜ค๊ณ  0 ์ด๋ฉด "์กฐํšŒ๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." ์•ˆ๋‚ด ๋ฉ”์„ธ์ง€๊ฐ€ ๋‚˜์˜ต๋‹ˆ๋‹ค.

  • ๊ฒ€์ƒ‰ ํ›„ ํŽ˜์ด์ง€ ์ด๋™ : handlePageButtonClick(pageNumber) : searchParams ๊ฐ์ฒด์— ์žˆ๋Š” page, type, keyword์˜ ๊ฐ’์ด ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์œผ๋กœ ํฌํ•จ๋˜๊ธฐ ๋•Œ๋ฌธ์— pageNumber์— ๋”ฐ๋ผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

  • ์ฃผ์†Œ ๋ณต์‚ฌ ํ›„ ์ƒˆ ํƒญ์— ์ด๋™ ์‹œ input์— ๋‚จ๊ธฐ๊ธฐ๊ณ  home ํด๋ฆญ ์‹œ ๊ฒ€์ƒ‰ tyep๊ณผ keyword๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.

const [searchType, setSearchType] = useState("all");
const [searchKeyword, setSearchKeyword] = useState("");

useEffect(() => {
    axios.get(`/api/board/list?${searchParams}`).then((res) => {
      setBoardList(res.data.boardList);
      setPageInfo(res.data.pageInfo);
    });
	
    setSearchType("all"); //์ดˆ๊ธฐํ™”
    setSearchKeyword(""); //์ดˆ๊ธฐํ™”

    const typeParam = searchParams.get("type");
    const keywordParam = searchParams.get("keyword");
    if (typeParam) {
      setSearchType(typeParam);
    }

    if (keywordParam) {
      setSearchKeyword(keywordParam);
    }
  }, [searchParams]);


<Select
                value={searchType}
                onChange={(e) => setSearchType(e.target.value)}
              >
                <option value={"all"}>์ „์ฒด</option>
                <option value={"text"}>๊ธ€(์ œ๋ชฉ+๋‚ด์šฉ)</option>
                <option value={"nickName"}> ์ž‘์„ฑ์ž</option>
              </Select>
            </Box>
            <Box>
              <Input
                value={searchKeyword}
                onChange={(e) => setSearchKeyword(e.target.value)}
                placeholder={"๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”."}
              />
            </Box>
  • searchParams๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ type๊ณผ keyword ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ฝ๊ณ , ์ด๋ฅผ ๊ฐ๊ฐ setSearchType๊ณผ setSearchKeyword ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • value ์†์„ฑ์€ searchType ์ƒํƒœ์™€ searchKeyword ์ƒํƒœ๋กœ ์„ค์ • ๋˜์–ด ์žˆ์–ด ์‚ฌ์šฉ์ž ๋ณ€๊ฒฝํ•˜๋ฉด ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๋‹ค.
profile
๊ฐœ๋ฐœ์ž ์ค€๋น„์ƒ~

0๊ฐœ์˜ ๋Œ“๊ธ€