Spring Boot 에서 검색 기능 구현

heeo·2023년 8월 30일
0

Team Project

목록 보기
3/3

이번 SpringBoot 프로젝트의 주요 기능인 검색 기능 구현을 시작했다.
DB item 테이블에서 item_name 키워드와 동일한 검색 키워드가 들어오면,
검색 완료 페이지에서 item-info, recyclable 등 DB에 저장된 정보를 반환한다.
만약 DB에 없는 키워드를 검색하면 검색 실패 페이지로 이동시킨다.


1. entity 작성

현재 item 관련된 데이터베이스는 이렇게 구성되어있다.

아이템 DB

테이블명컬럼명설명비고
category(대분류)category_id카테고리 PKA: 종이, B: 종이팩, ...
name카테고리 이름분리배출 가이드라인 pdf p.27 참조
item(소분류)item_id아이템 PKA숫자: 종이류, B숫자: 종이팩, ...
item_name아이템 이름
recycle_info버리는 방법
recyclable재활용 가능여부boolean 타입으로 Y/N
category_id카테고리 FK
item_similarItem_similar_pk아이템 유사 검색어 PK
similar_search_terms유사검색어nullable
item_id아이템FK
item_imageimage_id이미지 PKauto increment 사용하여 1씩 증가
image_path이미지 경로
image_name이미지명item 테이블 item_id 컬럼 + UUID
item_id아이템 FKItem join을 위한 컬럼

여기서 item_name 을 검색 키워드로 두고,
DB에 있다면 다른 정보들 반환
없다면 검색 실패 페이지로 이동


< Category Entity 작성 >

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Builder
@Accessors(chain = true)
public class Category {

    @Id
    @Enumerated(EnumType.STRING)
    private CategoryId category_id;

    private String name;

    // Category 1 : N Item
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "category")
    private List<Item> itemList;
}

< Item Entity 작성 >

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity  // JPA를 사용하여 테이블과 매핑할 클래스임을 나타냅니다.
@EntityListeners(AuditingEntityListener.class)
@Builder
@Accessors(chain = true)
public class Item {

    @Id
    private String item_id;

    @Column(name = "item_name")
    private String itemName; // 변수 이름 수정

    private String recycle_info;

    private Boolean recyclable;

    @ManyToOne
    @JoinColumn(name = "category_id")
    @Enumerated(EnumType.STRING)
    private Category category;

}


2. CategoryId 작성 (enum Type)

public enum CategoryId {

    A("A", "가전제품"),
    B("B", "고철"),
    C("C", "금속캔"),
    D("D", "대형폐기물"),
    E("E", "불연성종량제"),
    F("F", "비닐"),
    G("G", "유리병"),
    H("H", "음식물"),
    I("I", "의류"),
    J("J", "재질별분리"),
    K("K", "전문시설"),
    L("L", "전용함"),
    M("M", "종량제봉투"),
    N("N", "종이"),
    O("O", "종이팩"),
    P("P", "주의"),
    Q("Q", "플라스틱");

    private String category_id;
    private String name;

    CategoryId(String category_id, String name) {
        this.category_id = category_id;
        this.name = name;
    }

    public String getCategory_id() {
        return category_id;
    }

    public String getName() {
        return name;
    }
}


3. Repository 작성

< CategoryRepository >

@Repository
public interface CategoryRepository extends JpaRepository<Category, String> {
//    Category getOne(CategoryId category_id);
}

< ItemRepository >

@Repository  //엔티티, PK로 지정한 컬럼의 데이터 타입
public interface ItemRepository extends JpaRepository<Item, String> {
    // ItemName을 기준으로 해당 키워드가 포함된 것을 찾겠다.

    Item findByItemNameContaining(String itemName);
}


4. Controller 작성

// 검색 페이지 제공용 컨트롤러
@Controller
public class SearchController {

    @Autowired
    private ItemRepository itemRepository;

