본문 바로가기

카테고리 없음

항해 week2 - timeline service 만들기

이번 시간에 사용할 index.html 입니다.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Timeline Service</title>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap" rel="stylesheet">

  <style>
        @import url(//spoqa.github.io/spoqa-han-sans/css/SpoqaHanSans-kr.css);

        body {
            margin: 0px;
        }

        .area-edit {
            display: none;
        }

        .wrap {
            width: 538px;
            margin: 10px auto;
        }

        #contents {
            width: 538px;
        }

        .area-write {
            position: relative;
            width: 538px;
        }

        .area-write img {
            cursor: pointer;
            position: absolute;
            width: 22.2px;
            height: 18.7px;
            bottom: 15px;
            right: 17px;
        }

        .background-header {
            position: fixed;
            z-index: -1;
            top: 0px;
            width: 100%;
            height: 428px;
            background-color: #339af0;
        }

        .background-body {
            position: fixed;
            z-index: -1;
            top: 428px;
            height: 100%;
            width: 100%;
            background-color: #dee2e6;
        }

        .header {
            margin-top: 50px;
        }

        .header h2 {
            /*font-family: 'Noto Sans KR', sans-serif;*/
            height: 33px;
            font-size: 42px;
            font-weight: 500;
            font-stretch: normal;
            font-style: normal;
            line-height: 0.79;
            letter-spacing: -0.5px;
            text-align: center;
            color: #ffffff;
        }

        .header p {
            margin: 40px auto;
            width: 217px;
            height: 48px;
            font-family: 'Noto Sans KR', sans-serif;
            font-size: 16px;
            font-weight: 500;
            font-stretch: normal;
            font-style: normal;
            line-height: 1.5;
            letter-spacing: -1.12px;
            text-align: center;
            color: #ffffff;
        }

        textarea.field {
            width: 502px !important;
            height: 146px;
            border-radius: 5px;
            background-color: #ffffff;
            border: none;
            padding: 18px;
            resize: none;
        }

        textarea.field::placeholder {
            width: 216px;
            height: 16px;
            font-family: 'Noto Sans KR', sans-serif;
            font-size: 16px;
            font-weight: normal;
            font-stretch: normal;
            font-style: normal;
            line-height: 1;
            letter-spacing: -0.96px;
            text-align: left;
            color: #868e96;
        }

        .card {
            width: 538px;
            border-radius: 5px;
            background-color: #ffffff;
            margin-bottom: 12px;
        }

        .card .metadata {
            position: relative;
            display: flex;
            font-family: 'Spoqa Han Sans';
            font-size: 11px;
            font-weight: normal;
            font-stretch: normal;
            font-style: normal;
            line-height: 1;
            letter-spacing: -0.77px;
            text-align: left;
            color: #adb5bd;
            height: 14px;
            padding: 10px 23px;
        }

        .card .metadata .date {

        }

        .card .metadata .username {
            margin-left: 20px;
        }

        .contents {
            padding: 0px 23px;
            word-wrap: break-word;
            word-break: break-all;
        }

        .contents div.edit {
            display: none;
        }

        .contents textarea.te-edit {
            border-right: none;
            border-top: none;
            border-left: none;
            resize: none;
            border-bottom: 1px solid #212529;
            width: 100%;
            font-family: 'Spoqa Han Sans';
        }

        .footer {
            position: relative;
            height: 40px;
        }

        .footer img.icon-start-edit {
            cursor: pointer;
            position: absolute;
            bottom: 14px;
            right: 55px;
            width: 18px;
            height: 18px;
        }

        .footer img.icon-end-edit {
            cursor: pointer;
            position: absolute;
            display: none;
            bottom: 14px;
            right: 55px;
            width: 20px;
            height: 15px;
        }

        .footer img.icon-delete {
            cursor: pointer;
            position: absolute;
            bottom: 12px;
            right: 19px;
            width: 14px;
            height: 18px;
        }

        #cards-box {
            margin-top: 12px;
        }
    </style>
  <script>
        // 미리 작성된 영역 - 수정하지 않으셔도 됩니다.
        // 사용자가 내용을 올바르게 입력하였는지 확인합니다.
        function isValidContents(contents) {
            if (contents == '') {
                alert('내용을 입력해주세요');
                return false;
            }
            if (contents.trim().length > 140) {
                alert('공백 포함 140자 이하로 입력해주세요');
                return false;
            }
            return true;
        }

        // 익명의 username을 만듭니다.
        function genRandomName(length) {
            let result = '';
            let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
            let charactersLength = characters.length;
            for (let i = 0; i < length; i++) {
                let number = Math.random() * charactersLength;
                let index = Math.floor(number);
                result += characters.charAt(index);
            }
            return result;
        }

       // 수정 버튼을 눌렀을 때, 기존 작성 내용을 textarea 에 전달합니다.
       // 숨길 버튼을 숨기고, 나타낼 버튼을 나타냅니다.
        function editPost(id) {
            showEdits(id);
            let contents = $(`#${id}-contents`).text().trim();
            $(`#${id}-textarea`).val(contents);
        }

        function showEdits(id) {
            $(`#${id}-editarea`).show();
            $(`#${id}-submit`).show();
            $(`#${id}-delete`).show();

            $(`#${id}-contents`).hide();
            $(`#${id}-edit`).hide();
        }

        function hideEdits(id) {
            $(`#${id}-editarea`).hide();
            $(`#${id}-submit`).hide();
            $(`#${id}-delete`).hide();

            $(`#${id}-contents`).show();
            $(`#${id}-edit`).show();
        }
        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // 여기서부터 코드를 작성해주시면 됩니다.

        $(document).ready(function () {
            // HTML 문서를 로드할 때마다 실행합니다.
            getMessages();
        })

        // 메모를 불러와서 보여줍니다.
        function getMessages() {
            // 1. 기존 메모 내용을 지웁니다.
            $('#cards-box').empty();
            // 2. 메모 목록을 불러와서 HTML로 붙입니다.
            $.ajax({
                type: 'GET',
                url: '/api/memos',
                success: function (response) {
                    console.log(response);
                    for (let i = 0; i < response.length; i++) {
                        let message = response[i];
                        let id = message['id'];
                        let username = message['username'];
                        let contents = message['contents'];
                        let modifiedAt = message['modifiedAt'];
                        addHTML(id, username, contents, modifiedAt);
                    }
                }
            })
        }

        // 메모 하나를 HTML로 만들어서 body 태그 내 원하는 곳에 붙입니다.
        function addHTML(id, username, contents, modifiedAt) {
            // 1. HTML 태그를 만듭니다.
            let tempHtml = `<div class="card">
                <!-- date/username 영역 -->
                <div class="metadata">
                    <div class="date">
                        ${modifiedAt}
                    </div>
                    <div id="${id}-username" class="username">
                        ${username}
                    </div>
                </div>
                <!-- contents 조회/수정 영역-->
                <div class="contents">
                    <div id="${id}-contents" class="text">
                        ${contents}
                    </div>
                    <div id="${id}-editarea" class="edit">
                        <textarea id="${id}-textarea" class="te-edit" name="" id="" cols="30" rows="5"></textarea>
                    </div>
                </div>
                <!-- 버튼 영역-->
                <div class="footer">
                    <img id="${id}-edit" class="icon-start-edit" src="images/edit.png" alt="" onclick="editPost('${id}')">
                    <img id="${id}-delete" class="icon-delete" src="images/delete.png" alt="" onclick="deleteOne('${id}')">
                    <img id="${id}-submit" class="icon-end-edit" src="images/done.png" alt="" onclick="submitEdit('${id}')">
                </div>
            </div>`;
            // 2. #cards-box 에 HTML을 붙인다.
            $('#cards-box').append(tempHtml);
        }

        // 메모를 생성합니다.
        function writePost() {
            // 1. 작성한 메모를 불러옵니다.
            let contents = $('#contents').val();
            // 2. 작성한 메모가 올바른지 isValidContents 함수를 통해 확인합니다.
            if (isValidContents(contents) == false) {
                return;
            }
            // 3. genRandomName 함수를 통해 익명의 username을 만듭니다.
            let username = genRandomName(10);
            // 4. 전달할 data JSON으로 만듭니다.
            let data = {'username': username, 'contents': contents};
            // 5. POST /api/memos 에 data를 전달합니다.
            $.ajax({
                type: "POST",
                url: "/api/memos",
                contentType: "application/json", // JSON 형식으로 전달함을 알리기
                data: JSON.stringify(data),
                success: function (response) {
                    alert('메시지가 성공적으로 작성되었습니다.');
                    window.location.reload();
                }
            });
        }

        // 메모를 수정합니다.
        function submitEdit(id) {
            // 1. 작성 대상 메모의 username과 contents 를 확인합니다.
            let username = $(`#${id}-username`).text().trim();
            let contents = $(`#${id}-textarea`).val().trim();
            // 2. 작성한 메모가 올바른지 isValidContents 함수를 통해 확인합니다.
            if (isValidContents(contents) == false) {
                return;
            }
            // 3. 전달할 data JSON으로 만듭니다.
            let data = {'username': username, 'contents': contents};
            // 4. PUT /api/memos/{id} 에 data를 전달합니다.
            $.ajax({
                type: "PUT",
                url: `/api/memos/${id}`,
                contentType: "application/json",
                data: JSON.stringify(data),
                success: function (response) {
                    alert('메시지 변경에 성공하였습니다.');
                    window.location.reload();
                }
            });
        }

        // 메모를 삭제합니다.
        function deleteOne(id) {
            // 1. DELETE /api/memos/{id} 에 요청해서 메모를 삭제합니다.
            console.log('delete')
            $.ajax({
                type: "DELETE",
                url: `/api/memos/${id}`,
                success: function (response) {
                    alert('메시지 삭제에 성공하였습니다.');
                    window.location.reload();
                }
            })
        }
    </script>
