기능 | Method | URL | Return |
---|---|---|---|
메인 페이지 | GET | / | index.html |
메모 생성하기 | POST | /api/memos | Memo |
메모 조회하기 | GET | /api/memos | List |
메모 변경하기 | PUT | /api/memos/{id} | Long |
메모 삭제하기 | DELETE | /api/memos/{id} | Long |
<!--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>
//테이블 생성
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();
}
}
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;
}
//인터페이스로 생성
//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)
}
//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데이터베이스 접속 가능
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);
}
}
자동으로 시간을 매핑하여 데이터베이스의 테이블에 넣어주게 됩니다.
//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의 메소드 만들기
}
}
package com.sparta.hanghaememo.dto;
import lombok.Getter;
//메모 생성하기
@Getter
public class MemoRequestDto {
private String username; //Client에서 넘어오는 username, contents를 이 객체를 통해서 받기위함
private String contents;
}
//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)