본문 바로가기
스프링과 JPA 기반 웹 어플리케이션 개발/4부 스터디

49. 스터디 조회

by Backchus 2020. 4. 23.

타임리프 Variable Expression에서 객체의 메소드 호출 가능

th:if="${study.isManager(#authentication.principal)}"

 

StudyRepository에 스터디 URL로 스터디를 조회하는 메소드 추가

package me.weekbelt.studyolle.study;

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

    Study findByPath(String path);
}

 

스터디 조회 요청을 받는 핸들러 작성

package me.weekbelt.studyolle.study;

@RequiredArgsConstructor
@Controller
public class StudyController {

    // .......
    
    private final StudyRepository studyRepository;

    // .......
    
    @GetMapping("/study/{path}")
    public String viewStudy(@CurrentAccount Account account, @PathVariable String path,
                            Model model) {
        model.addAttribute(account);
        model.addAttribute(studyRepository.findByPath(path));
        return "study/view";
    }
}

 

Study엔티티에 지금 계정의 가입 여부관련 메소드 추가

package me.weekbelt.studyolle.domain;

@Builder @AllArgsConstructor @NoArgsConstructor
@Getter @Setter @EqualsAndHashCode(of = "id")
@Entity
public class Study {

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

    public boolean isJoinable(UserAccount userAccount){
        Account account = userAccount.getAccount();
        return this.isPublished() && this.isRecruiting()
                && !this.members.contains(account)
                && !this.managers.contains(account);
    }

    public boolean isMember(UserAccount userAccount) {
        return this.members.contains(userAccount.getAccount());
    }

    public boolean isManager(UserAccount userAccount) {
        return this.managers.contains(userAccount.getAccount());
    }
}

 

스터디의 기본 정보를 담은 HTML fragment를 fragments.html에 추가

<div th:fragment="study-info">
    <div class="row pt-4 text-left justify-content-center bg-light">
        <div class="col-6">
            <a href="#" class="text-decoration-none" th:href="@{'/study/' + ${study.path}}">
                <span class="h2" th:text="${study.title}">스터디 이름</span>
            </a>
        </div>
        <div class="col-4 text-right justify-content-end">
                <span th:if="${!study.published}" class="d-inline-block" tabindex="0" data-toggle="tooltip" data-placement="bottom" title="스터디 공개 준비중">
                    <button class="btn btn-primary btn-sm" style="pointer-events: none;"
                            type="button" disabled>DRAFT</button>
                </span>
            <span th:if="${study.closed}" class="d-inline-block" tabindex="0" data-toggle="tooltip" data-placement="bottom" title="스터디 종료함">
                    <button class="btn btn-primary btn-sm" style="pointer-events: none;"
                            type="button" disabled>CLOSED</button>
                </span>
            <span th:if="${!study.recruiting}" class="d-inline-block ml-1" tabindex="0" data-toggle="tooltip" data-placement="bottom" title="팀원 모집중 아님">
                    <button class="btn btn-primary btn-sm" style="pointer-events: none;"
                            type="button" disabled>OFF</button>
                </span>
            <span sec:authorize="isAuthenticated()"
                  th:if="${study.isJoinable(#authentication.principal)}" class="btn-group" role="group" aria-label="Basic example">
                    <a class="btn btn-primary" th:href="@{'/study/' + ${study.path} + '/join'}">
                        스터디 가입
                    </a>
                    <a class="btn btn-outline-primary" th:href="@{'/study/' + ${study.path} + '/members'}" th:text="${study.members.size()}">1</a>
                </span>
            <span sec:authorize="isAuthenticated()"
                  th:if="${!study.closed && study.isMember(#authentication.principal)}" class="btn-group" role="group">
                    <a class="btn btn-outline-warning" th:href="@{'/study/' + ${study.path} + '/leave'}">
                        스터디 탈퇴
                    </a>
                    <a class="btn btn-outline-primary" th:href="@{'/study/' + ${study.path} + '/members'}" th:text="${study.members.size()}">1</a>
                </span>
            <span sec:authorize="isAuthenticated()"
                  th:if="${study.published && !study.closed && study.isManager(#authentication.principal)}">
                    <a class="btn btn-outline-primary" th:href="@{'/study/' + ${study.path} + '/new-event'}">
                        <i class="fa fa-plus"></i>모임 만들기
                    </a>
                </span>
        </div>
    </div>
    <div class="row justify-content-center bg-light">
        <div class="col-10">
            <p class="lead" th:text="${study.shortDescription}"></p>
        </div>
    </div>
    <div class="row justify-content-center bg-light">
        <div class="col-10">
            <p>
                    <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}">Tag</span>
                        </a>
                    </span>
            </p>
        </div>
    </div>