</head>

<body>
<div class="background-header">

</div>
<div class="background-body">

</div>
<div class="wrap">
  <div class="header">
    <h2>Timeline Service</h2>
    <p>
      공유하고 싶은 소식을 입력해주세요.
      24시간이 지난 뒤에는 사라집니다.
    </p>
  </div>
  <div class="area-write">
        <textarea class="field" placeholder="공유하고 싶은 소식을 입력해주세요" name="contents" id="contents" cols="30"
                  rows="10"></textarea>
    <!--            <button class="btn btn-danger" onclick="writePost()">작성하기</button>-->
    <img src="images/send.png" alt="" onclick="writePost()">
  </div>
  <div id="cards-box" class="area-read">
    <div class="card">
      <!-- date/username 영역 -->
      <div class="metadata">
        <div class="date">
          October 10, 2020
        </div>
        <div id="1-username" class="username">
          anonymous
        </div>
      </div>
      <!-- contents 조회/수정 영역-->
      <div class="contents">
        <div id="1-contents" class="text">
          dsafnkalfklewakflekelafkleajfkleafkldsankflenwaklfnekwlafneklwanfkelawnfkelanfkleanfklew
        </div>
        <div id="1-editarea" class="edit">
          <textarea id="1-textarea" class="te-edit" name="" id="" cols="30" rows="5"></textarea>
        </div>
      </div>
      <!-- 버튼 영역-->
      <div class="footer">
        <img id="1-edit" onclick="editPost('1')" class="icon-start-edit" src="images/edit.png" alt="">
        <img id="1-delete" onclick="deleteOne('1')" class="icon-delete" src="images/delete.png" alt="">
        <img id="1-submit" onclick="submitEdit('1')" class="icon-end-edit" src="images/done.png" alt="">
      </div>
    </div>
  </div>
