Project Memo Part 2 (2. 적용)

박영준·2022년 11월 29일
0

Java

목록 보기
11/110

전체적인 패키지 및 파일 생성


1. 프로젝트 생성


2. API 설계하기 (CRUD)

기능MethodURLReturn
메인 페이지GET/index.html
메모 생성하기POST/api/memosMemo
메모 조회하기GET/api/memosList
메모 변경하기PUT/api/memos/{id}Long
메모 삭제하기DELETE/api/memos/{id}Long

3. 메모장 프로젝트 Client 구축

  1. src > main > resources > static 에 images 폴더 안에 사진 4장 넣기
  2. src > main > resources > templates 에 아래 index.html을 만들기

1. index.html

<!--index.html은 Client이다.-->
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<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">
  <script>
    $(document).ready(function () {
      // HTML 문서를 로드할 때마다 실행합니다.
      getMessages();
    })

    //메모 조회하기
    function getMessages() {
      // 1. 기존 메모 내용을 지웁니다.
      $('#cards-box').empty();

      // 2. 메모 목록을 불러와서 HTML로 붙입니다.
      $.ajax({
        type: "GET",    //GET 방식
        url: "/api/memos",    //url은 /api/memos
        data: {},
        success: function (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) {
      let tempHtml = makeMessage(id, username, contents, modifiedAt);
      $('#cards-box').append(tempHtml);
    }

    function makeMessage(id, username, contents, modifiedAt) {
      return `<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=""token interpolation">${id}')">
                            <img id="${id}-delete" class="icon-delete" src="/images/delete.png" alt=""token interpolation">${id}')">
                            <img id="${id}-submit" class="icon-end-edit" src="/images/done.png" alt=""token interpolation">${id}')">
                        </div>
                    </div>`;
    }

    // 사용자가 내용을 올바르게 입력하였는지 확인합니다.
    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;
    }

    //메모 생성하기
    function writePost() {      //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};

      $.ajax({
        type: "POST",     //POST 방식
        url: "/api/memos",    ///api/memos로 전달할 것이다
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function (response) {
          alert('메시지가 성공적으로 작성되었습니다.');
          window.location.reload();
        }
      });
    }

    // 수정 버튼을 눌렀을 때, 기존 작성 내용을 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 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({             //변경된 데이터와 어떤 메모를 변경했는지 알려주는 Id값을 PathVariable 형식으로 요청함. 요청 방법은 PUT 메소드
        type: "PUT",            //ajax 요청을 받는 서버 부분 필요 --> MemoController.java 로 이용해서 만들기
        url: `/api/memos/${id}`,
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function (response) {
          alert('메시지 변경에 성공하였습니다.');
          window.location.reload();
        }
      });
    }

    //메모 삭제하기
    function deleteOne(id) {        //MemoController.java에서 이걸 받는 서버 만들기
      $.ajax({
        type: "DELETE",     //DELETE 메소드 타입
        url: `/api/memos/${id}`,      //url은 PathVariable 형식으로 id를 남김
        success: function (response) {
          alert('메시지 삭제에 성공하였습니다.');
          window.location.reload();
        }
      })
    }

  </script>

  <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>
</head>

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

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

</div>
<div class="wrap">
  <div class="header">
    <h2>Timeline Service</h2>
    <p>
      공유하고 싶은 소식을 입력하세요.
    </p>
  </div>
  <div class="area-write">
        <textarea class="field" placeholder="공유하고 싶은 소식을 입력하세요." name="contents" th:id="contents" cols="30"
                  rows="10"></textarea>
    <img src="/images/send.png" alt="" onclick="writePost()">
  </div>
  <div id="cards-box" class="area-read">

  </div>
</div>
</body>

</html>

2. Memo.java

//테이블 생성
package com.sparta.hanghaememo.entity;

//주의! import org.springframework.data.annotation.Id;는 오류를 발생시킴 --> import javax.persistence.Id; 으로 수정하기
import com.sparta.hanghaememo.dto.MemoRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor
public class Memo extends Timestamped {         //Timestamped에서 createdAt, modifiedAt 컬럼 2개는 Memo클래스에 상속이 되어서 사용됨

    //PK인 ID, username, contents 필드값 3개를 가진다
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String contents;

    //메모 생성하기
    public Memo(MemoRequestDto requestDto) {        //MemoService.java 에서 객체 Memo의 값을 넣어줄 생성자 requestDto를 만듦
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
    }

    //메모 변경하기, 수정하기
    public void update(MemoRequestDto requestDto) {     //MemoService.java에 있던 update의 메소드를 만들기 위해 --> Memo Entity 안에 만들기!
        this.username = requestDto.getUsername();       //Username, Contents를 가져온 데이터를 수정
        this.contents = requestDto.getContents();
    }



}

3. Timestamped.java

