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

83. 로그인 한 사용자를 위한 첫 화면

by Backchus 2020. 5. 18.

로그인 한 첫 페이지

이 화면을 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 기반 웹 애플리케이션 개발 - 인프런

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

www.inflearn.com