    @GetMapping("/search")
    public String searchItem(@RequestParam("itemName") String itemName, Model model) {
        Item foundItem = itemRepository.findByItemNameContaining(itemName);

        if (foundItem == null) {
            return "search_fail";  // HTML 템플릿 이름
        }

        model.addAttribute("item", foundItem);
        return "search_success";  // HTML 템플릿 이름
    }
    
    @GetMapping("/게시판")
    public String 게시판() {
        return "게시판";
    }

}


5. SearchService 작성

@Service
public class SearchService {

    private final CategoryRepository categoryRepository;
    private final ItemRepository itemRepository;

    @Autowired
    public SearchService(CategoryRepository categoryRepository, ItemRepository itemRepository) {
        this.categoryRepository = categoryRepository;
        this.itemRepository = itemRepository;
    }

    // 아이템 조회
    public Item getItem(String item_id) {
        Optional<Item> item = itemRepository.findById(item_id);
        if (item.isPresent()) {
            return item.get();
        } else {
            throw new RuntimeException();
        }
    }

}


6. HTML 작성

< index.html : 검색창이 있는 메인 페이지 >

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="">
        <meta name="author" content="">

        <title>REUSE</title>

        <!-- CSS FILES -->        
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600;700&family=Open+Sans&display=swap" rel="stylesheet">
        <link href="/static2/css/bootstrap.min.css" rel="stylesheet">
        <link href="/static2/css/bootstrap-icons.css" rel="stylesheet">
        <link href="/static2/css/templatemo-topic-listing.css" rel="stylesheet">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
    </head>
    
    <body id="top">
        <main>

            <nav class="navbar navbar-expand-lg">
                <div class="container">
                    <a class="navbar-brand" th:href="@{/}">
                        <img src="/static2/images/logo.png" class="logo" alt="">
                        <span>Reuse</span>
                    </a>

                    <div class="d-lg-none ms-auto me-4">
                        <a href="#top" class="navbar-icon bi-person smoothscroll"></a>
                    </div>
    
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                        <span class="navbar-toggler-icon"></span>
                    </button>
    
                    <div class="collapse navbar-collapse" id="navbarNav">
                        <ul class="navbar-nav ms-lg-5 me-lg-auto">
                            <li class="nav-item">
                                <a class="nav-link click-scroll" th:href="@{/}">Home</a>
                            </li>

                            <li class="nav-item">
                                <a class="nav-link click-scroll" th:href="@{/게시판}">Community</a>
                            </li>

                            <!-- 회원가입 -->
                            <li class="nav-item">
                                <a class="nav-link click-scroll" sec:authorize="isAnonymous()" th:href="@{/user/join}">회원가입</a>
                            </li>

                            <!-- 로그인한 회원만 볼수있는 메뉴 -->
                            <li class="nav-item">
                                <a class="nav-link click-scroll" sec:authorize="hasRole('ROLE_USER')" th:href="@{/}">마이페이지</a>
                            </li>

                            <!-- 로그인, 로그아웃-->
                            <li class="nav-item">
                                <a class="nav-link click-scroll" sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link click-scroll" sec:authorize="isAuthenticated()" th:href="@{/user/logout}">로그아웃</a>
                            </li>

                        </ul>

                        <div class="d-none d-lg-block">
                            <a href="#top" class="navbar-icon bi-person smoothscroll"></a>
                        </div>
                    </div>
                </div>
            </nav>
            

            <section class="hero-section d-flex justify-content-center align-items-center" id="section_1">
                <div class="container">
                    <div class="row">

                        <div class="col-lg-8 col-12 mx-auto">
                            <h1 class="text-white text-center">Correct, Recycle, Reuse</h1>

                            <h6 class="text-center">Recycling Discharge Methods</h6>

                            <form method="get" class="custom-form mt-4 pt-2 mb-lg-0 mb-5" role="search" action="/search">
                                <div class="input-group input-group-lg">
                                    <span class="input-group-text bi-search" id="basic-addon1">
                                        
                                    </span>

                                    <input name="itemName" type="search" class="form-control" id="keyword" placeholder="예) 가습기, 보온병, 액자.." aria-label="Search">

                                    <button type="submit" class="form-control">Search</button>
                                </div>
                            </form>
                        </div>

                    </div>
                </div>
            </section>


            <section class="featured-section">
                <div class="container">
                    <div class="row justify-content-center">

                        <div class="col-lg-4 col-12 mb-4 mb-lg-0">
                            <div class="custom-block bg-white shadow-lg">
                                <a href="topics-detail.html">
                                    <div class="d-flex">
                                        <div>
                                            <h5 class="mb-2">Update Trash</h5>
                                        </div>
                                    </div>

                                    <img src="/static2/images/topics/M095.png" class="custom-block-image img-fluid" alt="">
                                </a>
                            </div>
                        </div>

                        <div class="col-lg-6 col-12">
                            <div class="custom-block custom-block-overlay">
                                <div class="d-flex flex-column h-100">
                                    <img src="/static2/images/topics/M098.png" class="custom-block-image img-fluid" alt="">

                                    <div class="custom-block-overlay-text d-flex">
                                        <div>
                                            <h5 class="text-white mb-2">Popular Trash</h5>
                                            <a href="/static/static2/topics-detail.html" class="btn custom-btn mt-2 mt-lg-3">Learn More</a>
                                        </div>
                                    </div>

                                    <div class="section-overlay"></div>
                                </div>
                            </div>
                        </div>

                    </div>
                </div>
            </section>
        </main>