</div>
</body>

</html>

 

백엔드를 위주로 공부하려고 하니 이번에는 만들어진 index.html을 기반으로 기능을 구현해 보겠습니다.

 

이번 시간에 만들 timeline service는 CRUD 기능이 있는 게시판의 형태입니다.

 

C: Create

submit (화살표) 버튼을 누르면 data가 서버에 저장될 수 있도록 구현해야합니다.

R: Read
page가 로딩될 때 DB에서 저장된 데이터를 들고 와 읽을 수 있어야 합니다.

U: Update

게시판에 저장한 내용을 수정할 수 있어야 합니다.

D: Delete
게시판에 저장한 내용을 삭제할 수 있어야 합니다.

 

 

Create : 공유하고 싶은 소식 저장


게시판에 공유할 내용을 저장해 봅시다.

 

web page를 구성하고 있는 index.html을 먼저 살펴보겠습니다.

 

submit 버튼을 누르면 writePost라는 기능이 실행되게 만들어져 있습니다.

<div class="area-write">
      <textarea class="field" placeholder="공유하고 싶은 소식을 입력해주세요" name="contents" id="contents" cols="30"
                rows="10"></textarea>
  <!--            <button class="btn btn-danger" onclick="writePost()">작성하기</button>-->
  <img src="images/send.png" alt="" onclick="writePost()">