</div>

 

 

스터디 조회의 탭키에 해당하는 HTMl fragment를 fragments.html에 추가

<!--스터디 소개 탭 키-->
<div th:fragment="study-menu (studyMenu)" class="row px-3 justify-content-center bg-light">
    <nav class="col-10 nav nav-tabs">
        <a class="nav-item nav-link" th:classappend="${studyMenu == 'info'}? active" th:href="@{'/study/' + ${study.path}}">
            <i class="fa fa-info-circle"></i>소개
        </a>
        <a class="nav-item nav-link" th:classappend="${studyMenu == 'members'}? active" th:href="@{'/study/' + ${study.path} + '/members'}">
            <i class="fa fa-user"></i>구성원
        </a>
        <a class="nav-item nav-link" th:classappend="${studyMenu == 'events'}? active" th:href="@{'/study/' + ${study.path} + '/events'}">
            <i class="fa fa-calendar"></i>모임
        </a>
        <a sec:authorize="isAuthenticated()" th:if="${study.isManager(#authentication.principal)}"
           class="nav-item nav-link" th:classappend="${studyMenu == 'settings'}? active" th:href="@{'/study/' + ${study.path} + '/settings/description'}">
            <i class="fa fa-cog"></i>설정
        </a>
    </nav>
</div>

 

등록한 스터디를 보여주는 study/view.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 th:replace="fragments.html::study-banner"></div>

    <div class="container">
        <div th:replace="fragments.html::study-info"></div>
        <div th:replace="fragments.html::study-menu(studyMenu='info')"></div>

        <!-- 본문 -->
        <div class="row px-3 justify-content-center">
            <div class="col-10 pt-3" th:utext="${study.fullDescription}"></div>
        </div>
        <!-- footer -->
        <div th:replace="fragments.html::footer"></div>
    </div>
    <script type="application/javascript">
        $(function () {
            $('[data-toggle="tooltip"]').tooltip()
        })
    </script>
</body>
</html>

 

스터디 조회 화면을 요청하면 Study, Account, Tag, Zone와 관련된 쿼리를 각각 호출하게 되어 5개나 되는 쿼리가 호출이 된다. 이것을 조인을 통해 한방에 불러올 수 있게 하는 게 쿼리를 덜 날리기 때문에 효율적이다.

Study엔티티에 조인할 엔티티를 설정

package me.weekbelt.studyolle.domain;
// 추가
@NamedEntityGraph(name = "Study.withAll", attributeNodes = {
        @NamedAttributeNode("tags"),
        @NamedAttributeNode("zones"),
        @NamedAttributeNode("managers"),
        @NamedAttributeNode("members")})
@Builder @AllArgsConstructor @NoArgsConstructor
@Getter @Setter @EqualsAndHashCode(of = "id")
@Entity
public class Study {

   // .....
   
}
package me.weekbelt.studyolle.study;

@Transactional(readOnly = true)
public interface StudyRepository extends JpaRepository<Study, Long> {
    
    // ......
    
    @EntityGraph(value = "Study.withAll", type= EntityGraph.EntityGraphType.LOAD)  // 추가
    Study findByPath(String path);
}

 

이렇게 설정하고 다시 스터디 조회를 하면 쿼리가 1개만 나가는 것을 알 수 있다.

 

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

 

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

이 강좌에서 여러분은 실제로 운영 중인 서비스를 스프링, JPA 그리고 타임리프를 비롯한 여러 자바 기반의 여러 오픈 소스 기술을 사용하여 웹 애플리케이션을 개발하는 과정을 학습할 수 있습니다. 이 강좌를 충분히 학습한다면 여러분 만의 웹 서비스를 만들거나 취직에 도움이 될만한 포트폴리오를 만들 수 있을 겁니다. 활용 웹 개발 프레임워크 및 라이브러리 Java Spring Spring Boot Spring Data JPA Thymeleaf 온라인 강의 스

www.inflearn.com