
우선 이 프로젝트는 따로 강의과제나 개인과제가 아닌 강의 중 진행하는 프로젝트라는 것을 알아두면 될 것 같다.
간단한 메모 작성, 조회, 수정, 삭제를 다루고 있으며, 1주차 강의의 끝인 JDBC까지만 적용을 하였고, 이후 2주차 강의 중에 메모장 프로젝트로 JPA 사용하는 부분까지 다룰 예정이다!
우선 3가지의 패키지로 나눌 예정이다.
1. controller - 매인 컨트롤러를 다루는 클래스를 담는 패키지
2. dto - Data Transfer Object 약자로 프로세스간 데이터를 전달하는 객체를 담는 패키지
3. entity - 데이터를 담는 패키지
클래스 이름은 Memo로 만들었으며, 코드는 아래와 같다.
package com.sparta.memo.entity;
import com.sparta.memo.dto.MemoRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class Memo {
private Long id;
private String username;
private String contents;
public Memo(MemoRequestDto requestDto) {
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
}
public void update(MemoRequestDto requestDto) {
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
}
}
id, username, contents 세가지의 컬럼을 가지고 있으며, 클래스 위에있는 Getter, Setter 애너테이션을 통해 해당 컬럼별 getter setter 메소드가 만들어져있으며, NoArgsConstructor 애너테이션은 매개변수가 없는 기본생성자가 있다는 것을 의미한다. 해당 클래스에 MemoRequestDto를 매개변수로 받는 생성자가 있기 때문에 기본생성자를 다른 클래스에서 쓴다면 필요할 것 같아 추가하였다.
DTO에는 두가지가 있다.
요청하는데에 쓰이는 RequestDto, 응답받는데 쓰이는 ResponseDto가 그것이다.
package com.sparta.memo.dto;
import lombok.Getter;
@Getter
public class MemoRequestDto {
private String username;
private String contents;
}
요청하는 request에서는 id는 autoincrement를 사용하여 자동으로 증가하게끔 할것이기 때문에 해당 값이 없으며, 이름과 내용만 담을 수 있도록 만들었다.
package com.sparta.memo.dto;
import com.sparta.memo.entity.Memo;
import lombok.Getter;
@Getter
public class MemoResponseDto {
private Long id;
private String username;
private String contents;
public MemoResponseDto(Memo memo) {
this.id = memo.getId();
this.username = memo.getUsername();
this.contents = memo.getContents();
}
public MemoResponseDto(Long id, String username, String contents) {
this.id = id;
this.username = username;
this.contents = contents;
}
}
응답받는 response는 id, name, contents 모두 받도록 하였다.
package com.sparta.memo.controller;
import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.web.bind.annotation.*;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
@RestController
@RequestMapping("/api")
public class MemoController {
private final JdbcTemplate jdbcTemplate;
public MemoController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@PostMapping("/memos")
public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
KeyHolder keyHolder = new GeneratedKeyHolder(); // 기본 키를 반환받기 위한 객체
String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
jdbcTemplate.update( con -> {
PreparedStatement preparedStatement = con.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, memo.getUsername());
preparedStatement.setString(2, memo.getContents());
return preparedStatement;
},
keyHolder);
// DB Insert 후 받아온 기본키 확인
Long id = keyHolder.getKey().longValue();
memo.setId(id);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
return memoResponseDto;
}
@GetMapping("/memos")
public List<MemoResponseDto> getMemos() {
// DB 조회
String sql = "SELECT * FROM memo";
return jdbcTemplate.query(sql, new RowMapper<MemoResponseDto>() {
@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String username = rs.getString("username");
String contents = rs.getString("contents");
return new MemoResponseDto(id, username, contents);
}
});
}
@PutMapping("/memos/{id}")
public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findById(id);
if(memo != null) {
// memo 내용 수정
String sql = "UPDATE memo SET username = ?, contents = ? WHERE id = ?";
jdbcTemplate.update(sql, requestDto.getUsername(), requestDto.getContents(), id);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
@DeleteMapping("/memos/{id}")
public Long deleteMemo(@PathVariable Long id) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findById(id);
if(memo != null) {
// memo 삭제
String sql = "DELETE FROM memo WHERE id = ?";
jdbcTemplate.update(sql, id);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
private Memo findById(Long id) {
// DB 조회
String sql = "SELECT * FROM memo WHERE id = ?";
return jdbcTemplate.query(sql, resultSet -> {
if(resultSet.next()) {
Memo memo = new Memo();
memo.setUsername(resultSet.getString("username"));
memo.setContents(resultSet.getString("contents"));
return memo;
} else {
return null;
}
}, id);
}
}
jdbctemplate을 사용하여 데이터베이스에 접근하였다.
또한, API별로 메소드를 만들어 메모 생성, 조회, 변경, 삭제를 하였다.
마지막 findById는 해당 id인 메모를 찾아서 그 아이디의 메모가 있는지를 판별하기 위해 만들었으며, update와 delete에 공통으로 사용되는 부분이라 메소드로 따로 빼어놓았다.
해당 기능들을 직접 사용해보기 위해 Postman과 html을 추가하여 실제 url에서도 작업을 해보았으며, 제공받은 html은 아래와 같다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memo 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();
}
$(document).ready(function () {
// HTML 문서를 로드할 때마다 실행합니다.
getMessages();
})
// 메모를 불러와서 보여줍니다.
function getMessages() {
// 1. 기존 메모 내용을 지웁니다.
$('#cards-box').empty();
// 2. 메모 목록을 불러와서 HTML로 붙입니다.
$.ajax({
type: 'GET',
url: '/api/memos',
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) {
// 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=""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>`;
// 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",
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} 에 요청해서 메모를 삭제합니다.
$.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>Memo</h2>
<p>
공유하고 싶은 소식을 입력해주세요.
</p>
</div>
<div class="area-write">
<textarea class="field" placeholder="공유하고 싶은 소식을 입력해주세요" name="contents" id="contents" cols="30"
rows="10"></textarea>
<!-- <button class="btn btn-danger">작성하기</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 class="username">
anonymous
</div>
</div>
<!-- contents 조회/수정 영역-->
<div class="contents">
</div>
<!-- 버튼 영역-->
<div class="footer">
<img id="1-edit" class="icon-start-edit" src="images/edit.png" alt="" onclick="editPost('1')">
<img id="1-delete" class="icon-delete" src="images/delete.png" alt="" onclick="deleteOne('1')">
<img id="1-submit" class="icon-end-edit" src="images/done.png" alt="" onclick="submitEdit('1')">
</div>
</div>
</div>
</div>
</body>
</html>

이와같이 메모를 작성할수도 있으며 아래 아이콘으로 편집과 삭제도 무리없이 진행이 가능했다.
이후 2주차 포스트도 열심히 작성해보겠다!