[ClassFlix] EP 8. view 페이지 제작과 컨트롤러 연결 - 4
Project/ClassFlix

[ClassFlix] EP 8. view 페이지 제작과 컨트롤러 연결 - 4

목차 (클릭시 해당 목차로 이동)


     

    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)}" 를 이용하면 된다.