</div>

writePost는 아래와 같은 기능입니다.

작성된 내용을 불러오고, username을 임의로 생성해서 backend로 전달하고 있습니다.

function writePost() {
    // 1. 작성한 메모를 불러옵니다.
    let contents = $('#contents').val();
    // 2. 작성한 메모가 올바른지 isValidContents 함수를 통해 확인합니다.
    if (isValidContents(contents) == false) {
        return;
    }
    // 3. genRandomName 함수를 통해 익명의 username을 만듭니다.
    let username = genRandomName(10);
    // 4. 전달할 data JSON으로 만듭니다.
    let data = {'username': username, 'contents': contents};
    // 5. POST /api/memos 에 data를 전달합니다.
    $.ajax({
        type: "POST",
        url: "/api/memos",
        contentType: "application/json", // JSON 형식으로 전달함을 알리기
        data: JSON.stringify(data),
        success: function (response) {
            alert('메시지가 성공적으로 작성되었습니다.');
            window.location.reload();
        }
    });
}

이 기능을 어떻게 받아볼까요?

이 기능을 받은 domain을 먼저 생성해줍시다.

domain의 이름은 간단히 Memo로 하겠습니다.

Memo에는 username(사용자 이름), content(메모 내용), createdAt(작성 날짜), modifiedAt(수정 날짜) 등을 요소로 받아보겠습니다.

 

Memo 클래스에는 createdAt, modifiedAt이라는 멤버 변수가 존재합니다.

현재는 domain에 시간을 저장하는 멤버 변수를 추가해줘도 되지만

이러한 생성 시간과 수정 시간은 다른 클래스에서도 자주 사용할 수 있는 멤버 변수입니다.

코드의 중복을 막기 위해서는 어떻게 해야 할까요??

 

JPA에서는 바로 이러한 중복을 막기 위해 Audit라는 기능을 제공하고 있습니다.

 Audit은 감시하다, 감사하다라는 뜻으로 Spring Data JPA에서 시간에 대해서 자동으로 값을 넣어주는 기능입니다. 

 

spring framework는 아래 4가지의 어노테이션을 제공하고 있습니다.

@MappedSuperclass 부모 클래스임을 명시하며 상속받는 클래스에서 멤버변수가 컬럼이 되도록 합니다
단순히 매핑정보를 상속할 목적으로만 사용됩니다.
@EntityListeners(AuditingEntityListener.class) 해당 클래스에 Auditing 기능을 포함
@CreatedDate Entity가 생성되어 저장될 때 시간이 자동 저장
@LastModifiedDate 조회한 Entity의 값을 변경할 때 시간이 자동 저장

 

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeStamped {

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

JPA Auditing을 활성화 하기 위해 최상단 root 클래스에 @EnableJpaAuditing 어노테이션을 추가해 주었습니다.

@EnableJpaAuditing
@SpringBootApplication
public class Week01Application {

   public static void main(String[] args) {
      SpringApplication.run(Week01Application.class, args);
   }

}

 

Dto 클래스입니다.

Dto클래스는 왜 사용할까요??

엔티티는 핵심 속성과 로직이 들어있고, 실제 DB와 매칭이 되어있는 중요한 클래스입니다.

그렇기 때문에 엔티티가 getter와 setter를 갖게 된다면, controller와 같은 비즈니스 로직과 크게 상관없는 곳에서 자원의 속성이 실수로라도 변경될 수 있다.

또한 엔티티를 UI계층에 노출하는 것은 테이블 설계를 화면에 공개하는 것이나 다름없기 때문에 보안상으로도 바람직하지 못한 구조가 됩니다.

따라서 엔티티의 내부 구현을 캡슐화하고 UI계층에 노출시키지 않아야하는 것은 충분히 데이터 전달 역할로 DTO를 사용해야 할 이유로 볼 수 있다.

 

dto에 대한 내용은 아래 Reference에서 더 자세하게 확인할 수 있습니다.

이처럼 중요한 엔티티를 캡슐화 할 수 있기 때문에 dto를 이용하여 비지니스 구조가 아닌 곳에서는 대신 정보를 전달해줄 매개체 역할을 담당할 클래스가 필요하고 이를 Dto라고 부르고 있습니다.

 

우리의 Dto를 아래에서 확인할 수 있습니다.

UI계층에서는 username과 contents만을 제공하면 되기 때문에 멤버 변수는 두개만 가지고 있습니다.

@Getter
public class MemoRequestDto {

    private String username;

    private String contents;
}

 

Dto를 써가며 그렇게 까지 보호하려 했던 Memo entity입니다.

dto를 이용하여 객체를 생성할 수 있도록 생성자를 구현해 주었습니다.

@Column(nullable =false) 라는 annotation을 추가하면, db가 생성될 때 해당 column은 not null이라는 제약조건이 붙습니다.

 

@Getter
@Entity
@NoArgsConstructor
public class Memo extends TimeStamped {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "memo_id")
    private Long id;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String contents;

    public Memo(MemoRequestDto memoRequestDto) {
        this.username = memoRequestDto.getUsername();
        this.contents = memoRequestDto.getContents();
    }
}

