본문 바로가기
스프링과 JPA 기반 웹 어플리케이션 개발/5부 모임

63. 모임 조회

by Backchus 2020. 5. 4.

등록된 모임 정보 화면을 요청하는 핸들러 작성

package me.weekbelt.studyolle.event;

@Controller
@RequestMapping("/study/{path}")
@RequiredArgsConstructor
public class EventController {

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

    @GetMapping("/events/{id}")
    public String getEvent(@CurrentAccount Account account, @PathVariable String path,
                           @PathVariable Long id, Model model) {
        model.addAttribute(account);
        model.addAttribute(eventService.findEventById(id));
        model.addAttribute(studyService.getStudy(path));
        return "event/view";
    }
}

 

EventService에서 모임 아이디로 모임을 찾는 findEventById메소드 작성

package me.weekbelt.studyolle.event;

@Service
@Transactional
@RequiredArgsConstructor
public class EventService {

    // ......
    
    public Event findEventById(Long id) {
        return eventRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("찾는 모임이 없습니다."));
    }
}

 

모임 뷰

타임리프, 객체의 타입변환

<span th:if="$event.eventType == T(com.studyolle.domain.EventType).FCFS">선착순</span>
<span th:if="$event.eventType == T(com.studyolle.domain.EventType).CONFIRMATIVE">관리자 확인</span>

 