<footer class="site-footer section-padding">
            <div class="container">
                <div class="row">

                    <div class="col-lg-3 col-12 mb-4 pb-2">
                        <a class="navbar-brand mb-2" href="index.html">
                            <img src="/static2/images/logo.png" class="logo" alt="">
                            <span>Reuse</span>
                        </a>
                    </div>

                    <div class="col-lg-3 col-md-4 col-6">
                        <h6 class="site-footer-title mb-3">Resources</h6>

                        <ul class="site-footer-links">
                            <li class="site-footer-link-item">
                                <a href="#" class="site-footer-link">Home</a>
                            </li>

                            <li class="site-footer-link-item">
                                <a href="#" class="site-footer-link">Community</a>
                            </li>

                            <li class="site-footer-link-item">
                                <a href="#" class="site-footer-link">SIGN IN</a>
                            </li>

                            <li class="site-footer-link-item">
                                <a href="#" class="site-footer-link">Contact</a>
                            </li>
                        </ul>
                    </div>

                    <div class="col-lg-3 col-md-4 col-6 mb-4 mb-lg-0">
                        <h6 class="site-footer-title mb-3">Information</h6>

                        <p class="text-white d-flex mb-1">
                            <a href="tel: 305-240-9671" class="site-footer-link">
                                305-240-9671
                            </a>
                        </p>

                        <p class="text-white d-flex">
                            <a href="mailto:reuse@company.com" class="site-footer-link">
                                reuse@company.com
                            </a>
                        </p>
                    </div>

                    <div class="col-lg-3 col-md-4 col-12 mt-4 mt-lg-0 ms-auto">
                        <div class="dropdown">
                            <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                            English</button>

                            <ul class="dropdown-menu">
                                <li><button class="dropdown-item" type="button">Thai</button></li>

                                <li><button class="dropdown-item" type="button">Myanmar</button></li>

                                <li><button class="dropdown-item" type="button">Arabic</button></li>
                            </ul>
                        </div>

                        <p class="copyright-text mt-lg-5 mt-4">Copyright © 2048 Topic Listing Center. All rights reserved.
                        <br><br>Design: <a rel="nofollow" href="https://templatemo.com" target="_blank">TemplateMo</a> Distribution <a href="https://themewagon.com">ThemeWagon</a></p>
                        
                    </div>

                </div>
            </div>
        </footer>

        <!-- JAVASCRIPT FILES -->
        <script src="/static2/js/jquery.min.js"></script>
        <script src="/static2/js/bootstrap.bundle.min.js"></script>
        <script src="/static2/js/jquery.sticky.js"></script>
        <script src="/static2/js/click-scroll.js"></script>
        <script src="/static2/js/custom.js"></script>

    </body>