아래에서 memo가 생성될 때 나가는 query를 눈으로 확인해보면, contents와 username에는 not null 이라는 제약조건이 붙은 것을 확인할 수 있습니다.

이로서 해당 칼럼에 null 값은 넣을 수가 없으니, 한 숨 돌려도 될 것 같습니다.

Hibernate: create table memo (
	memo_id bigint not null, 
	created_at timestamp, 
	modified_at timestamp, 
	contents varchar(255) not null, 
	username varchar(255) not null, 
	primary key (memo_id)
)

스프링부트에서는 Entity의 기본적인 CRUD가 가능하도록 JpaRepository 인터페이스를 제공하고 있습니다.

Spring Data JPA에서 제공하는 JpaRepository 인터페이스를 상속하기만 해도 되며,

인터페이스에 따로 @Repository등의 어노테이션을 추가할 필요가 없습니다.

 

이렇게 편할수가...

public interface MemoRepository extends JpaRepository<Memo, Integer> {
}

memoRepository를 DI주입한 다음, Dto에 있는 내용을 기반으로 memo 클래스를 생성합니다.

생성한 memo 클래스를 memoRepository.save에 넣어주기만 하면! 완성!

 

참 쉽죠?

@RequiredArgsConstructor
@RestController
public class MemoController {

    private final MemoRepository memoRepository;

    @PostMapping("/api/memos")
    public Memo save(@RequestBody MemoRequestDto memoRequestDto) {
        Memo memo = new Memo(memoRequestDto);
        return memoRepository.save(memo);
    }
}

 

Read :  db에 저장된 게시글을 불러와 화면에 rendering


앞에서 저장하는 기능을 구현하였습니다.

이번에는 저장한 메세지를 확인해 보는 시간을 가져보겠습니다.

 

아래는 페이지가 실행될 때 백엔드에 Get요청을 하고 Get요청을 받은 결과물을 바탕으로 게시글 html 을 완성하는 코드입니다.

$(document).ready(function () {
    // HTML 문서를 로드할 때마다 실행합니다.
    getMessages();
})

// 메모를 불러와서 보여줍니다.
function getMessages() {
    // 1. 기존 메모 내용을 지웁니다.
    $('#cards-box').empty();
    // 2. 메모 목록을 불러와서 HTML로 붙입니다.
    $.ajax({
        type: 'GET',
        url: '/api/memos',
        success: function (response) {
            console.log(response);
            for (let i = 0; i < response.length; i++) {
                let message = response[i];
                let id = message['id'];
                let username = message['username'];
                let contents = message['contents'];
                let modifiedAt = message['modifiedAt'];
                addHTML(id, username, contents, modifiedAt);
            }
        }
    })
}

자 그러면 다시 Controller에서 db에 저장된 게시글들을 불러오도록 하겠습니다.

getMemos 메서드를 확인해 봅시다.

너무 간단하죠? findAll 값을 반환해 주기만 하면 됩니다.

 

@RequiredArgsConstructor
@RestController
public class MemoController {

    private final MemoRepository memoRepository;

    @PostMapping("/api/memos")
    public Memo save(@RequestBody MemoRequestDto memoRequestDto) {
        Memo memo = new Memo(memoRequestDto);
        System.out.println("save memo");
        return memoRepository.save(memo);
    }

