[Spring Boot] 게시물 화면 구성 (2)
2020. 9. 17. 00:54ㆍSpring/Spring Boot
1. 조회 화면
1) index.mustache
전체 조회를 위한 UI를 변경한다.
어노테이션 및 코드 | 설명 |
{{#posts}} | posts라는 List를 순회한다. (for 문) |
{{변수명}} | List에서 뽑아낸 객체의 필드를 사용한다. |
{{>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">
<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>
{{>layout/footer}}
2) Repository
Spring Data JPA에서 제공하지 않는 메서드는 @Query 어노테이션을 통해 쿼리를 작성할 수 있다.
package com.springbook.biz.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();
}
※ 규모가 있는 프로젝트에서는 등록, 수정, 삭제 등은 Spring Data JPA를 통해 진행하지만, 복잡한 조회의 경우에는 조회용 프레임워크(querydsl, jooq, MyBatis 등)를 추가하여 사용한다. 그 중, 국내 많은 회사에서는 querydsl을 사용중이다. 문자열로 쿼리를 생성하는 것이 아닌 메서드 기반으로 쿼리를 생성하며 레퍼런스가 많은 장점이 있다.
3) Service
어노테이션 및 코드 | 설명 |
@Transactional(readOnly=true) | 트랜잭션 범위는 유지하되, 조회 기능만 남겨두어 조회 속도가 개선되도록 readOnly 속성을 준다. |
.map(PostsListResponseDto::new) | '.map(posts -> new PostsListResponseDto(posts)' 와 동일한 코드로 postsRepository의 결과인 Posts의 Stream을 map을 통해 변환하는 메서드이다. |
package com.springbook.biz.service.posts;
import com.springbook.biz.domain.posts.Posts;
import com.springbook.biz.domain.posts.PostsRepository;
import com.springbook.biz.web.dto.PostsResponseDto;
import com.springbook.biz.web.dto.PostsSaveRequestDto;
import com.springbook.biz.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class PostsService {
... // 추가될 내용
@Transactional(readOnly = true)
public List<PostsListResponseDto> findAllDesc() {
return postsRepository.findAllDesc().stream()
.map(PostsListResponseDto::new)
.collect(Collectors.toList());
}
}
4) PostListResponseDto
게시글 목록을 전달해주는 PostListResponseDto 객체를 생성한다.
package com.springbook.biz.web.dto;
import com.springbook.biz.domain.posts.Posts;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class PostsListResponseDto {
private Long id;
private String title;
private String author;
private LocalDateTime modifiedDate;
public PostsListResponseDto(Posts entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.author = entity.getAuthor();
this.modifiedDate = entity.getModifiedDate();
}
}
5) Controller
Model 객체를 이용하여 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장한다. 즉, index.mustache의 {{#posts}}에 결과물을 전달해준다.
package com.springbook.biz.web;
import com.springbook.biz.service.posts.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("posts", postsService.findAllDesc());
return "index";
}
@GetMapping("/posts/save")
public String postsSave() {
return "posts-save";
}
}
2. 수정 화면
1) 수정 화면
수정 화면을 위한 posts-update.mustache 파일을 생성한다.
어노테이션 및 코드 | 설명 |
{{posts.필드}} | 객체의 필드 접근할 때 사용한다. |
{{>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}}
2) index.js
btn-update 버튼을 선택할 경우, 동작할 기능에 대한 function을 추가한다.
var main = {
init : function () {
var _this = this;
$('#btn-save').on('click', function () {
_this.save();
});
$('#btn-update').on('click', function () {
_this.update();
});
},
save : function () {
var data = {
title: $('#title').val(),
author: $('#author').val(),
content: $('#content').val()
};
$.ajax({
type: 'POST',
url: '/api/v1/posts',
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));
});
},
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));
});
}
};
main.init();
3) index.mustache
전체 목록에서 수정 화면으로 이동하도록 한다.
{{#posts}}
<tr>
<td>{{id}}</td>
<td><a href="/posts/update/{{id}}">{{title}}</a></td>
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
4) Controller
수정 화면을 연결한다.
package com.springbook.biz.web;
import com.springbook.biz.service.posts.PostsService;
import com.springbook.biz.web.dto.PostsResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@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";
}
}
3. 삭제 화면
1) posts-update.mustache
수정 화면에서 삭제할 수 있도록한다.
{{>layout/header}}
... <!-- 추가될 내용 -->
<a href="/" role="button" class="btn btn-secondary">취소</a>
<button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>
<button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
</div>
</div>
{{>layout/footer}}
2) index.js
삭제 기능을 추가해준다.
var main = {
init : function () {
var _this = this;
$('#btn-save').on('click', function () {
_this.save();
});
$('#btn-update').on('click', function () {
_this.update();
});
$('#btn-delete').on('click', function () {
_this.delete();
});
},
save : function () {
var data = {
title: $('#title').val(),
author: $('#author').val(),
content: $('#content').val()
};
$.ajax({
type: 'POST',
url: '/api/v1/posts',
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));
});
},
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));
});
},
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));
});
}
};
main.init();
3) Service
삭제 API를 제작한다.
package com.springbook.biz.service.posts;
import com.springbook.biz.domain.posts.Posts;
import com.springbook.biz.domain.posts.PostsRepository;
import com.springbook.biz.web.dto.PostsListResponseDto;
import com.springbook.biz.web.dto.PostsResponseDto;
import com.springbook.biz.web.dto.PostsSaveRequestDto;
import com.springbook.biz.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class PostsService {
... // 추가될 내용
@Transactional
public void delete(Long id) {
Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
postsRepository.delete(posts);
}
}
4) Controller
화면에 대한 연결은 IndexController에서 처리하며, 순수 API를 처리하는 장소는 PostsApiController임을 조심해야한다.
package com.springbook.biz.web;
import com.springbook.biz.service.posts.PostsService;
import com.springbook.biz.web.dto.PostsResponseDto;
import com.springbook.biz.web.dto.PostsSaveRequestDto;
import com.springbook.biz.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
... // 추가될 내용
@DeleteMapping("/api/v1/posts/{id}")
public Long delete(@PathVariable Long id) {
postsService.delete(id);
return id;
}
}
5) 구현 화면
[참고] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스
728x90
'Spring > Spring Boot' 카테고리의 다른 글
[Spring Boot] 스프링 시큐리티, 구글 OAuth 2.0 로그인 (2) (0) | 2020.09.17 |
---|---|
[Spring Boot] 스프링 시큐리티, 구글 OAuth 2.0 로그인 (1) (0) | 2020.09.17 |
[Spring Boot] 게시물 화면 구성 (1) (0) | 2020.09.16 |
[Spring Boot] JPA Auditing 생성시간, 수정시간 (0) | 2020.09.16 |