package com.sparta.hanghaememo.entity;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {

    //createdAt, modifiedAt 컬럼 2개를 가진다
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

4. MemoRepository.java

//인터페이스로 생성
//repository
package com.sparta.hanghaememo.repository;

import com.sparta.hanghaememo.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

//메모 생성하기
public interface MemoRepository extends JpaRepository<Memo, Long> {     //JpaRepository의 상속을 받아서 데이터베이스와 연결. Memo: 테이블 명
    //메모 조회하기
    //MemoService.java 에서 memoRepository에 연결해서 데이터를 가져오기 위함
    List<Memo> findAllByOrderByModifiedAtDesc();        //List: 반환값, findAllByOrderByModifiedAtDesc: 수정일자기준(ByOrderByModifiedAt)으로 내림차순정렬(Desc)
}

5. application.properties

//h2 연결을 위한 설정
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
spring.datasource.username=sa
spring.datasource.password=

spring.thymeleaf.cache=false

h2 데이터베이스 접속
localhost:8080/h2-console로 들어가면, h2데이터베이스 접속 가능

6. hanghaememoApplication.java

package com.sparta.hanghaememo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing      //@EnableJpaAuditing 추가
@SpringBootApplication
public class HanghaememoApplication {

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

}

자동으로 시간을 매핑하여 데이터베이스의 테이블에 넣어주게 됩니다.

7. MemoController.java

//controller
package com.sparta.hanghaememo.controller;

import com.sparta.hanghaememo.dto.MemoRequestDto;
import com.sparta.hanghaememo.entity.Memo;
import com.sparta.hanghaememo.service.MemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

//메인 페이지
@RestController
@RequiredArgsConstructor
public class MemoController {

    //메모 생성하기
    private final MemoService memoService;      //MemoService에 연결

    //메인 페이지
    @GetMapping("/")
    public ModelAndView home() {        //객체 ModelAndView: String 반환값을 줬을 때, 이것을 templates directory에 있는 html 파일이름을 따라서 반환(여기서는 index)
        return new ModelAndView("index");   //index: templates에 반환할 tml 파일 이름 --> templates에 있는 index.html을 반환해줌
    }

    //메모 생성하기
    //index.html에서 function writePost()을 받는 부분
    @PostMapping("/api/memos")          //url은 /api/memos           //Memo: memo를 바로 반환할 것이므로, @RequestBody: 객체 형식으로 넘어오기 때문에. POST방식으로. Body안에 원하는 저장해야할 값들이 있음
    public Memo createMemo(@RequestBody MemoRequestDto requestDto) {      //MemoRequestDto: 객체  --> 객체 MemoRequestDto를 받기 위해, dto 패키지에 MemberRequestDto.java 추가해서 객체 MemoRequestDto를 만들기
        return memoService.createMemo(requestDto);      //memoService의 createMemo라는 연결되는 부분의 Method 만듦 --> service 패키지에 MemoService.java 추가, requestDto: 매개변수. Client에게서 가져온 RequestDto 안에 있는 값들을 사용하기 위함
                                                            //--> 데이터 로직을 작성하고, 데이터베이스와 연결하는 부분
    }

    //메모 조회하기
    //메모를 생성하면, 그 메모가 바로 아래에 생기고, 최근에 것일수록 위로 위치함(내림차순)
    @GetMapping("/api/memos")       //@GetMapping: GET방식으로 받아줌, /api/memos: url 주소
    public List<Memo> getMemos() {      //getMemos(): 함수 이름 --> Client에서 전달해주는 데이터는 없으므로, 파라미터는 불필요, List<Memo>: 반환 타입
        return memoService.getMemos();      //memoService에 연결해서 getMemos에 연결하는 메서드 --> MemoService.java 에서 getMemos에 대한 Method 만들기
    }

    //메모 변경하기, 수정하기
    @PutMapping("/api/memos/{id}")      //@PutMapping: PUT 방식으로 요청, {id}: PathVariable 방식으로 id값을 보냄
    public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {         //Long: 반환 타입, updateMemo: 함수 이름, PathVariable: PathVariable로 넘어왔으니까 이를 이용해서 값을 받음
        return memoService.update(id, requestDto);          //memoService:memoService와 연결         //@RequestBody: 어떤 부분을 수정했는지 그 수정된 데이터를 body에서 가져오기 위해
    }     //update(id, requestDto): id도 필요하고, 어떤값으로 변경됐는지 알아야하므로 requestDto를 사용        //MemoRequestDto requestDto: 가져올 내용은 username, contents 두개 밖에 없으므로 전에 만들어 둔 requestDto를 사용
          //--> update에 대한 Method를 MemoService.java에 생성하기

    //메모 삭제하기
    @DeleteMapping("/api/memos/{id}")
    public Long deleteMemo(@PathVariable Long id) {     //@PathVariable: PathVariable방식으로 값을 가져와야하므로, Long id: id를 받음
        return memoService.deleteMemo(id);          //memoService와 연결, deleteMemo: 이름, (id): id값이 필요하므로 파라미터에 추가함  --> MemoService.java로 가서 deleteMemo의 메소드 만들기
    }
}

8. MemoRequestDto.java

package com.sparta.hanghaememo.dto;

import lombok.Getter;

//메모 생성하기
@Getter
public class MemoRequestDto {
    private String username;        //Client에서 넘어오는 username, contents를 이 객체를 통해서 받기위함
    private String contents;
}

9. MemoService.java

//service  
package com.sparta.hanghaememo.service;

import com.sparta.hanghaememo.dto.MemoRequestDto;
import com.sparta.hanghaememo.entity.Memo;
import com.sparta.hanghaememo.repository.MemoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service        //서비스라는 걸 알려주는 어노테이션
@RequiredArgsConstructor
public class MemoService {

    //메모 생성하기
    private final MemoRepository memoRepository;        //데이터베이스와 연결하는 부분인 MemoRepository를 추가(MemoRepository를 사용할 수 있도록)

    //메모 생성하기
    @Transactional      //추후 강의에서 다룰 예정!
    public Memo createMemo(MemoRequestDto requestDto) {     //데이터베이스에 연결해서 저장하기위해서, @Entity가 붙은 Memo 클래스를 인스턴스로 만들고, 그 값을 사용해서 저장하기
        Memo memo = new Memo(requestDto);           // --> 이를 위해서, 메모 객체를 만들고 생성자 requestDto를 만들어서 값을 저장 --> Memo.java에 생성자 requestDto 부분을 만들어주기
        memoRepository.save(memo);      //memoRepository의 save() 함수를 사용해서 memo를 넣으면, 자동으로 쿼리가 생성되고 데이터베이스에 연결되고 저장됨
        return memo;        //만들어진 memo를 그대로 반환
    }

    //메모 조회하기
    @Transactional(readOnly = true)
    public List<Memo> getMemos() {      //MemoController.java에서 getMemos에 대한 Method
        return memoRepository.findAllByOrderByModifiedAtDesc();     //memoRepository에 연결해서 findAll()x findAllByOrderByModifiedAtDesc() 로 저장된 데이터를 모두 가져옴
    }                                         //--> 그런데, 가장 최근에 등록된 메모 순으로 보여주고 싶으므로  MemoRepository.java에서 추가 설정

    //메모 변경하기, 수정하기
    @Transactional
    public Long update(Long id, MemoRequestDto requestDto) {        //MemoController.java에서 update에 대한 Method
        //이번에는 새로 수정하는게 아니라 기존에 있던 메모를 수정하는 것이므로, 수정할 메모가 데이터베이스에 있는지부터 확인하기
        Memo memo = memoRepository.findById(id).orElseThrow(            //memoRepository에 연결해서 findById(id)를 넣어서 수정해야할 메모가 있는지 확인, orElseThrow(): 예외처리
                () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")   //그 예외처리의 내용: 예외 발생 시 "아이디가 존재하지 않습니다."를 반환 --> 예외 처리가 실행되지 않으면 = 오류가 발생하지 않으면 memo.update(requestDto)로 넘어감
        );
        memo.update(requestDto);        //update: 가지고 온 메모의 값을 변경, requestDto: 변경될 값은 Client에서 보내준 requestDto 안에 있는 값들 --> update() 함수의 Method를 만들기 위해, Memo.java로 가기
        return memo.getId();        //Memo.java에서 가지고 온 memo의 Id를 반환
    }

    //메모 삭제하기
    //메모 생성하기로 메모를 생성하면 데이터베이스에도 추가가 됨 --> 이걸 삭제했을때, 메모 조회하기에도 뜨지 않고, 데이터베이스에서도 기록이 사라지게 만듦
    @Transactional
    public Long deleteMemo(Long id) {           //MemoController.java에 있던 deleteMemo의 메소드 만들기
        memoRepository.deleteById(id);          //memoRepository에서 deleteById() 메소드를 사용해서 삭제, (id): 어떤 것을 삭제할지 알려주는 파라미터. Client에서 받은 값이다.
        return id;      //받아온 id를 그대로 반환
    }
}

※ [ Tip. dependency를 추가할 때, devtools를 사용하지 않고도 Springboot를 자동으로 재시작하게 만들어주는 설정 ]
우측 상단 - 구성 편집(Edit Configurations) - 옵션 수정(Modify options) - 프레임 비활성화 시(On frame deactivation) - 클래스 및 리소스 업데이트(update classes and resource)


Trouble Shooting

  1. 클래스 명은 MemberRequestDto로 만들고, 객체 생성할 때 클래스 명을 MemoRequestDto로 해서 오류
profile
개발자로 거듭나기!

0개의 댓글