    @GetMapping("/api/memos")
    public List<Memo> getMemos() {
        return memoRepository.findAll();
    }
}

 

Update: 게시글 수정하기


게시글을 불러오는 단계가 너무 쉬워서 힘이 빠졌나요??

괜찮습니다. 수정은 그렇게 쉽지 않을 테니까요

 

html에서 수정 버튼을 누르면 editPost 함수가 실행됩니다.

<div class="footer">
  <img id="1-edit" onclick="editPost('1')" class="icon-start-edit" src="images/edit.png" alt="">
  <img id="1-delete" onclick="deleteOne('1')" class="icon-delete" src="images/delete.png" alt="">
  <img id="1-submit" onclick="submitEdit('1')" class="icon-end-edit" src="images/done.png" alt="">
</div>

수정 버튼을 누르게 되면

수정한 내용을 가져오고 수정할(editarea) 내용을 적을 란이 보입니다.

수정을 완료한 뒤 submit 버튼을 누르게 되면 submitEdit 함수가 실행된다.

// 수정 버튼을 눌렀을 때, 기존 작성 내용을 textarea 에 전달합니다.
// 숨길 버튼을 숨기고, 나타낼 버튼을 나타냅니다.
 function editPost(id) {
     showEdits(id);
     let contents = $(`#${id}-contents`).text().trim();
     $(`#${id}-textarea`).val(contents);
 }

 function showEdits(id) {
     $(`#${id}-editarea`).show();
     $(`#${id}-submit`).show();
     $(`#${id}-delete`).show();

     $(`#${id}-contents`).hide();
     $(`#${id}-edit`).hide();
 }

submitEdit 함수는 작성된 메모의 username과 contents를 들고와서 data를 PUT으로 전달해줍니다.

// 메모를 수정합니다.
function submitEdit(id) {
    // 1. 작성 대상 메모의 username과 contents 를 확인합니다.
    let username = $(`#${id}-username`).text().trim();
    let contents = $(`#${id}-textarea`).val().trim();
    // 2. 작성한 메모가 올바른지 isValidContents 함수를 통해 확인합니다.
    if (isValidContents(contents) == false) {
        return;
    }
    // 3. 전달할 data JSON으로 만듭니다.
    let data = {'username': username, 'contents': contents};
    // 4. PUT /api/memos/{id} 에 data를 전달합니다.
    $.ajax({
        type: "PUT",
        url: `/api/memos/${id}`,
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function (response) {
            alert('메시지 변경에 성공하였습니다.');
            window.location.reload();
        }
    });
}

 

자 이제 본격적으로 수정해 봅시다.

updateMemo 메서드를 확인하면 되겠습니다.

 

@RequestParam 어노테이션을 이용하면, 인자값을 바로 받아올 수 있습니다.

@Requestbody 어노테이션을 이용하면 자바 객체에 값이 mapping이 되어 편리하게 사용할 수 있습니다.

 

@Requestbody를 찾아보던 중, 해당 어노테이션은 기본 생성자와 getter를 열어 놓아야 사용 할 수 있다고 하여서, 매개 변수 생성자와  getter만 열어놓고 테스트를 해보았는데 오류가 나타나지 않았다. 추가적인 확인이 필요한 상황이다.

 

@RequiredArgsConstructor
@RestController
public class MemoController {

    private final MemoRepository memoRepository;
    private final MemoService memoService;

    @PostMapping("/api/memos")
    public Memo save(@RequestBody MemoRequestDto memoRequestDto) {
        Memo memo = new Memo(memoRequestDto);
        System.out.println("save memo");
        return memoRepository.save(memo);
    }

    @GetMapping("/api/memos")
    public List<Memo> getMemos() {
        return memoRepository.findAll();
    }

    @PutMapping("/api/memos/{id}")
    public void updateMemo(@RequestParam Long id, @RequestBody MemoRequestDto memoRequestDto) {
        memoService.update(id, memoRequestDto);
    }
}

update 메서드를 생성하였다.

 

@Tranjactional

SQL C,U,D 를 할 때 메소드 위에 Tranjactional annotation을 붙여주면 된다.

트랜잭션이란 데이터베이스의 상태를 변경하는 작업 또는 한번에 수행되어야 하는 연산들을 의미한다.begin, commit 을 자동으로 수행해준다.

 

더욱 자세한 내용들은 아래 Reference 7, 8, 9를 참고하도록 하자.

