목차 (클릭시 해당 목차로 이동)
lecture 정보 페이지 제작
강의소개
설계
- 왼쪽에는 강의 대표 이미지가 나오고 오른쪽에는 강의정보들이 나온다.
- 강의정보에는 강의이름, 강의자, 평균별점, 수강평개수, 원래사이트이름(클릭시 이동) 정보가 나온다.
- 아래에는 강의에 대한 소개를 담은 content를 볼 수 있게 한다.
lecture.html
- bootstrap의 panel을 이용해 정보를 보여준다.
- thymeleaf를 이용해 컨트롤러에서 받은 lectureDto를 연결한다.
- 별점이 없을 경우에는 빈(회색) 별점 5개를 보여준다.
<div class="container">
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<div class="panel-title">
강의소개
</div>
</div>
<div class="panel-body">
<div class="media">
<div class="col-md-6" >
<div class="media-left">
<a href="">
<img src="/images/springInstroduction.png" class="img-responsive" alt="">
</a>
</div>
</div>
<div class="col-md-6">
<div class="media-body">
<h4 th:text="${lectureDto.getLectureName()}">강의이름</h4>
<h4 th:text="${lectureDto.getTeacherName()}">강의자</h4>
<p class="star_rating">
<a th:if="${lectureDto.getAverageRating() != 0}" class="on" th:each="num : ${#numbers.sequence(0, lectureDto.getAverageRating()-1)}">★</a>
<a th:unless="${lectureDto.getAverageRating() != 0}">★★★★★</a>
</p>
<h4 th:text="|${lectureDto.getReviewNum()}개의 수강평|">n개의 수강평</h4>
<a th:href="@{${lectureDto.getUri()}}" th:text="${lectureDto.getSiteName()}" target='_blank'>원래사이트</a>
</div>
</div>
</div>
</div>
<div>
<hr>
<div th:text="${lectureDto.getContent()}">content </div>
</div>
</div>
</div>
</div> <!-- /container -->
lectureController
- @PathVariable을 이용해 실제 lecture 객체를 찾고, LectureInfoDto에 정보를 옮긴다.
- Model의 addAttribute를 이용해 lecture.html에 lectureInfoDto를 넘겨준다.
@GetMapping("/lectures/{lectureId}")
public String lectureInfo(@PathVariable("lectureId") Long lectureId, Model lectureModel) {
Lecture lecture = lectureService.findById(lectureId);
LectureInfoDto lectureInfoDto = new LectureInfoDto();
setLectureInfoDto(lecture, lectureInfoDto);
lectureModel.addAttribute("lectureDto", lectureInfoDto);
return "lectures/lecture";
}
setLectureInfoDto
uri는 String으로 바꿔서 넘겨준다.
private void setLectureInfoDto(Lecture lecture, LectureInfoDto lectureInfoDto) {
lectureInfoDto.setAverageRating((int) Math.round(lecture.getAverageRating()));
lectureInfoDto.setContent(lecture.getContent());
lectureInfoDto.setLectureName(lecture.getLectureName());
lectureInfoDto.setReviewNum(lecture.getReviewNum());
lectureInfoDto.setSiteName(lecture.getSiteName());
lectureInfoDto.setTeacherName(lecture.getTeacherName());
if (lecture.getUri() != null) {
lectureInfoDto.setUri(lecture.getUri().toString());
}
}
LectureDto
package dongho.classflix.controller.dto;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class LectureInfoDto {
private String lectureName;
private String teacherName;
private String content;
private String siteName;
private String uri;
private int averageRating;
private int reviewNum;
}
구현확인
아직 리뷰가 등록되지 않아 별점은 0개, 0개의 수강평이 제대로 뜨는 것을 볼 수 있다.
리뷰등록, 조회
설계
- 회원선택을 드랍다운메뉴로 구현하여 저장되어있는 회원들 중에서 선택할 수 있게한다.
- 댓글을 입력할때 비밀번호를 입력할 수 있게한다.
- 별점을 1~5개중에 고를 수 있게 한다.
- 후기를 최대 25자까지 입력하게 한다.
- 리뷰등록시 같은 url로 redirect한다. (PRG이용)
lecture.html
- 회원선택에서 select, option 그리고 thymeleaf의 th:value, th:field를 이용해 선택한 회원이 넘어올 수 있도록한다.
별점도 마찬가지로 - 후기 입력시에는 225바이트까지(약 한글 50자)까지 입력할 수 있게 한다.
- 만약 리뷰가 없을 경우에는 th:if="${not #strings.isEmpty(reviewDtos)}"를 이용해 null이나 빈 문자일경우에 해당 태그를 아예 실행하지 않게 한다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<style>
.fieldError {
border-color: #bd2130;
}
.star_rating {font-size:0; letter-spacing:-4px;}
.star_rating a {
font-size:22px;
letter-spacing:0;
display:inline-block;
margin-left:5px;
color:#ccc;
text-decoration:none;
}
.star_rating a:first-child {margin-left:0;}
.star_rating a.on {color:#E1BC3F;}
</style>
<body>
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<div class="container">
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<div class="panel-title">
강의소개
</div>
</div>
<div class="panel-body">
<div class="media">
<div class="col-md-6" >
<div class="media-left">
<a href="">
<img src="/images/springInstroduction.png" class="img-responsive" alt="">
</a>
</div>
</div>
<div class="col-md-6">
<div class="media-body">
<h4 th:text="${lectureDto.getLectureName()}">강의이름</h4>
<h4 th:text="${lectureDto.getTeacherName()}">강의자</h4>
<p class="star_rating">
<a th:if="${lectureDto.getAverageRating() != 0}" class="on" th:each="num : ${#numbers.sequence(0, lectureDto.getAverageRating()-1)}">★</a>
<a th:unless="${lectureDto.getAverageRating() != 0}">★★★★★</a>
</p>
<h4 th:text="|${lectureDto.getReviewNum()}개의 수강평|">n개의 수강평</h4>
<a th:href="@{${lectureDto.getUri()}}" th:text="${lectureDto.getSiteName()}" target='_blank'>원래사이트</a>
</div>
</div>
</div>
</div>
<div>
<hr>
<div th:text="${lectureDto.getContent()}">content </div>
</div>
</div>
</div>
</div> <!-- /container -->
<!--리뷰조회-->
<div class="container">
<div>
<table class="table table-striped">
<thead>
<tr>
<th>회원이름</th>
<th>별점</th>
<th>후기</th>
<th></th>
</tr>
</thead>
<tbody><tr th:if="${not #strings.isEmpty(reviewDtos)}" th:each="reviewDto : ${reviewDtos}">
<td th:text="${reviewDto.getMemberName()}"></td>
<td>
<p class="star_rating">
<a th:if="${reviewDto.getRating() != 0}" class="on" th:each="num : ${#numbers.sequence(0, reviewDto.getRating()-1)}">★</a>
</p>
</td>
<td th:text="${reviewDto.getContent()}"></td>
<td>
<a href="#" class="btn btn-primary" role="button">수정</a>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 리뷰등록-->
<hr>
<form role="form" method="post" th:object="${reviewForm}">
<div class="form-group">
<label for="formMemberSelect">회원선택</label>
<select id="formMemberSelect" th:field="*{memberName}">
<option value="">==회원선택==</option>
<option th:each="memberDto : ${memberDtos}" th:text="${memberDto.getUserName()}" th:value="${memberDto.getUserName()}">동호</option>
<!-- <option th:text="" value="">영한</option>-->
<!-- <option value="">동영</option>-->
</select>
</div>
<div class="form-group">
<label for="formPassword">비밀번호입력</label>
<input type="password" id="formPassword" th:field="*{password}">
</div>
<div class="form-group">
<label for="formStarRating">별점</label>
<select id="formStarRating" th:field="*{rating}">
<option value="1">★</option>
<option value="2">★★</option>
<option value="3">★★★</option>
<option value="4">★★★★</option>
<option value="5" selected>★★★★★</option>
</select>
</div>
<div class="form-group">
<label>후기: </label>
<textarea class="form-control" rows="5" id="commentContent" placeholder="최대 50자까지 입력 가능합니다."
name="commentContent" maxlength="255" th:field="*{content}"></textarea>
<button type="submit" class="btn-primary pull-right">등록</button>
</div>
</form>
</div>
<div th:replace="fragments/footer :: footer" />
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="/js/bootstrap.js"></script>
</body>
</html>
LectureController
- 리뷰등록창에서 멤버를 조회해야 하기 때문에 lectureInfoMemberDtos를 만들어서 조회한 멤버들을 이름만 리스트로 담아 넘겨준다.
- ReviewForm도 Dto로 넘겨주어서 필요한 정보만 받고 컨트롤러에서 reviewForm을 넘겨준다.
- lectureId를 이용해 강의에 달린 리뷰를 조회해 Dto List로 view에 전달한다.
@GetMapping("/lectures/{lectureId}")
public String lectureInfo(@PathVariable("lectureId") Long lectureId, Model lectureModel, Model memberModel, Model reviewFormModel, Model reviewModel) {
Lecture lecture = lectureService.findById(lectureId);
LectureInfoDto lectureInfoDto = new LectureInfoDto();
setLectureInfoDto(lecture, lectureInfoDto);
lectureModel.addAttribute("lectureDto", lectureInfoDto);
List<LectureInfoMemberDto> lectureInfoMemberDtos = new ArrayList<>();
List<Member> members = memberService.findMembers();
for (int i = 0; i < members.size(); i++) {
LectureInfoMemberDto lectureInfoMemberDto = new LectureInfoMemberDto();
lectureInfoMemberDto.setUserName(members.get(i).getUserName());
lectureInfoMemberDtos.add(lectureInfoMemberDto);
}
memberModel.addAttribute("memberDtos", lectureInfoMemberDtos);
reviewFormModel.addAttribute("reviewForm", new ReviewForm());
List<Review> reviews = reviewService.findByLecture(lectureId);
List<ReviewDto> reviewDtos = new ArrayList<>();
for (int i = 0; i < reviews.size(); i++) {
ReviewDto reviewDto = new ReviewDto();
reviewDto.setMemberName(reviews.get(i).getMember().getUserName());
reviewDto.setContent(reviews.get(i).getContent());
reviewDto.setPassword(reviews.get(i).getPassword());
reviewDto.setRating(reviews.get(i).getRating());
reviewDto.setReviewDate(reviews.get(i).getReviewDate());
reviewDtos.add(reviewDto);
}
reviewModel.addAttribute("reviewDtos", reviewDtos);
return "lectures/lecture";
}
- url주소는 같게, 하지만 PostMapping으로 method만 다르게 받는다.
- redirectAttributes를 이용해 해당 강의로 다시 redirect한다.
@PostMapping("/lectures/{lectureId}")
private String createReview(@PathVariable("lectureId") Long lectureId, ReviewForm reviewForm, RedirectAttributes redirectAttributes) {
Member member = memberService.findByName(reviewForm.getMemberName()).get(0);
Lecture lecture = lectureService.findById(lectureId);
Review review = new Review(member, reviewForm.getPassword(), reviewForm.getContent(), reviewForm.getRating(), lecture, LocalDateTime.now());
reviewService.create(review);
redirectAttributes.addAttribute("lectureId", lectureId);
return "redirect:/lectures/{lectureId}";
}
ReviewService, ReviewRepository, Lecture수정
- 리뷰를 등록하면 해당 강의에도 리뷰를 추가해주어야 한다.
- 리뷰등록 -> 해당강의의 리뷰증가, 리뷰수 증가
ReviewService
// 리뷰 등록
public Long create(Review review) {
reviewRepository.save(review);
return review.getId();
}
// 강의에 달린 리뷰 조회
@Transactional(readOnly = true)
public List<Review> findByLecture(Long lectureId) {
return reviewRepository.findAllWithLecture(lectureId);
}
ReviewRepository
- Lecture의 addReview를 호출해 review를 넘겨준다.
- lectureId를 이용해 강의에 달린 리뷰를 조회한다.
// 리뷰 저장
public Long save(Review review) {
em.persist(review);
Lecture lecture = review.getLecture();
lecture.addReview(review);
return review.getId();
}
// 강의에 달린 리뷰
public List<Review> findAllWithLecture(Long lectureId) {
return em.createQuery(
"select r from Review r" +
" join fetch r.lecture l" +
" where l.id = :lectureId", Review.class)
.setParameter("lectureId", lectureId)
.getResultList();
}
Lecture
- averageRating을 계산할때 첫 리뷰는 나누지 않고 그대로 보여준다.
- 두번째 리뷰부터는 더하고 2로 나누는 것을 반복해준다.
public void addReview(Review review) {
this.reviewNum += 1;
reviews.add(review);
updateAverageRating(review.getRating());
}
public void removeReview(Review review) {
int restReview = this.reviewNum - 1;
if (restReview < 0) {
throw new NotEnoughReviewException("review is empty");
}
reviews.remove(review);
this.reviewNum -= 1;
updateAverageRating(review.getRating());
}
public void updateAverageRating(Integer rating) {
log.info("reviewNum = {}", reviewNum);
if (reviewNum == 0) {
this.averageRating = 0;
} else {
if (reviewNum == 1) {
this.averageRating = rating;
} else {
double average = (averageRating + rating) / 2;
log.info("averageRating = {}, rating = {}", averageRating, rating);
this.averageRating = Math.floor(average);
}
}
}
구현확인
추가
나중에 수정해야 할 것들
- password를 그냥 String으로 처리해놓았음.
- 강의소개(content)에서 입력시 줄바꿈들이 처리가 안되어서 출력됨.
- 리뷰 등록 폼 디자인 수정
- 리뷰에 날짜 추가
새로 알게된 점들
타임리프에서 데이터를 넘겨받지 못했을때는 th:if="${not #strings.isEmpty(reviewDtos)}" 를 이용하면 된다.
'Project > ClassFlix' 카테고리의 다른 글
[ClassFlix] EP 10. view 페이지 제작과 컨트롤러 연결 - 5 : 리뷰 수정 구현 (0) | 2021.05.11 |
---|---|
[ClassFlix] EP 9. 중간점검 : 앞으로의 계획 (0) | 2021.05.10 |
[ClassFlix] EP 7. view 페이지 제작과 컨트롤러 연결 - 3 (0) | 2021.04.30 |
[ClassFlix] EP 6. view 페이지 제작과 컨트롤러 연결 - 2 (0) | 2021.04.29 |
[ClassFlix] EP 5. view 페이지 제작과 컨트롤러 연결 - 1 (0) | 2021.04.27 |