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 기반 웹 어플리케이션 개발 > 8부 검색 및 첫 페이지' 카테고리의 다른 글
83. 로그인 한 사용자를 위한 첫 화면 (0) | 2020.05.18 |
---|---|
82. 로그인 하지 않은 사용자를 위한 첫 화면 (0) | 2020.05.18 |
81. 페이징 뷰 개선 (0) | 2020.05.18 |
80. 페이징 적용 (0) | 2020.05.17 |
79. N+1 Select 문제 해결 (0) | 2020.05.17 |