@RequiredArgsConstructor
@Service
public class MemoService {

    private final MemoRepository memoRepository;

    @Transactional
    public Long update(Long id, MemoRequestDto memoRequestDto) {
        // database에서 id가 일치하는 memo 정보를 들고오고
        // memo 내용을 수정해서 다시 저장한다
        Memo memo = memoRepository.findById(id).orElseThrow(
            () -> new NullPointerException("id가 존재하지 않습니다.")
        );

        memo.update(memoRequestDto);
        return memo.getId();
    }
}

update 메서드를 추가하였습니다.

memo 클래스에 수정하는 메서드를 추가함으로서 관리 한층 더 용이하게 만들었습니다.

@Getter
@Entity
@NoArgsConstructor
public class Memo extends TimeStamped {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "memo_id")
    private Long id;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String contents;

    public Memo(MemoRequestDto memoRequestDto) {
        this.username = memoRequestDto.getUsername();
        this.contents = memoRequestDto.getContents();
    }

    public void update(MemoRequestDto memoRequestDto) {
        contents = memoRequestDto.getContents();
    }
}

 

Delete : 게시글 삭제


대망의 게시글 삭제 부분입니다.

삭제 버튼을 누르게 되면 deleteOne 메서드가 작동하는데

deleteOne 메서드는 DELETE 요청을 백엔드로 보내는 역할을 하고 있습니다.

<div class="footer">
  <img id="1-edit" onclick="editPost('1')" class="icon-start-edit" src="images/edit.png" alt="">
  <img id="1-delete" onclick="deleteOne('1')" class="icon-delete" src="images/delete.png" alt="">
  <img id="1-submit" onclick="submitEdit('1')" class="icon-end-edit" src="images/done.png" alt="">
</div>
// 메모를 삭제합니다.
function deleteOne(id) {
    // 1. DELETE /api/memos/{id} 에 요청해서 메모를 삭제합니다.
    console.log('delete')
    $.ajax({
        type: "DELETE",
        url: `/api/memos/${id}`,
        success: function (response) {
            alert('메시지 삭제에 성공하였습니다.');
            window.location.reload();
        }
    })
}

delete를 하는 방법도 매우 간단합니다.

memoRepository.deleteById를 하기만 하면 게시글 정보가 데이터 베이스에서 삭제가 되버립니다.

@RequiredArgsConstructor
@RestController
public class MemoController {

    private final MemoRepository memoRepository;
    private final MemoService memoService;

    @PostMapping("/api/memos")
    public Memo save(@RequestBody MemoRequestDto memoRequestDto) {
        Memo memo = new Memo(memoRequestDto);
        System.out.println("save memo");
        return memoRepository.save(memo);
    }

    @GetMapping("/api/memos")
    public List<Memo> getMemos() {
       return memoRepository.findAll();
    }

    @PutMapping("/api/memos/{id}")
    public void updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto memoRequestDto) {
        memoService.update(id, memoRequestDto);
    }

    @DeleteMapping("/api/memos/{id}")
    public void deleteMemo(@PathVariable Long id) {
        memoRepository.deleteById(id);
    }
}

24시간 내에 수정된 게시글만 보기

 


JpaRepository에서는 다양한 메서드가 존재합니다.

공식 홈페이지에서 더욱 자세한 내용을 살펴볼 수 있습니다. (Reference 10)

이를 이용하면 24시간 내에 수정한 게시글을 내림차순으로 살펴볼 수 있겠네요!

@Repository
public interface MemoRepository extends JpaRepository<Memo, Long> {
//    List<Memo> findAllByOrderByModifiedAtDesc();
    List<Memo> findAllByModifiedAtBetweenOrderByModifiedAtDesc(LocalDateTime start, LocalDateTime end);
}

 

@RequiredArgsConstructor
@RestController
public class MemoController {

    private final MemoRepository memoRepository;
    private final MemoService memoService;

    @PostMapping("/api/memos")
    public Memo save(@RequestBody MemoRequestDto memoRequestDto) {
        Memo memo = new Memo(memoRequestDto);
        System.out.println("save memo");
        return memoRepository.save(memo);
    }

