등록된 모임 정보 화면을 요청하는 핸들러 작성
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">×</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">×</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">×</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 기반 웹 어플리케이션 개발 > 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 |