Spring

TO-DO 앱 만들어보기 - 할 일 추가 기능 구현하기

몰라모르겠어요 2022. 8. 17. 15:43

이전까지는 GET을 통해 모든 투두 데이터를 가져오는 것까지 해봤다.

이제 추가하는 기능을 구현해보겠다.

 

Spring Boot(Backend)

백엔드 개발 방법에는 Test-Driven Development라고 요즘 많이 쓰는 TDD 방식이 있다.

TDD란 클래스와 메소드의 껍데기를 정의하고, Unit Tests를 작성해 테스트를 먼저 하고 그에 따라 클래스와 메소드 내부를 구현하는 방식이다. 나는 아직 이 방식에 익숙치 않아서 다음번에 기회가 된다면 해보겠다.

 

  • ToDoItemService
public ToDoItem update(final ToDoItem toDoItem){
         
    if(toDoItem == null){
        throw new NullPointerException("To Do Item can not be null!");
    }

    final ToDoItem original = toDoItemRepository.findById(toDoItem.getId())
            .orElseThrow(() -> new RuntimeException("Entity Not Found"));

    original.setTitle(toDoItem.getTitle());
    original.setDone(toDoItem.isDone());

    return toDoItemRepository.save(original);
}

서비스에 위 메서드를 추가해준다.

수정할 내용이 담긴 toDoItem을 받고 원래 데이터인 original을 찾아 수정할 내용으로 변경해준다.

 

  • ToDoItemController
@RequestMapping(method = RequestMethod.PUT)
public @ResponseBody ToDoItemResponse update(@RequestBody final ToDoItemRequest toDoItemRequest){
    List<String>  errors = new ArrayList<>();
    ToDoItem toDoItem = ToDoItemAdapter.toToItem(toDoItemRequest);

    try{
        toDoItem = toDoItemService.update(toDoItem);
    }catch (final Exception e){
        errors.add(e.getMessage());
    }

    return ToDoItemAdapter.toDoItemResponse(toDoItem, errors);
}

서비스의 update 만든 후 컨트롤러에도 추가해준다.

여기서 확인해야 할 부분은 update는 id를 가지고 original을 찾는다. update 부분에서 ToDoItemRequest을 받아서 어댑터를 이용해 ToDoItem으로 변환하는데 이 때 id가 제대로 가져오는지 확인해보기 위해 어댑터의 코드를 확인해보겠다.

 

  • ToDoItemAdapter

헉..id가 빠져있다 추가해줘야 한다.

 

이렇게 하고 CORS도 다시 설정해줘야 한다.

이전에는 GET 할 때 리소스 거부 에러가 일어났는데 origin을 추가해줌으로써 해결해줬다. 그런데 GET 외에 다양한 리퀘스트 메서드를 이용하기 위해서는 allowedMethods를 추가해줘야 한다.

 

  • WebConfig

이렇게 수정해주고 스프링 부트 앱을 실행해본다.

에러나지 않았으니 프론트엔드를 개발하겠다.

 

Node.js/Vue.js(Frontend)

이제 axios를 이용해 POST와 PUT 메소드를 call하는 메소드를 작성하겠다.

1. 추가 버튼을 누르면 POST call

2. 체크박스를 클릭하면 PUT call

3. 체크된 투두 아이템은 회색과 strikethrough를 적용한다.

 

  • Todo.vue
<script>
import axios from 'axios'

let baseUrl = "http://127.0.0.1:8081/todo/"

export default{
  name: 'todo-items',
  data: () => {
    return{
      todoItems: [], //toDoItems를 빈 리스트로 초기화
      newToDoItemReqeust: {}
    }
  },
  methods: {
    initTodoList: function() {
      let vm = this

      axios.get(baseUrl)
        .then(response => {
          vm.todoItems = response.data.map(r => r.data)
        })
        .catch(e => {
          console.log('데이터 초기화 중 error : ', e)
        })
    },
    createTodo: function(event) {
      event.preventDefault()
      let vm = this

      if(!vm.newToDoItemReqeust.title) return
      axios.post(baseUrl, vm.newToDoItemReqeust)
        .then(() => {
          vm.initTodoList()
          vm.newToDoItemReqeust = {}
        })
        .catch(error => {
          console.log('데이터 추가 중 error : ', error)
        })
    },
    markDone: function(todoItem) {
      if(!todoItem) return
      
      let vm = this
      todoItem.done = !todoItem.done
      axios.put(baseUrl, todoItem)
        .then(() => {
          vm.initTodoList()
        })
        .catch(error => {
          console.log('데이터 수정 중 error : ', error)
        })
    }
  },
  created () {
    this.initTodoList()
  }
}
</script>

