Spring

Spring 시험 (7번), Thymleaf 이용 리스트 출력

code2772 2023. 1. 9. 08:19
728x90
반응형

 

영화 테이블을 만들어라 (영화번호, 제목, 국가, 장르, 개봉일, 런닝타임)
    더미 데이터의 수는 상관 없다. 이를 어떠한 방식으로든 리스트로 출력해라

✔ 영화 테이블 생성 (Entity) - sql 을 만들지 않고 @Table 을 이용하여 컬럼(제목, 국가, 장르, 런닝타임) 선언

package com.koreait.task7.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import net.bytebuddy.asm.Advice;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;


@Getter
@ToString(callSuper = true)// 자기거라 부모것도 불러와라
@Table(indexes = { //@Table은 엔티티와 매핑할 테이블을 지정
        @Index(columnList = "title"),
        @Index(columnList = "national"),
        @Index(columnList = "genre"),
        @Index(columnList = "running")
        // name을 추가하면 테이블이름이 name값으로 설정이 되고 생략시 Entity이름으로 테이블이 만들어지는 것을 확인할 수 있다.
})
@Entity
//@Entity 어노테이션은 JPA를 사용해 테이블과 매핑할 클래스에 붙여주는 어노테이션이다. 이 어노테이션을 붙임으로써 JPA가 해당 클래스를 관리하게 된다.
public class Movie extends AuditingFields{
    @Id// Entity 입력할 경우 프라이머리 키를 선언해 줘야한다.
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키 생성을 DB에 위임 (Mysql)
    private Long id;
    @Setter @Column(nullable = false) private String title; // 제목
//    @Setter @Column(nullable = false) private LocalDateTime open; // 개봉시기
    @Setter private String national;
    @Setter private String genre;
    @Setter private String running;


    //JPA에서 DB Table의 Column을 Mapping 할 때 @Column Annotation을 사용한다.


    protected Movie() {}

    private Movie( String title,LocalDateTime open, String national,  String genre, String running) {

        this.title = title;
//        this.open = open;
        this.national = national;
        this.genre = genre;
        this.running = running;
    }

    public static Movie of(String title, LocalDateTime open, String national, String genre, String running) { // of함수 객체를 만들 수 있는 함수
        return new Movie(title, open, national, genre, running);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
    @Override
    public boolean equals(Object obj) {
        if(this == obj) return true;
        if(!(obj instanceof Movie movie)) return false;
        return id != null && id.equals(movie.id);
    }
}


 

✔ 영화 테이블의 개봉일을 AudithingFields로 자동 선언

package com.koreait.task7.domain;

import lombok.Getter;
import lombok.ToString;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.format.annotation.DateTimeFormat;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@ToString
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass//이 아이는 부모가 될거야(UserAccount는 자식으로 AuditingFields를 상속한다.)
public abstract class AuditingFields {

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) // 데이터의 형태를 사용하기 쉽게 변경하기
    @CreatedDate
    @Column(nullable = false) private LocalDateTime open; // 생성일시 수정 시 자동 업데이트

 

✔ 더미 데이터 생성하여 테이블에 값을 넣어주기

 

✔ 테이블 데이터 들을 Dto 로 선언하여 사용하기

package com.koreait.task7.dto;

import com.koreait.task7.domain.Movie;

import java.time.LocalDateTime;

public record MovieDto(
        Long id,
        String title,
        LocalDateTime open,
        String national,
        String genre,
        String running
)
{
    public static MovieDto of(  Long id
            ,String title, LocalDateTime open,
                                  String national, String genre,
 String running) {

        return new MovieDto( id, title,  open,
                national,  genre,  running);
    }
    public static  MovieDto from(Movie entity){
        return new MovieDto(
                entity.getId(),
                entity.getTitle(),
                entity.getOpen(),
                entity.getNational(),
                entity.getGenre(),
                entity.getRunning()
        );
    }
    public Movie toEntity(){
        return  Movie.of(
                title,
                open,
                national,
                genre,
                running

        );
    }
}

 

✔ 게시글에 테이블 데이터를 출력하기 위한 서비스를 만들어 주기

package com.koreait.task7.service;

import com.koreait.task7.domain.Movie;
import com.koreait.task7.dto.MovieDto;
import com.koreait.task7.repository.MovieRepository;
import com.koreait.task7.domain.Type.SearchType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityNotFoundException;

@Slf4j
@Transactional
@RequiredArgsConstructor
@Service
public class MovieService {
    private final MovieRepository movieRepository;

    @Transactional(readOnly = true)// 데이터에 권한을 주지 않는다
    public Page<MovieDto> searchMovies(SearchType searchType, String searchKeyword, Pageable pageable){
        if (searchKeyword == null || searchKeyword.isBlank()){
            return movieRepository.findAll(pageable).map(MovieDto::from);
        }
        return switch (searchType){
            case TITLE -> movieRepository.findByTitleContaining(searchKeyword, pageable).map(MovieDto::from);
            case OPEN -> movieRepository.findByOpenContaining(searchKeyword, pageable).map(MovieDto::from);
            case ID -> movieRepository.findByNationalContaining(searchKeyword, pageable).map(MovieDto::from);
            case GENRE -> movieRepository.findByGenre(searchKeyword, pageable).map(MovieDto::from);
            case NATIONAL -> movieRepository.findByNationalContaining("#"+ searchKeyword, pageable).map(MovieDto::from);
            case RUNNING -> movieRepository.findByRunning("#"+ searchKeyword, pageable).map(MovieDto::from);
        };
    }
    @Transactional(readOnly = true)
    public MovieDto getMovie(Long movieId){
        return movieRepository.findById(movieId)
                .map(MovieDto::from)
                .orElseThrow(() -> new EntityNotFoundException("게시글이 없습니다 - movieId : " + movieId));
    }


