이 화면을 N+1 Select 문제 없이, 쿼리 6개를 사용해서 만드세요.
- 계정 조회(관심 주제, 지역 정보 포함)
- 참석할 모임 조회
- 나의 주요 활동 지역과 관심 주제에 해당하는 스터디 조회 (Tag와 Zone은 && 조건입니다.)
- 관리중이 스터디 조회
- 참여중인 스터디 조회
- 알림 조회
뷰에 전달해야 할 모델 데이터
- account: 현재 로그인한 사용자의 정보
- enrollmentList: 참석 확정된 참가 신청 정보를 통해 '참석할 모임' 목록 출력
- studyList: 주요 활동 지역의 관심 주제 스터디 리스트 6개까지만(스터디 공개시간 역순)
- studyManagerOf: 지금 접속한 계정으로 관리중인 스터디 5개 까지(스터디 공개시간 역순)
- studyMemberOf: 지금 접속한 계정으로 참여중인 스터디 5개 까지(스터디 공개시간 역순)
관심 주제와 지역정보를 포함하는 계정을 조회하는 쿼리메소드 작성
package me.weekbelt.studyolle.modules.account;
@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long>, QuerydslPredicateExecutor<Account> {
// .........
@EntityGraph(attributePaths = {"tags", "zones"})
Account findAccountWithTagsAndZonesById(Long id);
}
참석 모임을 조회하는 쿼리 메소드작성
package me.weekbelt.studyolle.modules.event;
public interface EnrollmentRepository extends JpaRepository<Enrollment, Long> {
// ........
@EntityGraph("Enrollment.withEventAndStudy")
List<Enrollment> findByAccountAndAcceptedOrderByEnrolledAtDesc(Account account, boolean accepted);
}
package me.weekbelt.studyolle.modules.event;
// 추가
@NamedEntityGraph(
name = "Enrollment.withEventAndStudy",
attributeNodes = {
@NamedAttributeNode(value = "event", subgraph = "study")
},
subgraphs = @NamedSubgraph(name = "study", attributeNodes = @NamedAttributeNode("study"))
)
@Getter @Setter @EqualsAndHashCode(of = "id")
@Entity
public class Enrollment {
// ...............
}
주요 관심사와 활동지역에 따른 스터디를 조회하는 쿼리메소드 작성
package me.weekbelt.studyolle.modules.study;
@Transactional(readOnly = true)
public interface StudyRepositoryExtension {
// ........
List<Study> findByAccount(Set<Tag> tags, Set<Zone> zones);
}
package me.weekbelt.studyolle.modules.study;
public class StudyRepositoryExtensionImpl extends QuerydslRepositorySupport implements StudyRepositoryExtension {
// ..........
@Override
public List<Study> findByAccount(Set<Tag> tags, Set<Zone> zones) {
QStudy study = QStudy.study;
JPQLQuery<Study> query = from(study)
.where(study.published.isTrue()
.and(study.closed.isFalse())
.and(study.tags.any().in(tags))
.and(study.zones.any().in(zones)))
.leftJoin(study.tags, QTag.tag).fetchJoin()
.leftJoin(study.zones, QZone.zone).fetchJoin()
.orderBy(study.publishedDateTime.desc())
.distinct()
.limit(9);
return query.fetch();
}
}
관리중인 스터디와 참여중인 스터디를 조회하는 쿼리메소드 작성
package me.weekbelt.studyolle.modules.study;
@Transactional(readOnly = true)
public interface StudyRepository extends JpaRepository<Study, Long>, StudyRepositoryExtension {
// ............
List<Study> findFirst5ByManagersContainingAndClosedOrderByPublishedDateTimeDesc(Account account, boolean closed);
List<Study> findFirst5ByMembersContainingAndClosedOrderByPublishedDateTimeDesc(Account account, boolean checked);
}
로그인시 보여줄 화면으로 이동할 수 있도록 핸들러 수정
package me.weekbelt.studyolle.modules.main;
@RequiredArgsConstructor
@Controller
public class MainController {
private final StudyRepository studyRepository;
private final AccountRepository accountRepository;
private final EnrollmentRepository enrollmentRepository;
@GetMapping("/")
public String home(@CurrentAccount Account account, Model model) {
if (account != null) {
Account findAccount = accountRepository.findAccountWithTagsAndZonesById(account.getId());
List<Enrollment> enrollmentList = enrollmentRepository.findByAccountAndAcceptedOrderByEnrolledAtDesc(account, true);
List<Study> studyList = studyRepository.findByAccount(findAccount.getTags(), findAccount.getZones());
List<Study> studyManagerOf = studyRepository.findFirst5ByManagersContainingAndClosedOrderByPublishedDateTimeDesc(account, false);
List<Study> studyMemberOf = studyRepository.findFirst5ByMembersContainingAndClosedOrderByPublishedDateTimeDesc(account, false);
model.addAttribute("account", findAccount);
model.addAttribute("enrollmentList", enrollmentList);
model.addAttribute("studyList", studyList);
model.addAttribute("studyManagerOf", studyManagerOf);
model.addAttribute("studyMemberOf", studyMemberOf);
return "index-after-login";
}
List<Study> studyList = studyRepository.findFirst9ByPublishedAndClosedOrderByPublishedDateTimeDesc(true, false);
model.addAttribute("studyList", studyList);
return "index";
}
// ..........
}
로그인 후 메인화면 (index-after-login.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="alert alert-warning" role="alert" th:if="${account != null && !account?.emailVerified}">
스터디올레 가입을 완료하려면 <a href="#" th:href="@{/check-email(email=${account.email})}" class="alert-link">계정 인증 이메일을 확인</a>하세요.
</div>
<div class="container mt-4">
<div class="row">
<div class="col-md-2">
<h5 class="font-weight-light">관심 스터디 주제</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item" th:each="tag: ${account.tags}">
<i class="fa fa-tag"></i> <span th:text="${tag.title}"></span>
</li>
<li class="list-group-item" th:if="${account.tags.size() == 0}">
<a th:href="@{/settings/tags}" class="btn-text">관심 스터디 주제</a>를 등록하세요.
</li>
</ul>
<h5 class="mt-3 font-weight-light">주요 활동 지역</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item" th:each="zone: ${account.zones}">
<i class="fa fa-globe"></i> <span th:text="${zone.getLocalNameOfCity()}">Zone</span>
</li>
<li class="list-group-item" th:if="${account.zones.size() == 0}">
<a th:href="@{/settings/zones}" class="btn-text">주요 활동 지역</a>을 등록하세요.
</li>
</ul>
</div>
<div class="col-md-7">
<h5 th:if="${#lists.isEmpty(enrollmentList)}" class="font-weight-light">참석할 모임이 없습니다.</h5>
<h5 th:if="${!#lists.isEmpty(enrollmentList)}" class="font-weight-light">참석할 모임</h5>
<div class="row row-cols-1 row-cols-md-2" th:if="${!#lists.isEmpty(enrollmentList)}">
<div class="col mb-4" th:each="enrollment: ${enrollmentList}">
<div class="card">
<div class="card-body">
<h5 class="card-title" th:text="${enrollment.event.title}">Event title</h5>
<h6 class="card-subtitle mb-2 text-muted" th:text="${enrollment.event.study.title}">Study title</h6>
<p class="card-text">
<span>
<i class="fa fa-calendar-o"></i>
<span class="calendar" th:text="${enrollment.event.startDateTime}">Last updated 3 mins ago</span>
</span>
</p>
<a th:href="@{'/study/' + ${enrollment.event.study.path} + '/events/' + ${enrollment.event.id}}" class="card-link">모임 조회</a>
<a th:href="@{'/study/' + ${enrollment.event.study.path}}" class="card-link">스터디 조회</a>
</div>
</div>
</div>
</div>
<h5 class="font-weight-light mt-3" th:if="${#lists.isEmpty(studyList)}">관련 스터디가 없습니다.</h5>
<h5 class="font-weight-light mt-3" th:if="${!#lists.isEmpty(studyList)}">주요 활동 지역의 관심 주제 스터디</h5>
<div class="row justify-content-center">
<div th:replace="fragments.html :: study-list (studyList=${studyList})"></div>
</div>
</div>
<div class="col-md-3">
<h5 class="font-weight-light" th:if="${#lists.isEmpty(studyManagerOf)}">관리중인 스터디가 없습니다.</h5>
<h5 class="font-weight-light" th:if="${!#lists.isEmpty(studyManagerOf)}">관리중인 스터디</h5>
<div class="list-group" th:if="${!#lists.isEmpty(studyManagerOf)}">
<a href="#" th:href="@{'/study/' + ${study.path}}" th:text="${study.title}"
class="list-group-item list-group-item-action" th:each="study: ${studyManagerOf}">
Study title
</a>
</div>
<h5 class="font-weight-light mt-3" th:if="${#lists.isEmpty(studyMemberOf)}">참여중인 스터디가 없습니다.</h5>
<h5 class="font-weight-light mt-3" th:if="${!#lists.isEmpty(studyMemberOf)}">참여중인 스터디</h5>
<div class="list-group" th:if="${!#lists.isEmpty(studyMemberOf)}">
<a href="#" th:href="@{'/study/' + ${study.path}}" th:text="${study.title}"
class="list-group-item list-group-item-action" th:each="study: ${studyManagerOf}">
Study title
</a>
</div>
</div>
</div>
</div>
<div th:replace="fragments.html :: footer"></div>
<div th:replace="fragments.html :: date-time"></div>
</body>
</html>
참고: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1#
'스프링과 JPA 기반 웹 어플리케이션 개발 > 8부 검색 및 첫 페이지' 카테고리의 다른 글
82. 로그인 하지 않은 사용자를 위한 첫 화면 (0) | 2020.05.18 |
---|---|
81. 페이징 뷰 개선 (0) | 2020.05.18 |
80. 페이징 적용 (0) | 2020.05.17 |
79. N+1 Select 문제 해결 (0) | 2020.05.17 |
78. 검색 기능 구현 (0) | 2020.05.17 |