</html>

< search_success.html : 검색 성공 페이지 >

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Item Detail</title>
</head>
<body>
<h1>Item Detail</h1>
<p><strong>Item Name:</strong> <span th:text="${item.itemName}"></span></p>
<p><strong>Recycle Info:</strong> <span th:text="${item.recycle_info}"></span></p>
<p><strong>Recyclable:</strong> <span th:text="${item.recyclable}"></span></p>
<p><strong>Category:</strong> <span th:text="${item.category.name}"></span></p>
</body>
</html>

< search_fail.html : 검색 실패 페이지 >

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>REUSE</title>

    <!-- CSS FILES -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600;700&family=Open+Sans&display=swap" rel="stylesheet">
    <link href="/static2/css/bootstrap.min.css" rel="stylesheet">
    <link href="/static2/css/bootstrap-icons.css" rel="stylesheet">
    <link href="/static2/css/templatemo-topic-listing.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
</head>

<body id="top">
<main>

<!-- 네비바 시작 -->
    <nav class="navbar navbar-expand-lg">
        <div class="container">
            <a class="navbar-brand" th:href="@{/}">
                <img src="/static2/images/logo.png" class="logo" alt="">
                <span>Reuse</span>
            </a>

            <div class="d-lg-none ms-auto me-4">
                <a href="#top" class="navbar-icon bi-person smoothscroll"></a>
            </div>

            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ms-lg-5 me-lg-auto">

                    <li class="nav-item">
                        <a class="nav-link click-scroll" th:href="@{/}">Home</a>
                    </li>

                    <li class="nav-item">
                        <a class="nav-link click-scroll" th:href="@{/게시판}">Community</a>
                    </li>

                    <!-- 회원가입 -->
                    <li class="nav-item">
                        <a class="nav-link click-scroll" sec:authorize="isAnonymous()" th:href="@{/user/join}">회원가입</a>
                    </li>

                    <!-- 로그인한 회원만 볼수있는 메뉴 -->
                    <li class="nav-item">
                        <a class="nav-link click-scroll" sec:authorize="hasRole('ROLE_USER')" th:href="@{/}">마이페이지</a>
                    </li>

                    <!-- 로그인, 로그아웃-->
                    <li class="nav-item">
                        <a class="nav-link click-scroll" sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link click-scroll" sec:authorize="isAuthenticated()" th:href="@{/user/logout}">로그아웃</a>
                    </li>

                </ul>

                <div class="d-none d-lg-block">
                    <a href="#top" class="navbar-icon bi-person smoothscroll"></a>
                </div>
            </div>
        </div>
    </nav>
<!-- 네비바 끝 -->

    <section class="search-fail-top d-flex justify-content-center align-items-center" id="section_1">
        <div class="container">

            <img src="/static2/images/exclamation-mark.png" class="exclamation-mark-img" alt="">

            <h4 class="text-white text-center search-fail-text-1">검색 결과가 없습니다.</h4>

                <div class="search-fail-btn-aline">
                    <a th:href="@{/}" class="btn search-fail-btn">돌아가기</a>
                </div>
                <div class="search-fail-btn-aline">
                    <a th:href="@{/게시판}" class="btn search-fail-btn">재활용 추가 신청</a>
                </div>
        </div>
    </section>

    <section class="featured-section">
        <div class="container">
            <div class="row">
                <h6 class="search-fail-text-2">혹시 이런건 어떠세요?</h6>
                    <div class="col-lg-8">
                        <a th:href="@{/게시판}" class="btn search-fail-text-btn">우산</a>
                        <a th:href="@{/게시판}" class="btn search-fail-text-btn">그릇</a>
                        <a th:href="@{/게시판}" class="btn search-fail-text-btn">보조배터리</a>
                    </div>
            </div>
        </div>
    </section>

</main>

