이 글은 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 책을 참고하여 작성되었습니다.
첫 페이지에 글 목록을 나타내기 위해 index.mustache에 코드를 추가하자.
index.mustache
...
</div>
</div>
<br>
<!-- 목록 출력 영역 -->
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
{{#posts}}
<tr>
<td>{{id}}</td>
<td>{{title}}</td>
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
</tbody>
</table>
</div>
{#posts}
: posts라는 list를 순회한다. java의 for문과 비슷하다.{{id}}, {{title}}, ...
: list에서 뽑아낸 객체의 필드를 사용한다. 이제 레포지토리, 서비스, 컨트롤러 코드를 작성하자.
PostsRepository.java
public interface PostsRepository extends JpaRepository<Posts, Long> {
@Query(value = "SELECT p.* FROM posts p ORDER BY p.id DESC", nativeQuery = true)
List<Posts> findAllDesc();
}
github에 게시된 issue를 참고하자.
@Query
: SpringDataJpa에서 제공하지 않는 메소드는 위처럼 쿼리로 작성해도 된다. 여기서는 일반적인 sql이 아닌 JPA에서 사용되는 JPQL이다.PostsServiec.java
@RequiredArgsConstructor
@Service
public class PostsService {
...
@Transactional(readOnly = true)
public List<PostsListResponseDto> findAllDesc() {
return postsRepository.findAllDesc().stream()
.map(PostsListResponseDto::new)
.collect(Collectors.toList());
}
...
}
readOnly = true
: @Transactional
의 옵션이며, 조회 기능만 남겨두어 조회 속도를 개선시켜준다..map(PostsListResponseDto::new)
: 람다식으로 표현된 방식이며 .map(posts -> newPostsListResponseDto(posts)와 같다. Posts의 stream을 map을 통해 PostsListResponseDto로 변환 -> List로 반환하는 메서드이다.
PostsListResponseDto.java
public class PostsListResponseDto {
... // 필드 선언 생략
public PostsListResponseDto(Posts entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.author = entity.getAuthor();
this.modifiedDate = entity.getModifiedDate();
}
}
entity를 통해 필요한 값만 받아온다.
IndexController.java
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user) {
model.addAttribute("posts", postsService.findAllDesc());
return "index";
}
...
}
model
: 서버 템플릿 엔진에서 사용가능한 객체를 저장할 수 있다. findAllDesc()로 가져온 결과를 index.mustache에 전달한다.미리 만들어둔 게시글 수정 api로 요청하는 화면을 작성하자.
posts-update.mustache
{{>layout/header}}
<h1>게시글 수정</h1>
<div class="col-md-12">
<div class="col-md-4">
<form>
<div class="form-group">
<label for="title">글 번호</label>
<input type="text" class="form-control" id="id" value="{{post.id}}" readonly>
</div>
<div class="form-group">
<label for="title">제목</label>
<input type="text" class="form-control" id="title" value="{{post.title}}">
</div>
<div class="form-group">
<label for="author"> 작성자 </label>
<input type="text" class="form-control" id="author" value="{{post.author}}" readonly>
</div>
<div class="form-group">
<label for="content"> 내용 </label>
<textarea class="form-control" id="content">{{post.content}}</textarea>
</div>
</form>
<a href="/" role="button" class="btn btn-secondary">취소</a>
<button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>
</div>
</div>
{{>layout/footer}}
{{post.id}}
: 머스테치는 객체의 필드 접근 시 . 으로 구분한다. 여기서는 Post 클래스의 id 필드에 대한 접근이다.readonly
: Input 태크에 읽기 가능만 허용하여 수정이 불가능해진다. id, author는 수정이 불가능하다.수정 버튼이 동작하도록 index.js에도 코드를 추가하자.
index.js
var main = {
init : function () {
...
$('#btn-update').on('click', function () {
_this.update();
});
},
...
update : function () {
var data = {
title: $('#title').val(),
content: $('#content').val()
};
var id = $('#id').val();
$.ajax({
type: 'PUT',
url: '/api/v1/posts/'+id,
dataType: 'json',
contentType:'application/json; charset=utf-8',
data: JSON.stringify(data)
}).done(function() {
alert('글이 수정되었습니다.');
window.location.href = '/';
}).fail(function (error) {
alert(JSON.stringify(error));
});
}
};
type: PUT
: api에서 @PutMapping
으로 선언했기 때문에 PUT으로 사용한다. REST 규약에 맞게 HTTP method를 매핑한 것이다. Create - POST, Read - GET, Update - PUT, Delete - DELETEurl: 'api/v1/posts/'+id
: 어느 게시글을 수정할지 URL path로 구분하기 위해 path에 id를 추가한다.앞서 만들었던 전체 목록에서 바로 수정 페이지로 이동하도록 페이지 이동 기능을 추가하기 위해 index.mustache 에서 title부분을 수정하자.
index.mustache
...
<td><a href="/posts/update/{{id}}">{{title}}</a></td>
...
마지막으로 컨트롤러를 작성하자.
IndexController.java
@RequiredArgsConstructor
@Controller
public class IndexController {
@GetMapping("posts/update/{id}")
public String postsUpdate(@PathVariable Long id, Model model) {
PostsResponseDto dto = postsService.findById(id);
model.addAttribute("post", dto);
return "posts-update";
}
}
model
: 전체 조회처럼 id로 글을 찾아 posts-update.mustache로 넘겨준다.앞서 작성했던 posts-update.mustache에서 수정 완료 버튼옆에 삭제 버튼을 추가한다.
posts-update.mustache
...
<button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>
<button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
...
삭제 버튼이 동작하도록 index.js에 코드를 추가하자.
index.js
var main = {
init : function () {
var _this = this;
...
$('#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));
});
}
};
type: 'DELETE'
: 앞서 언급한 REST 규약에 맞게 삭제는 DELETE 메서드로 작성되었다.이제 삭제 API를 완성해보자.
PostsService.java
@RequiredArgsConstructor
@Service
public class PostsService {
...
@Transactional
public void delete (Long id) {
Posts posts = postsRepository.findById(id)
.orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
postsRepository.delete(posts);
}
...
}
postsRepository.delete(posts)
: JpaRepository에서 지원하는 delete 메서드를 사용한다. 엔티티를 파라미터로 삭제할 수도 있고, deleteById 메서드로 id를 이용할 수도 있다. 존재하는 포스트인지 확인 후 삭제한다.IndexController.java
@RequiredArgsConstructor
@Controller
public class IndexController {
...
@DeleteMapping("posts/update/{id}")
public Long delete(@PathVariable Long id) {
postsService.delete(id);
return id;
}
}