모임 정보를 보여주는 event/view.html 작성

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head th:replace="fragments.html :: head"></head>
<body>
<nav th:replace="fragments.html :: main-nav"></nav>
<div th:replace="fragments.html :: study-banner"></div>
<div class="container">
    <div class="row py-4 text-left justify-content-center bg-light">
        <div class="col-6">
                <span class="h2">
                <a href="#" class="text-decoration-none" th:href="@{'/study/' + ${study.path}}">
                    <span th:text="${study.title}">스터디 이름</span>
                </a> / </span>
            <span class="h2" th:text="${event.title}"></span>
        </div>
        <div class="col-4 text-right justify-content-end">
                <span sec:authorize="isAuthenticated()">
                    <button th:if="${event.isEnrollableFor(#authentication.principal)}"
                            class="btn btn-outline-primary" data-toggle="modal" data-target="#enroll">
                        <i class="fa fa-plus-circle"></i> 참가 신청
                    </button>
                    <button th:if="${event.isDisenrollableFor(#authentication.principal)}"
                            class="btn btn-outline-primary" data-toggle="modal" data-target="#disenroll">
                        <i class="fa fa-minus-circle"></i> 참가 신청 취소
                    </button>
                    <span class="text-success" th:if="${event.isAttended(#authentication.principal)}" disabled>
                        <i class="fa fa-check-circle"></i> 참석 완료
                    </span>
                </span>
        </div>
        <div class="modal fade" id="disenroll" tabindex="-1" role="dialog" aria-labelledby="leaveTitle" aria-hidden="true">
            <div class="modal-dialog modal-dialog-centered" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="leaveTitle" th:text="${event.title}"></h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <p>모임 참가 신청을 취소하시겠습니까?</p>
                        <p><strong>확인</strong>하시면 본 참가 신청을 취소하고 다른 대기자에게 참석 기회를 줍니다.</p>
                        <p>감사합니다.</p>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">닫기</button>
                        <form th:action="@{'/study/' + ${study.path} + '/events/' + ${event.id} + '/disenroll'}" method="post">
                            <button class="btn btn-primary" type="submit" aria-describedby="submitHelp">확인</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
        <div class="modal fade" id="enroll" tabindex="-1" role="dialog" aria-labelledby="enrollmentTitle" aria-hidden="true">
            <div class="modal-dialog modal-dialog-centered" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="enrollmentTitle" th:text="${event.title}"></h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <p>모임에 참석하시겠습니까? 일정을 캘린더에 등록해 두시면 좋습니다.</p>
                        <p><strong>확인</strong> 버튼을 클릭하면 모임 참가 신청을 합니다.</p>
                        <p>감사합니다.</p>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">닫기</button>
                        <form th:action="@{'/study/' + ${study.path} + '/events/' + ${event.id} + '/enroll'}" method="post">
                            <button class="btn btn-primary" type="submit" aria-describedby="submitHelp">확인</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="row px-3 justify-content-center">
        <div class="col-7 pt-3">
            <dt class="font-weight-light">상세 모임 설명</dt>
            <dd th:utext="${event.description}"></dd>

            <dt class="font-weight-light">모임 참가 신청 (<span th:text="${event.enrollments.size()}"></span>)</dt>
            <dd>
                <table class="table table-borderless table-sm" th:if="${event.enrollments.size() > 0}">
                    <thead>
                    <tr>
                        <th scope="col">#</th>
                        <th scope="col">참석자</th>
                        <th scope="col">참가 신청 일시</th>
                        <th scope="col">참가 상태</th>
                        <th th:if="${study.isManager(#authentication.principal)}" scope="col">
                            참가 신청 관리
                        </th>
                        <th th:if="${study.isManager(#authentication.principal)}" scope="col">
                            출석 체크
                        </th>
                    </tr>
                    </thead>
                    <tbody>
                    <tr th:each="enroll: ${event.enrollments}">
                        <th scope="row" th:text="${enrollStat.count}"></th>
                        <td>
                            <a th:href="@{'/profile/' + ${enroll.account.nickname}}"
                               class="text-decoration-none">
                                <svg th:if="${#strings.isEmpty(enroll.account?.profileImage)}" data-jdenticon-value="nickname"
                                     th:data-jdenticon-value="${enroll.account.nickname}" width="24" height="24" class="rounded border bg-light"></svg>
                                <img th:if="${!#strings.isEmpty(enroll.account?.profileImage)}"
                                     th:src="${enroll.account?.profileImage}" width="24" height="24" class="rounded border"/>
                                <span th:text="${enroll.account.nickname}"></span>
                            </a>
                        </td>
                        <td>
                            <span class="date-time" th:text="${enroll.enrolledAt}"></span>
                        </td>
                        <td>
                            <span th:if="${enroll.accepted}">확정</span>
                            <span th:if="${!enroll.accepted}">대기중</span>
                        </td>
                        <td th:if="${study.isManager(#authentication.principal)}">
                            <a th:if="${event.canAccept(enroll)}" href="#" class="text-decoration-none"
                               th:href="@{'/study/' + ${study.path} + '/events/' + ${event.id} + '/enrollments/' + ${enroll.id} + '/accept'}" >신청 수락</a>
                            <a th:if="${event.canReject(enroll)}" href="#" class="text-decoration-none"
                               th:href="@{'/study/' + ${study.path} + '/events/' + ${event.id} + '/enrollments/' + ${enroll.id} + '/reject'}">취소</a>
                        </td>
                        <td th:if="${study.isManager(#authentication.principal)}">
                            <a th:if="${enroll.accepted && !enroll.attended}" href="#" class="text-decoration-none"
                               th:href="@{'/study/' + ${study.path} + '/events/' + ${event.id} + '/enrollments/' + ${enroll.id} + '/checkin'}">체크인</a>
                            <a th:if="${enroll.accepted && enroll.attended}" href="#" class="text-decoration-none"
                               th:href="@{'/study/' + ${study.path} + '/events/' + ${event.id} + '/enrollments/' + ${enroll.id} + '/cancel-checkin'}">체크인 취소</a>
                        </td>
                    </tr>
                    </tbody>
                </table>
            </dd>
        </div>
        <dl class="col-3 pt-3 text-right">
            <dt class="font-weight-light">모집 방법</dt>
            <dd>
                <span th:if="${event.eventType == T(com.studyolle.modules.event.EventType).FCFS}">선착순</span>
                <span th:if="${event.eventType == T(com.studyolle.modules.event.EventType).CONFIRMATIVE}">관리자 확인</span>
            </dd>

            <dt class="font-weight-light">모집 인원</dt>
            <dd>
                <span th:text="${event.limitOfEnrollments}"></span>명
            </dd>

            <dt class="font-weight-light">참가 신청 마감 일시</dt>
            <dd>
                <span class="date" th:text="${event.endEnrollmentDateTime}"></span>
                <span class="weekday" th:text="${event.endEnrollmentDateTime}"></span><br/>
                <span class="time" th:text="${event.endEnrollmentDateTime}"></span>
            </dd>

            <dt class="font-weight-light">모임 일시</dt>
            <dd>
                <span class="date" th:text="${event.startDateTime}"></span>
                <span class="weekday" th:text="${event.startDateTime}"></span><br/>
                <span class="time" th:text="${event.startDateTime}"></span> -
                <span class="time" th:text="${event.endDateTime}"></span>
            </dd>

            <dt class="font-weight-light">모임장</dt>
            <dd>
                <a th:href="@{'/profile/' + ${event.createdBy?.nickname}}" class="text-decoration-none">
                    <svg th:if="${#strings.isEmpty(event.createdBy?.profileImage)}"
                         th:data-jdenticon-value="${event.createdBy?.nickname}" width="24" height="24" class="rounded border bg-light"></svg>
                    <img th:if="${!#strings.isEmpty(event.createdBy?.profileImage)}"
                         th:src="${event.createdBy?.profileImage}" width="24" height="24" class="rounded border"/>
                    <span th:text="${event.createdBy?.nickname}"></span>
                </a>
            </dd>

            <dt th:if="${study.isManager(#authentication.principal)}" class="font-weight-light">모임 관리</dt>
            <dd th:if="${study.isManager(#authentication.principal)}">
                <a class="btn btn-outline-primary btn-sm my-1"
                   th:href="@{'/study/' + ${study.path} + '/events/' + ${event.id} + '/edit'}" >
                    모임 수정
                </a> <br/>
                <button class="btn btn-outline-danger btn-sm" data-toggle="modal" data-target="#cancel">
                    모임 취소
                </button>
            </dd>
        </dl>
        <div class="modal fade" id="cancel" tabindex="-1" role="dialog" aria-labelledby="cancelTitle" aria-hidden="true">
            <div class="modal-dialog modal-dialog-centered" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="cancelTitle" th:text="${event.title}"></h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <p>모임을 취소 하시겠습니까?</p>
                        <p><strong>확인</strong>하시면 본 모임 및 참가 신청 관련 데이터를 삭제합니다.</p>
                        <p>감사합니다.</p>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">닫기</button>
                        <form th:action="@{'/study/' + ${study.path} + '/events/' + ${event.id}}" th:method="delete">
                            <button class="btn btn-primary" type="submit" aria-describedby="submitHelp">확인</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div th:replace="fragments.html :: footer"></div>