처음에 initTodoList를 통해 초기화를 해주고 creaetTodo는 추가버튼을 누르면 POST 통신, markDone은 체크박스를 클릭한 후 PUT 통신을 의미한다. post나 put은 response를 어디서 이용하지 않으므로 ()로 해줬다. 그러지 않고 response를 선언해주면 esLint가 오류를 보낸다.

그리고 반복적으로 쓰는 url을  baseUrl로 지정해주었다.

 

이렇게 메서드를 만들어줬으면 화면에 보이는 템플릿과 연결해주어야 한다. 그리고 아이템도 넘겨줘야 한다.

 

  • Todo.vue
<template>
  <div class="todo">
    <b-card 
    header="오늘 해야 할 일"
    style="max-width: 40rem; margin: auto; margin-top: 10vh; text-align: left;"
    class="mb-2"
    border-variant="info">
    
      <b-form-group id="to-do-input">
        <b-container fluid>
          <b-row class="my-1">
            <b-col sm="10">
              <b-form-input v-model="newToDoItemReqeust.title" type="text" placeholder="새 할 일 적으세요." @keyup.enter="createTodo"/>
            </b-col>
            <b-col sm="2">
              <b-button variant="outline-primary" v-on:click="createTodo">추가</b-button>
            </b-col>
          </b-row>
        </b-container>
      </b-form-group>

      <b-list-group v-if="todoItems && todoItems.length">
        <b-list-group-item
          v-for="todoItem of todoItems"
          v-bind:data="todoItem.title"
          v-bind:key="todoItem.id" style="display: flex;">
          <b-form-checkbox
          v-model="todoItem.done"
          v-on:click="markDone(todoItem)">
          </b-form-checkbox>
          <span v-if="todoItem.done" style="text-decoration: line-through; color:#D3D3D3;">
          {{todoItem.title}}</span>
          <span v-else>{{todoItem.title}}</span>
        </b-list-group-item>
      </b-list-group>
    </b-card>
  </div>
</template>

일단 이벤트를 만들어 연결시켜주고 모델을 통해 데이터를 전달해주도록 했다. 중간중간 조금씩 바뀐 부분이 있다.

이제 체크박스를 눌러서 실행완료가 되면 아이템을 회색처리하고 줄긋기가 되도록 표시하는 것을 추가해보겠다.

처음에 v-on:change를 썼는데, 자꾸 두 번 클릭해야 done 값이 변경되었다. 알아보니 v-on:change는 값이 변경됨을 감지 후 동작한다고 했다. v-on:click으로 바꾸면 클릭 시 바로 markDone이 실행되도록 바꾸었더니, 원하는대로 동작했다.

그리고 만약 done이라면 style을 적용해 회색과 줄긋기 처리를 해주고 아니라면 style 적용을 없애는 코드도 추가했다.

 

그리고 테스트하다가 크롬 오류가 있어서 사파리에서 테스트를 했다.

크롬에서 콘솔 데이터가 자꾸 안떠서 사파리에서 했더니 잘 보였다.

 

이렇게 하면 기본적인 투두 앱이 만들어졌다.

 

 

추가로 삭제 API도 만들었다. 일단 포스트맨으로 테스트 해줬다.

 

  • ToDoItemService
public ToDoItem delete(final String id){
    final ToDoItem toDoItem = toDoItemRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("Entity Not Found"));

    if(toDoItem == null) {
        throw new NullPointerException("To Do Item can not be null!");
    }
    toDoItemRepository.deleteById(id);

    return  toDoItem;//확인용으로 돌려주기
}

 

  • ToDoItemController
@RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
@ResponseBody
public ToDoItemResponse delete(@PathVariable(value = "id") String id){
    List<String> errors = new ArrayList<>();
    ToDoItem toDoItem = null;

    try{
        toDoItem = toDoItemService.delete(id);
    }catch(final Exception e){
        errors.add(e.getMessage());
    }

    return ToDoItemAdapter.toDoItemResponse(toDoItem, errors);
}

그리고 추가 버튼이 버벅이는 거 같은데 나중에 다시 봐야겠다.

시간이 되거나, 공부하기 싫을 때 조금 더 리팩토링하며 발전시켜볼거다!