package com.multicampus.springbootdeveloper.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity // JPA 엔터티 클래스임을 나타내는 어노테이션
@Getter // Lombok 어노테이션으로, 모든 필드에 대한 Getter 메서드를 자동으로 생성
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Lombok으로 생성된 기본 생성자를 protected 접근 지정자로 생성
public class Article {
@Id // 엔터티의 주요 키(primary key)를 나타내는 어노테이션
@GeneratedValue // 자동으로 값을 생성하도록 지정하는 어노테이션, 기본 설정대로 자동 생성
@Column(name="id", updatable = false) // 엔터티의 필드와 데이터베이스 컬럼 간의 매핑을 지정하는 어노테이션
private Long id;
@Column(name="title", nullable=false)
private String title;
@Column(name="content", nullable = false)
private String content;
@Builder //Lombok으로 생성자를 자동으로 생성하는데, 빌더 패턴을 활용하여 객체를 생성
public Article(String title, String content){
this.title = title;
this.content=content;
}
/*new Article("abc", "aaaaaa"); //빌더 패턴을 적용하지 않고 객체 생성*/
/*Article.builder().title("abc").content("def").build(); //빌더패턴 적용*/
}
Article
엔터티에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행하는데 사용되는 스프링 데이터 JPA 리포지토리JpaRepository
를 상속하여 제네릭 형식으로 엔터티 타입과 주요 키 타입(id)을 지정package com.multicampus.springbootdeveloper.repository;
import com.multicampus.springbootdeveloper.domain.Article;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BlogRepository extends JpaRepository<Article, Long> {
}
Article
을 생성하기 위한 요청package com.multicampus.springbootdeveloper.dto;
import com.multicampus.springbootdeveloper.domain.Article;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor //기본 생성자를 자동으로 생성
@AllArgsConstructor //모든 필드를 파라미터로 받는 생성자를 자동으로 생성
@Getter
public class AddArticleRequest {
private String title;
private String content;
public Article toEntity(){ //DTO(데이터 전송 객체)에서 엔터티로 변환하기 위한 메서드를 제공
return Article.builder().title(title).content(content).build();
}
}
package com.multicampus.springbootdeveloper.service;
import com.multicampus.springbootdeveloper.domain.Article;
import com.multicampus.springbootdeveloper.dto.AddArticleRequest;
import com.multicampus.springbootdeveloper.repository.BlogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor //final 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service // 해당 빈을 서블릿 컨테이너에 등록
public class BlogService {
private final BlogRepository blogRepository;
public Article save(AddArticleRequest request){
return blogRepository.save(request.toEntity());
}
}
package com.multicampus.springbootdeveloper.controller;
import com.multicampus.springbootdeveloper.domain.Article;
import com.multicampus.springbootdeveloper.dto.AddArticleRequest;
import com.multicampus.springbootdeveloper.repository.BlogRepository;
import com.multicampus.springbootdeveloper.service.BlogService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController //HTTP Response Body에 객체 데이터를 json형식을 반환하는 컨트롤러
public class BlogApiController {
private final BlogService blogService;
@PostMapping("/api/articles")
public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request){
Article saveAritcle = blogService.save(request);
return ResponseEntity.status(HttpStatus.CREATED).body(saveAritcle);
}
}
⇒ 데이터베이스 자동 생성
package com.multicampus.springbootdeveloper.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.multicampus.springbootdeveloper.domain.Article;
import com.multicampus.springbootdeveloper.dto.AddArticleRequest;
import com.multicampus.springbootdeveloper.repository.BlogRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
private WebApplicationContext context;
@Autowired
BlogRepository blogRepository;
@BeforeEach
public void mockMvcSetUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.build();
blogRepository.deleteAll();
}
@DisplayName("addArticle: 블로그 글 추가에 성공한다.")
@Test
public void addArticle() throws Exception {
// given
final String url = "/api/articles";
final String title = "title";
final String content = "content";
final AddArticleRequest userRequest = new AddArticleRequest(title, content);
//객체 json으로 직렬화
final String requestBody = objectMapper.writeValueAsString(userRequest);
// when
ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(requestBody));
// then
result.andExpect(status().isCreated());
List<Article> articles = blogRepository.findAll();
assertThat(articles.size()).isEqualTo(1);
assertThat(articles.get(0).getTitle()).isEqualTo(title);
assertThat(articles.get(0).getContent()).isEqualTo(content);
}
}
package com.multicampus.springbootdeveloper.service;
import com.multicampus.springbootdeveloper.domain.Article;
import com.multicampus.springbootdeveloper.dto.AddArticleRequest;
import com.multicampus.springbootdeveloper.repository.BlogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Service;
import java.util.List;
@RequiredArgsConstructor //final 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service // 해당 빈을 서블릿 컨테이너에 등록
public class BlogService {
private final BlogRepository blogRepository;
public Article save(AddArticleRequest request){
return blogRepository.save(request.toEntity());
}
public List<Article> findAll(){ //article 테이블에 있는 모든 데이터를 조회
return blogRepository.findAll();
}
public Article findById(Long id){ // 파라미터로 ID를 받도록 수정
return blogRepository.findById(id).orElse(null); // ID에 해당하는 Article을 찾지 못하면 null 반환
}
}
package com.multicampus.springbootdeveloper.controller;
import com.multicampus.springbootdeveloper.domain.Article;
import com.multicampus.springbootdeveloper.dto.AddArticleRequest;
import com.multicampus.springbootdeveloper.repository.BlogRepository;
import com.multicampus.springbootdeveloper.service.BlogService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequiredArgsConstructor
@RestController //HTTP Response Body에 객체 데이터를 json형식을 반환하는 컨트롤러
public class BlogApiController {
private final BlogService blogService;
@PostMapping("/api/articles")
public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request){
Article saveAritcle = blogService.save(request);
return ResponseEntity.status(HttpStatus.CREATED).body(saveAritcle);
}
@GetMapping("api/articles")
public ResponseEntity<List<ArticleResponse>> findAllArticles(){
List<ArticleResponse> articles = blogService.findAll().stream().map(ArticleResponse::new).toList();
return ResponseEntity.ok().body(articles);
}
//get방식 /api/articles/{id}
@GetMapping("/api/article/{id}")
public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id){
Article article = blogService.findById(id);
return ResponseEntity.ok().body(new ArticleResponse(article));
}
}
⇒ 무슨 문제인고 하니…
package com.multicampus.springbootdeveloper.controller;
import com.multicampus.springbootdeveloper.domain.Article;
public class ArticleResponse {
public ArticleResponse(Article article) {
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.multicampus'
version = '1.0-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral() //의존성을 받을 저장소를 지정
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' //spring MVC를 사용해서 RESTful 웹 서비스를 개발할 때 필요한 의존성
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 스프링 데이터 베이스 JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2' //인메모리 데이터베이스
compileOnly 'org.projectlombok:lombok' //롬복
annotationProcessor 'org.projectlombok:lombok'
//타임리프
implementation 'org.springframework.boot::spring-boot-starter-thymeleaf'
}
tasks.named('test') {
useJUnitPlatform()
}
package com.multicampus.springbootdeveloper.controller;
import lombok.Getter;
import lombok.Setter;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.time.LocalDate;
import java.util.List;
@Controller
public class ExampleController {
@PostMapping("/thymeleaf/example")
public String thymelafExample(Model model){
Person person = new Person();
person.setId(1L);
person.setAge(11);
person.setName("홍길동");
person.setHobbies(List.of("운동", "독서"));
model.addAttribute("person", person);
model.addAttribute("today", LocalDate.now());
return "example";
}
//Person 객체
@Getter
@Setter
class Person{
private Long id;
private String name;
private int age;
private List<String> hobbies;
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>타임 리프 테스트</h2>
<p th:text="${#temporals.format(today, 'yyyy-MM-dd')}"></p>
<div th:object="${person}">
<p th:text="|이름 + *{name}|"></p>
<p th:text="|나이 + *{age}|"></p>
</div>
<p>취미</p>
<ul th:each="hobby : *{hobbies}">
<li th:text="${hobby}"></li>
<li th:text="${hobby =='운동'}">(대표취미)</li>
</ul>
<a th:href></a>
</body>
</html>
버전 바꾸니 잘 됨
화면은 잘 뜸
객체 생성
: dto/ArticleListViewResponse.javapackage com.multicampus.springbootdeveloper.dto;
import lombok.Getter;
import com.multicampus.springbootdeveloper.domain.Article;
@Getter
public class ArticleListViewResponse {
private final Long id;
private final String title;
private final String content;
public ArticleListViewResponse(Article article) {
this.id = article.getId();
this.title = article.getTitle();
this.content = article.getContent();
}
}
package com.multicampus.springbootdeveloper.controller;
//blog article all list : /articles GET 요청을 처리한다. 블로그 글 전체 리스트를 담은 뷰 반환
import com.multicampus.springbootdeveloper.dto.ArticleListViewResponse;
import com.multicampus.springbootdeveloper.service.BlogService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class BlogViewController {
private final BlogService blogService;
@GetMapping("/articles")
public String getArticle(Model model){
List<ArticleListViewResponse> articles = blogService.findAll().stream().map(ArticleListViewResponse::new).toList();
model.addAttribute("articles", articles);
return "articleList"; //artocleList.html 조회
}
}
<!DOCTYPE html>
<html xmlns:th="www.thymeleaf.org">
<head> <!--모델에서 전달한 블로그 글 리스트 개수만큼 반복해서 글 정보를 보여준다.-->
<meta charset="UTF-8">
<title>블로그 글 목록</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h1 class="mb-3">My Blog</h1>
<h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>
<div class="container">
<button type="button" id="create-btn"
th:onclick="|location.href='@{/new-article}'|"
class="btn btn-secondary btn-sm mb-3">글 등록</button>
<div class="row-6" th:each="item : ${articles}">
<div class="card">
<div class="card-header" th:text="${item.id}">
</div>
<div class="card-body">
<h5 class="card-title" th:text="${item.title}"></h5>
<p class="card-text" th:text="${item.content}"></p>
<a th:href="@{/articles/{id}(id=${item.id})}" class="btn btn-primary">보러가기</a>
</div>
</div>
<br>
</div>
</div>
<script src="/js/article.js"></script>
</body>
// 삭제 기능
const deleteButton = document.getElementById('delete-btn');
if (deleteButton) {
deleteButton.addEventListener('click', event => {
let id = document.getElementById('article-id').value;
fetch(`/api/articles/${id}`, {
method: 'DELETE'
})
.then(() => {
alert('삭제가 완료되었습니다.');
location.replace('/articles');
});
});
}
// 수정 기능
const modifyButton = document.getElementById('modify-btn');
if (modifyButton) {
modifyButton.addEventListener('click', event => {
let params = new URLSearchParams(location.search);
let id = params.get('id');
fetch(`/api/articles/${id}`, {
method: 'PUT',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: document.getElementById('title').value,
content: document.getElementById('content').value
})
})
.then(() => {
alert('수정이 완료되었습니다.');
location.replace(`/articles/${id}`);
});
});
}
// 생성 기능
const createButton = document.getElementById('create-btn');
if (createButton) {
createButton.addEventListener('click', event => {
fetch('/api/articles', {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: document.getElementById('title').value,
content: document.getElementById('content').value
})
})
.then(() => {
alert('등록 완료되었습니다.');
location.replace('/articles');
});
});
}
package com.multicampus.springbootdeveloper.domain;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import java.time.LocalDateTime;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {
@CreatedDate //엔터티가 생성될 때 생성 시간 저장
@Column(name="created_at")
private LocalDateTime createAt;
@LastModifiedDate //엔터티가 수정될 때 수정 시간 저장
@Column(name = "updated_at")
private LocalDateTime updateAt;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id", updatable = false)
private Long id;
@Column(name="title", nullable=false)
private String title;
@Column(name="content", nullable = false)
private String content;
@Builder //빌더패턴을 이용해서 객체 생성
public Article(String title, String content){
this.title = title;
this.content=content;
}
/*new Article("abc", "aaaaaa"); //빌더 패턴을 적용하지 않고 객체 생성*/
/*Article.builder().title("abc").content("def").build(); //빌더패턴 적용*/
}
INSERT INTO article (title, content, created_at, updated_at) VALUES ('title1', 'content1', now(), now())
INSERT INTO article (title, content, created_at, updated_at) VALUES ('title2', 'content3', now(), now())
INSERT INTO article (title, content, created_at, updated_at) VALUES ('title2', 'content3', now(), now())
@EnableJpaAuditing
package com.multicampus.springbootdeveloper;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class SpringbootDeveloperApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDeveloperApplication.class, args);
}
}
@GetMapping("/articles/{id}") //상세보기?
public String getArticle(@PathVariable Long id, Model model){
// 1개이니 List가 아님
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleResponse(article));
return "article"; //article.html 조회
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>블로그 글</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h1 class="mb-3">My Blog</h1>
<h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>
<div class="container mt-5">
<div class="row">
<div class="col-lg-8">
<article>
<input type="hidden" id="article-id" th:value="${article.id}">
<header class="mb-4">
<h1 class="fw-bolder mb-1" th:text="${article.title}"></h1>
<div class="text-muted fst-italic mb-2" th:text="|Posted on ${#temporals.format(article.createdAt, 'yyyy-MM-dd HH:mm')}|"></div>
</header>
<section class="mb-5">
<p class="fs-5 mb-4" th:text="${article.content}"></p>
</section>
<button type="button" id="modify-btn"
th:onclick="|location.href='@{/new-article?id={articleId}(articleId=${article.id})}'|"
class="btn btn-primary btn-sm">수정</button>
<button type="button" id="delete-btn"
class="btn btn-secondary btn-sm">삭제</button>
</article>
</div>
</div>
</div>
<script src="/js/article.js"></script>
</body>
<!DOCTYPEhtml>
<htmlxmlns:th="http://www.thymeleaf.org">
<head>
<metacharset="UTF-8">
<title>블로그 글</title>
<linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<divclass="p-5 mb-5 text-center</> bg-light">
<h1class="mb-3">My Blog</h1>
<h4class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>
<divclass="container mt-5">
<divclass="row">
<divclass="col-lg-8">
<article>
<inputtype="hidden"id="article-id"th:value="${article.id}">
<headerclass="mb-4">
<inputtype="text"class="form-control"placeholder="제목"id="title"th:value="${article.title}">
</header>
<sectionclass="mb-5">
<textareaclass="form-control h-25"rows="10"placeholder="내용"id="content"th:text="${article.content}"></textarea>
</section>
<buttonth:if="${article.id} != null"type="button"id="modify-btn"class="btn btn-primary btn-sm">수정</button>
<buttonth:if="${article.id} == null"type="button"id="create-btn"class="btn btn-primary btn-sm">등록</button>
</article>
</div>
</div>
</div>
<scriptsrc="/js/article.js"></script>
</body>
요즘... 팀 프로젝트랑 아르바이트로 하루도 제대로 쉬지를 못했더니 체력이 안좋아서 자꾸 블로그글 쓰는 걸 밀리게 된다.. 그래도 1주일안에 쓰기는 하는데 밀리지말자..!!!