멀티캠퍼스 백엔드 과정 69~70일차[9월 13일~9월 14일] - 스프링 부트, jpa

GoldenDusk·2023년 9월 16일
0
post-thumbnail

API

1. REST API 특징

서버/클라이언트 구조

  1. 무상태
  2. 캐시 처리 가능
  3. 계층화
  4. 인터페이스 일관성
  5. 장점
  • URL 만 보고도 무슨 행동을 하는 API 명확하게 알 수 있다.
  • 클라이언트와 서버의 역할이 명확하게 분리
  • HTTP 표준 사용하는 모든 플랫폼 적용 가능
  1. 단점
  • HTTP 메서드
  • HTTP MDN 사이트
  • GET POST 같은 방식의 개수에 제한
  • 설계를 위한 공식없음. 각 설계자 설계
  • "REST 하게 디자인한 API" RESTFul API 부른다. ==> RESTFul 서비스 구현
  1. REST API 사용하는 방법
  • 규칙1 : URL에 동사를 쓰시 말고, 자원을 표시(가져오는 데이터)
    • ./students/1 O
    • ./get-students?student_id=1 X
  • 규칙2: 동사는 HTTP메서드
    • GET(조회), PUT(수정), POST(쓰기),DELETE(삭제) ---CRUD
    • URL 입력값이 아니다. 내부적으로 처리하는 방식

실습

블로그 글 작성

  1. Ariticle.java
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(); //빌더패턴 적용*/
}
  1. BlogRepository.java
  • 이 인터페이스는 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> {
}
  1. AddArticleRequest.java
  • 이 클래스는 클라이언트에서 새로운 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();
    }
}
  1. BlogService.java
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());
    }

}
  1. BlogApiController.java
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);
    }
}

⇒ 데이터베이스 자동 생성

  1. controllertest.java
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);
    }

}
  1. BlogService.java
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 반환
    }

}
  1. BlogApiController.java
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));
    }
}

⇒ 무슨 문제인고 하니…

  1. ArticleResponse.java
package com.multicampus.springbootdeveloper.controller;

import com.multicampus.springbootdeveloper.domain.Article;

public class ArticleResponse {

    public ArticleResponse(Article article) {
    }
}

🧐 타임리프

  1. 타임리프 build.gradle
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()
}
  1. ExampleController.java
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;
    }
}

  1. example.html
<!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>

문제사항

  1. 404에러랑 500에러 나길래 코드 수정 후 에러 메세지 확인 후 build.gradle 건들었더니 난리다.. 그래서 버전을 바꿔줌 boot

  1. 버전 바꾸니 잘 됨

  2. 화면은 잘 뜸

  1. 근데 html이 음… 이게 버전을 바꾸면

  1. 만약 405 오류가 발생라면

🔗 출처 : https://hello-nanam.tistory.com/272

  • method 방식을 GET 을 사용하면 되는데 POST 를 사용하면 405 오류가 발생하더라
  • Access-Control-Allow-Origin 을 헤더 정보에 넣어줘야 한다
  • 만약 그래도 안된다면..

🧐 블로그 화면 구성

  1. 블로그 글 목록 뷰 구현(전체 리스트)
  • 컨트롤러 메서드 작성
    • 뷰에게 데이터를 전달하기 위해 객체 생성 : dto/ArticleListViewResponse.java
  • HTML 뷰 만들고 테스팅
  1. 블로그 글 뷰 구현(한 개의 글)
  • 엔터티에 생성, 수정 시간 추가
  • 컨트롤러 메서드 작성
  • HTML 뷰 만들기
  • 실행/테스트
  1. 삭제
  • 컨트롤러 메서드 작성
  • html 뷰 만들기
  • 실행/테스트

실습

  1. ArticleListViewResponse. java
package 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();
    }
}
  1. BlogViewController.java
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 조회
    }
}
  1. articleList.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>
  1. article.js
// 삭제 기능
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');
            });
    });
}
  1. Article.java 추가
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(); //빌더패턴 적용*/
}
  1. data.sql 설정
  • 더미 데이터
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())
  1. 감시 에너테이션 추가 : @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);
    }

}
  1. BlogViewController.java 추가
@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 조회
    }
  1. 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>
  1. newArticle.html
<!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주일안에 쓰기는 하는데 밀리지말자..!!!

profile
내 지식을 기록하여, 다른 사람들과 공유하여 함께 발전하는 사람이 되고 싶다.

0개의 댓글