본문 바로가기
스프링과 JPA 기반 웹 어플리케이션 개발/8부 검색 및 첫 페이지

78. 검색 기능 구현

by Backchus 2020. 5. 17.

GET "/search/study"

  • keyword 입력 받아서 스터디 검색
  • 스터디 제목, 태그 이름, 도시 로컬 이름에 해당하는 키워드를 가지고 있는 공개된 스터디 조회
  • 페이징 없이
  • 정렬 조건 없이
  • 로그인 없어도 사용 가능

보여줄 내용

  • 검색 키워드와 결과 개수, 없으면 없다고 표기
  • 스터디 당 보여줄 정보
    • 스터디 이름
    • 짧은 소개
    • 태그
    • 지역
    • 멤버 수
    • 스터디 공개 일시

keyword에 따른 Study를 불러오기위해 QueryDsl을 활용합니다.

먼저 StudyRepository를 확장하여 StudyRepositoryExtension 인터페이스에 keyword에따른 Study리스트를 불러오는 메소드를 생성합니다.

package me.weekbelt.studyolle.modules.study;

@Transactional(readOnly = true)
public interface StudyRepositoryExtension {

    List<Study> findByKeyword(String keyword);
}

 

StudyRepositoryExtension의 구현체인 StudyRepositoryExtensionImpl을 작성합니다.

package me.weekbelt.studyolle.modules.study;

public class StudyRepositoryExtensionImpl extends QuerydslRepositorySupport implements StudyRepositoryExtension{

    public StudyRepositoryExtensionImpl() {
        super(Study.class);
    }

    @Override
    public List<Study> findByKeyword(String keyword) {
        QStudy study = QStudy.study;
        JPQLQuery<Study> query = from(study).where(study.published.isTrue()
                .and(study.title.containsIgnoreCase(keyword))
                .or(study.tags.any().title.containsIgnoreCase(keyword))
                .or(study.zones.any().localNameOfCity.containsIgnoreCase(keyword)));
        return query.fetch();
    }
}

 

StudyRepository인터페이스에서 StudyRepositoryExtension을 상속받아 확장시킵니다.

package me.weekbelt.studyolle.modules.study;

@Transactional(readOnly = true)
public interface StudyRepository extends JpaRepository<Study, Long>, StudyRepositoryExtension {
    
    // ..........
    
}

 

MainController에서 홈 화면에 Study리스트와 검색을 처리해주는 핸들러를 작성합니다.

package me.weekbelt.studyolle.modules.main;

@RequiredArgsConstructor
@Controller
public class MainController {

    private final StudyRepository studyRepository;

    // ..........

    @GetMapping("/search/study")
    public String searchStudy(String keyword, Model model) {
        List<Study> studyList = studyRepository.findByKeyword(keyword);
        model.addAttribute("studyList", studyList);
        model.addAttribute("keyword", keyword);
        return "search";
    }
}

 

홈 화면에 Study리스트를 보여주는 search.html를 작성합니다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments.html :: head"></head>
<body class="bg-light">
<div th:replace="fragments.html :: main-nav"></div>
<div class="container">
    <div class="py-5 text-center">
        <div class="dropdown">
            <button class="btn btn-light dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                검색 결과 정렬 방식
            </button>
            <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
                <a class="dropdown-item" th:classappend="${#strings.equals(sortProperty, 'publishedDateTime')}? active"
                   th:href="@{'/search/study?sort=publishedDateTime,desc&keyword=' + ${keyword}}">
                    스터디 공개일
                </a>
                <a class="dropdown-item" th:classappend="${#strings.equals(sortProperty, 'memberCount')}? active"
                   th:href="@{'/search/study?sort=memberCount,desc&keyword=' + ${keyword}}">
                    멤버수
                </a>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-md-4" th:each="study: ${studyList}">
            <div class="card mb-4 shadow-sm">
                <img th:src="${study.image}" class="card-img-top" th:alt="${study.title}" >
                <div class="card-body">
                    <a th:href="@{'/study/' + ${study.path}}" class="text-decoration-none">
                        <h5 class="card-title context" th:text="${study.title}"></h5>
                    </a>
                    <p class="card-text" th:text="${study.shortDescription}">Short description</p>
                    <p class="card-text context">
                                <span th:each="tag: ${study.tags}" class="font-weight-light text-monospace badge badge-pill badge-info mr-3">
                                    <a th:href="@{'/search/tag/' + ${tag.title}}" class="text-decoration-none text-white">
                                        <i class="fa fa-tag"></i> <span th:text="${tag.title}">Tag</span>
                                    </a>
                                </span>
                        <span th:each="zone: ${study.zones}" class="font-weight-light text-monospace badge badge-primary mr-3">
                                    <a th:href="@{'/search/zone/' + ${zone.id}}" class="text-decoration-none text-white">
                                        <i class="fa fa-globe"></i> <span th:text="${zone.localNameOfCity}" class="text-white">City</span>
                                    </a>
                                </span>
                    </p>
                    <div class="d-flex justify-content-between align-items-center">
                        <small class="text-muted">
                            <i class="fa fa-user-circle"></i>
                            <span th:text="${study.memberCount}"></span>명
                        </small>
                        <small class="text-muted date" th:text="${study.publishedDateTime}">9 mins</small>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<div th:replace="fragments.html :: footer"></div>
<script th:replace="fragments.html :: date-time"></script>
<script src="/node_modules/mark.js/dist/jquery.mark.min.js"></script>
<script type="application/javascript">
    $(function(){
        var mark = function() {
            // Read the keyword
            var keyword = $("#keyword").text();

            // Determine selected options
            var options = {
                "each": function(element) {
                    setTimeout(function() {
                        $(element).addClass("animate");
                    }, 150);
                }
            };

            // Mark the keyword inside the context
            $(".context").unmark({
                done: function() {
                    $(".context").mark(keyword, options);
                }
            });
        };

        mark();
    });
</script>
</body>
</html>

 

SecurityConfig 설정에 들어가서 로그인하지 않은 사람들도 검색할 수 있게 처리한다.

package me.weekbelt.studyolle.infra.config;

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;
    private final DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/", "/login", "/sign-up", "/check-email-token", "/login-by-email",
                        "/email-login", "/check-email-login", "/login-link", "/search/study").permitAll()  // /search/study 추가
                .mvcMatchers(HttpMethod.GET, "/profile/*").permitAll()
                .anyRequest().authenticated();

        // ..........
       
    }

    // ..........
}

 

참고: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1#

 

스프링과 JPA 기반 웹 애플리케이션 개발 - 인프런

이 강좌에서 여러분은 실제로 운영 중인 서비스를 스프링, JPA 그리고 타임리프를 비롯한 여러 자바 기반의 여러 오픈 소스 기술을 사용하여 웹 애플리케이션을 개발하는 과정을 학습할 수 있습�

www.inflearn.com