bootstrap.js는 제이쿼리가 꼭 있어야 함(bootstrap.js가 제이쿼리에 의존)
부트스트랩보다 먼저 호출되도록 작성
라이브러리를 비롯한 기타 HTML 태그들이 모두 레이아웃에 추가됨
index.mustache에 필요한 코드만 남김
index.mustache
{{>layout/header}}<h1>스프링 부트로 시작하는 웹 서비스</h1>{{>layout/footer}}
{{>}}는 현재 머스테치 파일(index.mustache)를 기준으로 다른 파일을 가져옴
index.mustache(글 등록 버튼 추가)
{{>layout/header}}<h1>스프링 부트로 시작하는 웹 서비스</h1><div class = "col-md-12"> <div class = "row"> <div class = "col-md-6"> <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a> </div> </div></div>{{>layout/footer}}
<a> 태그를 사용하여 글 등록 페이지로 이동하는 버튼 생성
IndexController.java
public class IndexController{ // 기존 코드 @GetMapping("/posts/save") public String postSave() { return "posts-save"; }}
not-null property references a null or transient value
어떤 에러인지는 스택오버플로우를 참고하자. Posts.java에서 nullable을 전부 true로 바꿔주니 해결하긴 했다.
전체 조회 화면 만들기
index.mustache
{{>layout/header}}<h1>스프링 부트로 시작하는 웹 서비스</h1><div class = "col-md-12"> <div class = "row"> <div class = "col-md-6"> <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a> </div> </div> <br> <!--목록 출력 영역--> <table class="table table-horizontal table-bordered"> <thread class="thread=strong"> <tr> <th>게시글번호</th> <th>제목</th> <th>작성자</th> <th>최종수정일</th> </tr> </thread> <tbody id="tbody"> {{#posts}} <tr> <td>{{id}}</td> <td>{{title}}</td> <td>{{author}}</td> <td>{{modifiedDate}}</td> </tr> {{/posts}} </tbody> </table></div>{{>layout/footer}}
{{#post}}: posts라는 List를 순회하는 일종의 for문
{{id}}: List에서 뽑아낸 객체의 필드를 사용
PostsRepository.java
package com.jojoldu.book.springboot.domain.posts;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.Query;import java.util.List;public interface PostsRepository extends JpaRepository<Posts, Long>{ @Query("SELECT p FROM Posts p ORDER BY p.id DESC") List<Posts> findAllDesc();}
SpringDataJpa에서 제공하는 메소드로도 위의 동작을 할 수 있으나, 쿼리로 작성하는 것이 더 가독성이 좋아 @Query 사용
PostsService.java
// 기존 코드import com.jojoldu.book.springboot.web.dto.PostsListResponseDto;import java.util.List;import java.util.stream.Collectors;@RequiredArgsConstructor@Servicepublic class PostsService{ // 기존 코드 @Transactional(readOnly = true) // 트랜잭션 범위는 유지하되 조회 기능만 남겨두어 조회 속도 개선 public List<PostsListResponseDto> findAllDesc() { return postsRepository.findAllDesc().stream() .map(PostsListResponseDto::new) .collect(Collectors.toList()); }}
.map(PostsListResponseDto::new)는 람다식
.map(posts -> new PostsListResponseDto(posts))와 같음
// 기존 코드import org.springframework.ui.Model;import lombok.RequiredArgsConstructor;@RequiredArgsConstructor@Controllerpublic class IndexController{ private final PostsService postsService; @GetMapping("/") public String index(Model model) // 기존 index 수정 { model.addAttribute("posts", postsService.findAllDesc()); return "index"; } // 기존 코드}
게시글 수정, 삭제 화면 만들기
게시글 수정
PostsApiController.java
public class PostsApiController{ // 아래 내용 추가 @PutMapping("/api/v1/posts/{id}") public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto) { return postsService.update(id, requestDto); }}
var main = { init : function () { // ... // btn-update란 id를 가진 HTML 엘리먼트에 click 이벤트가 발생할 때 update function을 실행하는 이벤트 등록 $('#btn-update').on('click', function (){ _this.update(); }); }, save : function () { //... }, update : function (){ var data = { title: $('#title').val(), content: $('#content').val() }; var id = $('#id').val(); $.ajax({ type: 'PUT', url: '/api/v1/posts/'+id, // 어느 게시글을 수정할지 URL PATH로 구분하기 위해 id 추가 dataType: 'json', contentType: 'application/json; charset=urf-8', data: JSON.stringify(data) }).done(function (){ alert('글이 수정되었습니다.'); window.location.href = '/'; }).fail(function (error) { alert(JSON.stringify(error)); }); }};
type: 'PUT
HTTP 메소드 중 PUT을 선택
PostsApiController에 있는 API에 @PutMapping으로 선언했기 때문에 PUT을 사용(REST 규약에 따른 설정)
var main = { init : function () { //... $('#btn-delete').on('click', function (){ _this.delete(); }); }, //... delete : function (){ var id = $('#id').val(); $.ajax({ type: 'DELETE', url: '/api/v1/posts/' +id, dataType: 'json', contentType: 'application/json; charset=utf-8' }).done(function (){ alert('글이 삭제되었습니다.'); window.location.href = '/'; }).fail(function (error){ alert(JSON.stringify(error)) }); }};
PostsService.java
@RequiredArgsConstructor@Servicepublic class PostsService{ // ... @Transactional public void delete (Long id) { Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id)); // JpaRepository에서 지원하는 메소드 활용 // deleteById(id)도 사용 가능 postsRepository.delete(posts); }}
PostsApiController.java
@RequiredArgsConstructor@RestControllerpublic class PostsApiController{ //... @DeleteMapping("/api/v1/posts/{id}") public Long delete(@PathVariable Long id) { postsService.delete(id); return id; }}