</div>
<script th:replace="fragments.html :: date-time"></script>
</body>
</html>

 

모임에 따른 참가신청, 참가신청취소, 참석 완료 상태를 우측 상단에 표시하기위해 Event 엔티티에 검증 메소드 추가

package me.weekbelt.studyolle.domain;

@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(of = "id")
@Entity
public class Event {

    // ..........
    
    public boolean isEnrollableFor(UserAccount userAccount) {
        return isNotClosed() && !isAlreadyEnrolled(userAccount);
    }

    public boolean isDisenrollableFor(UserAccount userAccount) {
        return isNotClosed() && isAlreadyEnrolled(userAccount);
    }

    private boolean isNotClosed() {
        return this.endEnrollmentDateTime.isAfter(LocalDateTime.now());
    }

    public boolean isAttended(UserAccount userAccount) {
        Account account = userAccount.getAccount();
        for (Enrollment enrollment : enrollments) {
            if (enrollment.getAccount().equals(account) && enrollment.isAttended()){
                return true;
            }
        }

        return false;
    }

    private boolean isAlreadyEnrolled(UserAccount userAccount) {
        Account account = userAccount.getAccount();
        for (Enrollment enrollment : this.enrollments) {
            if (enrollment.getAccount().equals(account)) {
                return true;
            }
        }
        return false;
    }

}

 

view에 날짜 표시를 편리하게 하기 위해 Moment.js를 사용

  • 날짜를 여러 형태로 표현해주는 라이브러리. 예) 2020년 3월, 일주일 뒤, 4시간 전, .......
  • https://moments.com/
npm install moment --save

 

fragments.html에 date-time fragment추가

<div th:fragment="date-time">
    <script src="/node_modules/moment/min/moment-with-locales.min.js"></script>
    <script type="application/javascript">
        $(function () {
            moment.locale('ko');
            $(".date-time").text(function(index, dateTime) {
                return moment(dateTime, "YYYY-MM-DD`T`hh:mm").format('LLL');
            });
            $(".date").text(function(index, dateTime) {
                return moment(dateTime, "YYYY-MM-DD`T`hh:mm").format('LL');
            });
            $(".weekday").text(function(index, dateTime) {
                return moment(dateTime, "YYYY-MM-DD`T`hh:mm").format('dddd');
            });
            $(".time").text(function(index, dateTime) {
                return moment(dateTime, "YYYY-MM-DD`T`hh:mm").format('LT');
            });
            $(".calendar").text(function(index, dateTime) {
                return moment(dateTime, "YYYY-MM-DD`T`hh:mm").calendar();
            });
            $(".fromNow").text(function(index, dateTime) {
                return moment(dateTime, "YYYY-MM-DD`T`hh:mm").fromNow();
            });
            $(".date-weekday-time").text(function(index, dateTime) {
                return moment(dateTime, "YYYY-MM-DD`T`hh:mm").format('LLLL');
            });
        })
    </script>
</div>

 

 

참고: 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

 

'스프링과 JPA 기반 웹 어플리케이션 개발 > 5부 모임' 카테고리의 다른 글

65. 모임 수정  (0) 2020.05.04
64. 모임 목록 조회  (0) 2020.05.04
62. 모임 만들기 폼 서브밋  (0) 2020.05.04
61. 모임 만들기  (0) 2020.04.30
60. 모임 도메인  (0) 2020.04.30