    public void saveMovie(MovieDto dto){
        movieRepository.save(dto.toEntity());
    }
    public void updateMovie(MovieDto dto) {// 바로 세이브 하는것이 아니라 있는지 없는지 확인을 하고 업데이트를 해야한다. getReferenceById : 셀렉트 없이 실행
        try {
            Movie movie = movieRepository.getReferenceById(dto.id());
            if (dto.title() != null) {
                movie.setTitle(dto.title());
            }
            if (dto.title() != null) {
                movie.setTitle(dto.title());
            }
            movie.setRunning(dto.running());
            // dto.title() 없으면 get ()안에 머가 있으면 set -> 자바17버전
        }catch (EntityNotFoundException e){
            log.warn("게시글 업데이트 실패. 게시글을 찾을 수 없음 - dto: {}", dto);
        }
    }
}

 

✔ 출력할 리스트의 페이지를 만들어 주고 데이터를 전달 할 부분을 만들어 주기

package com.koreait.task7.controller;

import com.koreait.task7.dto.response.MovieResponse;
import com.koreait.task7.service.MovieService;
import com.koreait.task7.domain.Type.SearchType;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@RequiredArgsConstructor// 생성자 추가시 작성
@RequestMapping("movies")
@Controller
public class MovieController {

    private final MovieService movieService;// 생성자



    @GetMapping
    public String movies(
            @RequestParam(required =false) SearchType searchType,
            @RequestParam(required =false)String searchValue,
            @PageableDefault(size = 10, sort = "open",direction = Sort.Direction.DESC) Pageable pageable, ModelMap map
    ){
        map.addAttribute("movies", movieService
                .searchMovies(searchType,searchValue,pageable).map(MovieResponse::from));
        return "movies/index"; //  index 전달
    }

 

✔ 출력할 페이지의 HTML


<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="hunseop">
    <title>게시판 페이지</title>

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
<!--    <link href="/css/search-bar.css" rel="stylesheet">-->
</head>

<body>

<header id="header">
    헤더 삽입
    <hr>
</header>
<main class="container">

    <div class="row">
        <div class="card card-margin search-form">
            <div class="card-body p-0">
                <form id="card search-form">
                    <div class="row">
                        <div class="col-12">
                            <div class="row no-gutters">
                                <div class="col-lg-3 col-md-3 col-sm-12 p-0">
                                    <label for="search-type" hidden>검색 유형</label>
                                    <select class="form-control" id="search-type">
                                        <option>제목</option>
                                        <option>본문</option>
                                        <option>id</option>
                                        <option>닉네임</option>
                                        <option>해시태그</option>
                                    </select>
                                </div>
                                <div class="col-lg-8 col-md-6 col-sm-12 p-0">
                                    <label for="search-value" hidden>검색어</label>
                                    <input type="text" placeholder="검색어..." class="form-control" id="search-value" name="search-value">
                                </div>
                                <div class="col-lg-1 col-md-3 col-sm-12 p-0">
                                    <button type="submit" class="btn btn-base">
                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search">
                                            <circle cx="11" cy="11" r="8"></circle>
                                            <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
                                        </svg>
                                    </button>
                                </div>
                            </div>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>

    <table class="table" id="movie-table">

        <thead>
        <tr>
            <th class="title">제목</th>
            <th class="open">개봉시기</th>
            <th class="national">국가</th>
            <th class="genre">장르</th>
            <th class="running ">런닝타임</th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td class = "title">아바타</td>
            <td class = "open"><time>2023-01-03</time></td>
            <td class = "national">한국</td>
            <td class = "genre">액션</td>
            <td class = "running">10분</td>
        </tr>
        <tr>
            <td>두번째글</td>
            <td>#spring</td>
            <td>김사과</td>
            <td>2023-01-03</td>
            <td>2023-01-03</td>
        </tr>
        <tr>
            <td>세번째글</td>
            <td>#java</td>
            <td>김사과</td>
            <td>2023-01-03</td>
            <td>2023-01-03</td>
        </tr>
        </tbody>
    </table>
    <nav aria-label="Page navigation example">
        <ul class="pagination justify-content-center">
            <li class="page-item"><a class="page-link" href="#">Previous</a></li>
            <li class="page-item"><a class="page-link" href="#">1</a></li>
            <li class="page-item"><a class="page-link" href="#">2</a></li>
            <li class="page-item"><a class="page-link" href="#">3</a></li>
            <li class="page-item"><a class="page-link" href="#">Next</a></li>
        </ul>
    </nav>
</main>

<footer id="footer">
    푸터 삽입
    <hr>
</footer>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2" crossorigin="anonymous"></script>
</body>
</html>

 

✔ HTML 에 데이터를 보내주기 위한 부분

<?xml version="1.0"?>
<thlogic>
    <attr sel="#movie-table">
        <attr sel="tbody" th:remove="all-but-first">
            <attr sel="tr[0]" th:each="movie : ${movies}">
                <attr sel="td.title" th:text="${movie.title}"/>
                <attr sel="td.open/time" th:datetime="${movie.open}"  th:text="${#temporals.format(movie.open, 'yyyy-MM-dd')}"/>
                <attr sel="td.national" th:text="${movie.national}"/>
                <attr sel="td.genre" th:text="${movie.genre}"/>
                <attr sel="td.running" th:text="${movie.running}"/>
            </attr>
        </attr>
    </attr>
</thlogic>

 

✔ 영화 리스트 출력화면

 

반응형