이 글은 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 책을 참고하여 작성되었습니다.

1. 전체 조회 화면

첫 페이지에 글 목록을 나타내기 위해 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에 전달한다.

2. 게시글 수정 화면

미리 만들어둔 게시글 수정 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 - DELETE
  • url: '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로 넘겨준다.

3. 게시글 삭제 화면

앞서 작성했던 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;
    }
}

profile
여러가지를 시도하는 학생입니다

0개의 댓글