<!-- 푸터 시작 -->
<footer class="site-footer section-padding">
    <div class="container">
        <div class="row">

            <div class="col-lg-3 col-12 mb-4 pb-2">
                <a class="navbar-brand mb-2" href="index.html">
                    <img src="/static2/images/logo.png" class="logo" alt="">
                    <span>Reuse</span>
                </a>
            </div>

            <div class="col-lg-3 col-md-4 col-6">
                <h6 class="site-footer-title mb-3">Resources</h6>

                <ul class="site-footer-links">
                    <li class="site-footer-link-item">
                        <a href="#" class="site-footer-link">Home</a>
                    </li>

                    <li class="site-footer-link-item">
                        <a href="#" class="site-footer-link">Community</a>
                    </li>

                    <li class="site-footer-link-item">
                        <a href="#" class="site-footer-link">SIGN IN</a>
                    </li>

                    <li class="site-footer-link-item">
                        <a href="#" class="site-footer-link">Contact</a>
                    </li>
                </ul>
            </div>

            <div class="col-lg-3 col-md-4 col-6 mb-4 mb-lg-0">
                <h6 class="site-footer-title mb-3">Information</h6>

                <p class="text-white d-flex mb-1">
                    <a href="tel: 305-240-9671" class="site-footer-link">
                        305-240-9671
                    </a>
                </p>

                <p class="text-white d-flex">
                    <a href="mailto:reuse@company.com" class="site-footer-link">
                        reuse@company.com
                    </a>
                </p>
            </div>

            <div class="col-lg-3 col-md-4 col-12 mt-4 mt-lg-0 ms-auto">
                <div class="dropdown">
                    <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                        English</button>

                    <ul class="dropdown-menu">
                        <li><button class="dropdown-item" type="button">Thai</button></li>

                        <li><button class="dropdown-item" type="button">Myanmar</button></li>

                        <li><button class="dropdown-item" type="button">Arabic</button></li>
                    </ul>
                </div>

                <p class="copyright-text mt-lg-5 mt-4">Copyright © 2048 Topic Listing Center. All rights reserved.
                    <br><br>Design: <a rel="nofollow" href="https://templatemo.com" target="_blank">TemplateMo</a> Distribution <a href="https://themewagon.com">ThemeWagon</a></p>

            </div>

        </div>
    </div>
</footer>
<!-- 푸터 끝 -->

<!-- JAVASCRIPT FILES -->
<script src="/static2/js/jquery.min.js"></script>
<script src="/static2/js/bootstrap.bundle.min.js"></script>
<script src="/static2/js/jquery.sticky.js"></script>
<script src="/static2/js/click-scroll.js"></script>
<script src="/static2/js/custom.js"></script>

</body>
</html>


구현하면서 생겼던 문제점과 해결법

처음 build를 했을 때 검색 실패 로직은 정상 작동하는데, 검색 완료 페이지가 구현이 안됐다. 찾아보니 Item 엔티티 클래스의 변수 이름과 search_success.html 템플릿 파일에서 사용하고자 하는 변수 이름이 다른데, 이로 인해 Thymeleaf에서 변수를 찾을 수 없는 문제가 발생했다. 그래서 Item 엔티티 클래스의 변수명을 수정했다.
그리고 search_success.html 템플릿 파일에서 Thymeleaf 문법 사용 시 ${item.recycleInfo}와 같은 부분을 모두 ${item.recycle_info}로 수정했더니 검색 완료 페이지가 떴다.


그러나 검색 완료 페이지에서 DB에 저장된 item에 관한 정보들을 불러오지 못했다. 찾아보니 search_success.html 템플릿 파일에서의 th:if 문법을 사용하면 조건이 참일 때만 해당 요소가 표시되니까 이 경우, 조건을 만족하지 않을 때에는 요소 자체가 표시되지 않으므로 이로 인해 의도치 않은 오류가 발생할 수 있다고 한다. 조건을 만족하지 않더라도 요소가 표시되어야 한다면 th:if 대신 th:text를 사용하여 조건에 맞지 않을 때에는 빈 문자열을 출력하도록 처리해야 한다고 해서 코드를 수정했다.

profile
Hello, World!

0개의 댓글