    @GetMapping("/api/memos")
    public List<Memo> getMemos() {
//        return memoRepository.findAll();
        LocalDateTime start = LocalDateTime.now().minusDays(1);
        LocalDateTime end = LocalDateTime.now();
        return memoRepository.findAllByModifiedAtBetweenOrderByModifiedAtDesc(start, end);
    }

    @PutMapping("/api/memos/{id}")
    public void updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto memoRequestDto) {
        memoService.update(id, memoRequestDto);
    }

    @DeleteMapping("/api/memos/{id}")
    public void deleteMemo(@PathVariable Long id) {
        memoRepository.deleteById(id);
    }
}

 

Reference

1. JPA Auditing

https://thalals.tistory.com/220

 

[Spring] JPA Auditting 과 TimeStamp Class (테이블 시간 기

Spring이든 뭐든 프로젝트를 하다보면, 테이블의 생성기간과, 수정시간이 필요할 때가 있다. 오늘은 이 테이블에 생성, 수정시간을 기록할 수 있는 컬럼을 효율적으로 작성하는 법에 대해 공부해

thalals.tistory.com

2. JPA Auditing

https://webcoding-start.tistory.com/53

 

JPA Auditing 기능이란?

JPA Auditing이란? Java에서 ORM 기술인 JPA를 사용하여 도메인을 관계형 데이터베이스 테이블에 매핑할 때 공통적으로 도메인들이 가지고 있는 필드나 컬럼들이 존재합니다. 대표적으로 생성일자, 수

webcoding-start.tistory.com

 

3. DTO는 왜 사용할까?

https://tecoble.techcourse.co.kr/post/2020-08-31-dto-vs-entity/

 

요청과 응답으로 엔티티(Entity) 대신 DTO를 사용하자

tecoble.techcourse.co.kr

 

4. 생성과 제약조건 매핑

https://kafcamus.tistory.com/15

 

[JPA] nullable=false와 @NotNull 비교, Hibernate Validation

오늘은 다음의 고민 때문에 글을 작성하게 되었다. JPA에서 DDL을 자동으로 생성할 수 있는데, 이 때 not null 옵션은 어떻게 붙이나? JPA의 엔티티 객체에 @NotNull 검증 어노테이션을 주면 어떻게 되나

kafcamus.tistory.com

 

5. request body

https://velog.io/@conatuseus/RequestBody%EC%97%90-%EA%B8%B0%EB%B3%B8-%EC%83%9D%EC%84%B1%EC%9E%90%EB%8A%94-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80

 

@RequestBody에 왜 기본 생성자는 필요하고, Setter는 필요 없을까? #1

Springboot로 토이 프로젝트를 진행중 Request DTO(requestBody로 오는)에 @NoArgsConstructor를 빠뜨려서 에러가 났다. (습관적으로 적어오던 어노테이션...) 그런데 @RequestBody로 넘어오는 객체에는 기본 생성자

velog.io

 

6. request body

https://velog.io/@conatuseus/RequestBody%EC%97%90-%EC%99%9C-%EA%B8%B0%EB%B3%B8-%EC%83%9D%EC%84%B1%EC%9E%90%EB%8A%94-%ED%95%84%EC%9A%94%ED%95%98%EA%B3%A0-Setter%EB%8A%94-%ED%95%84%EC%9A%94-%EC%97%86%EC%9D%84%EA%B9%8C-3-idnrafiw

 

@RequestBody에 왜 기본 생성자는 필요하고, Setter는 필요 없을까? #3

이전 글에서는 RestController에서 @RequestBody 바인딩을 Jackson 라이브러리의 ObjectMapper가 하는 것을 확인했습니다.그리고 RequestBody를 생성할 때, DTO가 Property기반이 아니거나 Delegate를 한 상태가 아니라

velog.io

 

7. Transactional

https://kafcamus.tistory.com/30

 

@Transactional 어노테이션의 이해

나는 보통 서비스 코드에 @Transactional 어노테이션을 활용해준다. 그런데 사실 뜻도 잘 모르고 좋다고 그래서 쓴거라...지나고 보니 정확히 설명하기가 어려웠다. 그런고로, 해당 어노테이션의 작

kafcamus.tistory.com

 

8. Tranjactional

https://velog.io/@kdhyo/JavaTransactional-Annotation-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-26her30h

 

9.  Tranjactional

https://www.baeldung.com/transaction-configuration-with-jpa-and-spring

 

10. JPA 공식 홈페이지에서 살펴보기

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io