spring boot 게시판 만들기

박준우·2025년 8월 10일
0

Spring Boot

목록 보기
10/14

1. 개요

Spring boot를 이용하여 게시판을 만들고, 데이터를 업로드, 확인하고, 수정, 삭제하는 (CRUD)기능을 구현하였다. 또한, 스프링 내부에서 데이터가 처리되는 논리적 순서대로 개발과정을 만들고자 한다.

사용한 개발도구

1.JPA(DB연결)
2.thymeleaf(탬플릿 엔진)
3.ojdbc(DB연결)
4.lombok(개발 도구)
5.junit(테스트)
6.Oracle DB(데이터베이스)
7.spring web(톰켓 서버 사용)
8.spring devtool(개발 도구)

최종 결과물

https://github.com/flyjunu/firstSpringWebsite

프로젝트 패키지 구조

2. 목록 확인 기능 개발

(0) 데이터 베이스 연결하기

게시판에 올릴 글의 데이터는 오라클 데이터 베이스에 저장할 것이다. 이를 위하여 데이터베이스 세팅의 1번과정(application.yml 파일 만들기)와 2번과정(application.properties 수정하기)를 그대로 만들었다.

(1) BoardDTO 추가

BoardDTO는 DB의 뷰기능과 같은 존재로 이를 통해 직접 DB에서 데이터를 추출하지 않고, 데이터를 가공해 html뷰로 전송하여 확장성과, 보안성을 챙길 수 있다.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BoardDTO {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 (옵션)
    private Long id;
    private String boardWriter;
    private String boardPass;
    private String boardTitle;
    private String boardContents;
    private int boardHits;
    private Timestamp createdAt;

    // Entity -> DTO 변환용 생성자
    public BoardDTO(BoardEntity entity) {
        this.id = entity.getId();
        this.boardWriter = entity.getBoardWriter();
        this.boardPass = entity.getBoardPass();
        this.boardTitle = entity.getBoardTitle();
        this.boardContents = entity.getBoardContents();
        this.boardHits = entity.getBoardHits();
        this.createdAt = entity.getCreatedAt();
    }
}

해석

에노테이션메서드
@Dataget, set 함수 자동생성
@NoArgsConstructor인수없는 생성자 생성
@AllArgsConstructor모든인수를 가지는 생성자 생성
@Id이 컬림이 PK임을 선언한다.
@GeneratedValuePK방식 선언문, pk 정합성을 DB에게 맡긴다.

(2) BoardEntity추가

@Entity는 스프링에서 SQL이 아닌 자바 클래스 형태로 DB를 생성 및 사용할 수 있도록, 만들어 주는 것으로 JPA,hibernate를 이용해 DB를 연결하고 명령하여 데이터를 처리할 수 있다.

@Entity 							// 이 클래스를 JPA가 다룰수있게 한다. 
@Table(name = "board_sts_jpa") 		// 테이블명은 board_sts_jpa이다.
@Data								// getter, setter를 자동으로 만들어 준다.
public class BoardEntity {
    // @Id 는 이 컬럼이 PK임을 선언하기 위한 에노테이션이다.
    // @GeneratedValue는 PK컬럼에 사용하며,PK를 어떻게 생성할지 선택한다.
    // Table= 키 생성 전용 테이블을 만든다.
    // SEQUENCE = 시퀸스 테이블을 만든다.
    // IDENTITY = PK생성을 DB에 위임한다.
    // AUTO = JPA가 자동으로 생성전략을 결정한다.
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

	//@Column을 통해 컬럼을 선언하고, 제약을 선언한다 
    @Column(length = 20, nullable = false)
    private String boardWriter;

    @Column(length = 30, nullable = false)
    private String boardTitle;

    @Column(length = 1000)
    private String boardContents;

    private String boardPass;

    //조회수
    private int boardHits = 0;

    @CreationTimestamp
    private Timestamp createdAt;

    // DTO -> Entity 변환용 static 메서드
    public static BoardEntity toEntity(BoardDTO dto) {
        BoardEntity entity = new BoardEntity();
        entity.setId(dto.getId());
        entity.setBoardWriter(dto.getBoardWriter());
        entity.setBoardPass(dto.getBoardPass());
        entity.setBoardTitle(dto.getBoardTitle());
        entity.setBoardContents(dto.getBoardContents());
        entity.setBoardHits(dto.getBoardHits());
        entity.setCreatedAt(dto.getCreatedAt());
        return entity;
    }
}

(1) Board컨트롤러 만들기

게시판 기능을 구현하기 위해서 BoardController 파일을 만들었다. 컨트롤러 파일은 사용자로 부터 get, set등 명령을 받았을 때 서비스에게 내부로직을 처리하게 지시하고, 서비스로 부터 응답을 받아 뷰에 객체정보(모델)를 보내는 역할을 한다.

구현 코드

@Controller //컨트롤러로 지정
@RequiredArgsConstructor // 모든 속성에 인수를 받는 생성자를 생성
public class BoardController {
    private final BoardService boardService;

    @GetMapping("/list")
    public String findAll(Model model) { 
        List<BoardDTO> boardList = boardService.findAll();
        model.addAttribute("boardList", boardList);
        return "list";
    }

해석

사용자가 Get 방식으로 /list주소로 이동하면, findAll함수를 실행한다. 이 때 Model을 인자로 받는데, 이 Model은 스프링부트에서 제공하는 클래스로, model.addAttribute를 통해 model에 값이나, 객체를 담아 return에서 지정된 주소(/list)로 전송할 수 있다. 이때 model.attribute의 첫번째 인자리턴받을 뷰에서 사용할 객체명이고, 두번째 인자모델에 담을 원래 객체명 or 속성 or 값이다.

다시 코드로 돌아와 boardService.findAll()을 통해 BoardDTO 타입객체 boardList에 담는다. 이를 위해 BoardService로 이동해보자.

(2) BoardService 만들기

서비스층은 서비스의 핵심적인 기능 구현 및 내부 로직을 처리하기 위한 계층이다. 이런 핵심기능들은 컨트롤러가 서비스를 호출하는 방식으로 처리하며, 직접 처리하지 않는다.

구현 코드

@Service//자바 내부 로직을 처리하기 위한 Service 계층 선언
@RequiredArgsConstructor // 모든 인자를 인수로 받는 생성자 생성
public class BoardService {
    private final BoardRepository boardRepository;

    // 전체 글 목록 반환 (Entity → DTO 변환)
    public List<BoardDTO> findAll() {
        List<BoardEntity> entityList = boardRepository.findAll();
        List<BoardDTO> dtoList = new ArrayList<>();
        for (BoardEntity entity : entityList) {
            dtoList.add(new BoardDTO(entity));
        }
        return dtoList;
    }

해석

위 함수가 Service임을 선언하고, 모든 모든 인자를 인수로 받는 생성자를 생성하였다.
컨트롤러에서 findAll()함수를 호출하여 이를 실행한다.

boardRepository 인터페이스에서 findAll()을 실행하면, JPA/hibernate가 데이터베이스에 접속한 뒤, 각각의 인스턴스(=row 행)를 가리키는 모든행의 참조변수(&)를 List안에 집어넣게 된다. 그 후 객체타입을 BoardDTO 타입으로 변환하고 각 행을 가리키는 참조변수가 담긴 리스트 dtoList를 반환한다.

(3) BoardRepository 만들기

BoardRepostory는 JpaRepository를 확장한 인터페이스로, spring data JPA의존성을 추가했을 때 사용할 수 있다.

구현 코드

public interface BoardRepository extends JpaRepository<BoardEntity, Long> {
}

JPA는 BoardEntity로 부터 데이터를 받아서, findAll()과 같은 JPA함수를 사용할 수 있게 되며, DB에 적용되는 명령을 수행한다. 첫번째 인자는 테이블명이며, 두번째 인자는 테이블 PK 컬럼타입이다.(테이블 기준X JPA 기준O)

이렇게 처리된 findall() 즉, 모든 인스턴스가 담긴 리스트객체가 컨트롤러로 반환되고, 그곳에서 모델에 추가된 뒤 list.html 뷰로 전송된다.

대표적 JPA 함수

기능메서드 (예시 시그니처)반환형대표 사용 예
단건 조회findById(ID id)Optional<T>repo.findById(1L).orElseThrow()
지연 참조getReferenceById(ID id)T(프록시)post.setAuthor(userRepo.getReferenceById(uid))
다건 조회findAll()List<T>repo.findAll()
PK 묶음 조회findAllById(Iterable<ID>)List<T>repo.findAllById(List.of(1L,2L))
저장/수정save(T entity)Trepo.save(entity)
다건 저장saveAll(Iterable<S>)List<S>repo.saveAll(entities)
저장+즉시반영saveAndFlush(T)T제약조건 즉시 검증 필요 시
삭제(여러 건)deleteAll(Iterable<? extends T>) / deleteAll()voidrepo.deleteAll(list)
대량 삭제deleteAllInBatch() / deleteAllInBatch(Iterable<T>) / deleteAllByIdInBatch(Iterable<ID>)void조건 없는 전체/묶음 일괄 삭제
전체 개수count()longlong n = repo.count()
존재 여부existsById(ID)booleanrepo.existsById(id)
정렬 조회findAll(Sort sort)List<T>repo.findAll(Sort.by("createdAt").descending())
페이징 조회findAll(Pageable pageable)Page<T>repo.findAll(PageRequest.of(0,20, Sort.by("id")))
플러시flush()voidrepo.flush()

(4) list.html 만들기

list.html은 사용자에게 목록을 보여주기 위한 뷰로, 뷰는 컨트롤러를 통해 모델에 저장된 객체나 값을 전달받는다. 이 목록 페이지에서는 컨트롤러 findAll()을 통해 모델에 저장된 (model.addAttribute("boardList", boardList))즉, "boardList" 모델명을 사용하여, 객체를 받아올 수 있다. 이때 타임리프 탬플릿 엔진을 사용한 문법으로 이 객체를 처리하였다.

출력 결과

구현 코드

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>게시글 목록</title>
  <style>
    .row { margin-bottom: 8px; }
    .col-id { display: inline-block; width: 60px; }
    .col-title { display: inline-block; width: 150px; }
    .col-writer { display: inline-block; width: 80px; }
    .col-date { display: inline-block; width: 160px; }
    .header { font-weight: bold; }
    .header .col-id,
    .header .col-title,
    .header .col-writer,
    .header .col-date {
      text-align: center;
    }
    a.col-title { color: blue; text-decoration: underline; }
  </style>
</head>
<body>
<h2>게시글 목록</h2>
<div>
  <div class="row header">
    <span class="col-id">글 번호</span> |
    <span class="col-title">제목</span> |
    <span class="col-writer">작성자</span> |
    <span class="col-date">작성일</span>
  </div>
  //board List 객체를 한 인스턴스(행)씩 가져와 board 변수에 저장하고, 모든 행을 처리할 때까지 반복한다. 
  <div th:each="board : ${boardList}" class="row">
    <span class="col-id" th:text="${board.id}"></span> |
    <a class="col-title" th:href="@{/detail/{id}(id=${board.id})}" th:text="${board.boardTitle}"></a> |
    <span class="col-writer" th:text="${board.boardWriter}"></span> |
    <span class="col-date" th:text="${#dates.format(board.createdAt, 'yyyy-MM-dd HH:mm')}"></span>
  </div>
</div>
<a href="/save">글 작성</a>
</body>
</html>

3. 저장 기능 개발

list.html 에서 "글 작성" 텍스트 클릭 통해 링크를 타고 /save로 이동한다. /save에서는 글의 제목과, 작성자를 입력하고, 비밀번호, 내용을 입력하고, 글의 내용이 DB에 저장되도록 개발하였다.

(1) Board컨트롤러 만들기

구현코드

@GetMapping("/save")
public String saveForm(Model model) {
    model.addAttribute("board", new BoardDTO());
    return "save";
}

//모델 어트리뷰트 에노테이션: 파라미터의 오브젝트를 선언하지 않아도 자동 생성한다.
// 단 getter setter 함수가 생성하려는 객체에 있어야만 가능하다.
@PostMapping("/save")
public String save(@ModelAttribute BoardDTO boardDTO){
    boardService.save(boardDTO);
    return "redirect:/";
}

해석

유저가 /save로 이동하면 mapping 방식을 통해 이동한다. 여기서 단순히 입력뷰만 보여주는 saveForm함수에 왜 모델에 boardDTO 객체를 추가하고, save로 반환해야 하는 이유는 save.html에서 모델에서 board라는 이름의 객체를 전달받지 못할 경우, NullPointerException을 발생시키기 때문이다.

post방식에서 @ModelAttribute를 통해 인자를 받는데, 이를 사용하면, save.html로 부터 BoardDTO 타입의 객체를 입력받을 수 있다. 이렇게 뷰로부터 입력된 데이터를 boardservice의 save함수를 통해 저장한다.

단, 이를 사용하려면 getter setter 함수가 있어야 하는데, 이는 lombok패키지의 @Data를 통해 자동 생성된다.(목록 기능 구현 컨트롤러참조)

(2) save.html

구현코드

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
//form의 내용(board 객체)를 /save 주소로, post방식으로 준다.
<form th:action="@{/save}" th:object="${board}" method="post">
    <label for="boardTitle">제목</label>
    <input id="boardTitle" th:field="*{boardTitle}"><br>

    <label for="boardWriter">작성자</label>
    <input id="boardWriter" th:field="*{boardWriter}"><br>

    <label for="boardPass">비밀번호</label>
    <input id="boardPass" type="password" th:field="*{boardPass}"><br>

    <label for="boardContents">내용</label>
    <textarea id="boardContents" th:field="*{boardContents}" rows="10" cols="50"></textarea><br>

    <input type="submit" value="작성"><br>
</form>
</body>
</html>

해석

유저가 글을 작성하면 post방식을 통해 save.html의 form 내용이 board객체 안에 저장된다. *th:field는 boardDTO의 속성과 일치해야하며 컨트롤러의@ModelAttribute 에너테이션을 통해 값이 자동으로 객체의 속성과 바인딩된다.

boardDTO의 속성들
    private Long id;
    private String boardWriter;
    private String boardPass;
    private String boardTitle;
    private String boardContents;
    private int boardHits;
    private Timestamp createdAt;

참조: label for와 input id는 아래와 같은 기능을 가진다.

1. <label for="boardTitle">제목</label>
역할: 폼 컨트롤(입력창)과 시각적으로/프로그램적으로 연결. 
for 속성 값이 **대상 <input>의 id**와 같아야 함.
효과:
접근성(스크린리더) 향상 → 레이블 읽어줌.
마우스 클릭 시 해당 입력창에 포커스가 감.

2. id="boardTitle" (in <input>)
역할: HTML 문서 내 고유 식별자.
<label for="...">에서 연결 대상이 되려면 이 값이 필요.
JS/CSS에서도 특정 입력 요소를 선택할 때 사용 가능.

(3) Boardservice 만들기

글을 작성하기 위해 컨트롤러 부분으로 돌아가, BoardService를 Post로 호출하면, 작성내용이 boardDTO에 담기고, 이를 인자로 삼는 BoardService의 save를 호출하게 된다.

구현코드

1. 컨트롤러 
@PostMapping("/save")
public String save(@ModelAttribute BoardDTO boardDTO){
    boardService.save(boardDTO);
    return "redirect:/";
}

2. BoardService 구현
    public String save(BoardDTO boardDTO) {
        BoardEntity entity = BoardEntity.toEntity(boardDTO);
        boardRepository.save(entity);
        return "index";
    }

해석

boardService에서는 boardDTO를 BoardEntity 타입으로 변경한 후 이를 boardRepostiry를 통해 저장한다. 그 후 index 페이지로 돌아간다. boardRepostory는 목록 확인 기능 개발의 (3) BoardRepository 만들기 부분과 완전히 똑같으며, jpa/hibernate를 통해 save함수를 사용하는 부분만 다르다.

정리

    1. 글을 아래와 같이 작성후 작성버튼을 누른다.
    1. index page로 돌아간다.
    1. 글목록을 확인하니 아래와 같이 데이터가 입력되었다.
    1. (아직 구현X) 글 내용도 정상적으로 처리되었음을 알 수 있다.

4. 글 상세 개발

글 상세는 list.html의 제목 클릭을 통해 이동할 수 있으며, 글 1개에 작성된 내용을 확인할 수 있다.

(1) BoardController만들기

구현코드

@GetMapping("/detail/{id}")
public String detail(@PathVariable Long id, Model model) {
    // boardService에서 ID를 가져온다.
    BoardDTO board = boardService.findById(id);
    // 모델에 board 객체를 추가하여, detail.html로 보낸다.
    // 그러면 detail.html에서 이 객체를 사용할 수 있다.
    model.addAttribute("board", board);
    return "detail";
}

해석

컨트롤러의 주소에 {id}는 고정된 주소가 아닌 변동가능한 주소를 의미한다. 이 값은 list.html의 의해 id 주소가 넘겨진다. @pathVariable 에노테이션은 URL의 주소에 담긴 동적인 주소{id}를 매개변수 Long id에 넘겨줌을 의미한다. 그후, boardService에서 ID를 찾아 값을 "board"라는 이름으로 모델에 추가한뒤, detail.html로 넘겨준다.

<a class="col-title" th:href="@{/detail/{id}(id=${board.id})}" th:text="${board.boardTitle}"></a>

(2) BoardService 만들기

구현코드

BoardService에서는 Controller에 글의 id를 찾아 반환해야한다.

public BoardDTO findById(Long id) {
    BoardEntity entity = boardRepository.findById(id)
    .orElseThrow(() -> new IllegalArgumentException("해당 글이 없습니다. id=" + id));
    return new BoardDTO(entity);
}

해석

컨트롤러에서 findById명령을 받으면, 인자로 받은 id를 가지고 BoardRepository에 접속하여, 일치하는 1개의 인스턴스를 가리키는 참조변수&를 가져와 entity 변수에 저장한다. BoardRepository는 목록 확인 기능 개발의 (3)BoardRepository와 똑같다.

만약, Id를 찾을 수 없다면 해당글이 없습니다.를 반환하며, 있을 경우 Entity를 BoardDTO타입으로 바꿔 반환한다.

(3) detail.html

구현코드

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8" >
    <title>게시글 상세</title>
</head>
<body>
<h2>게시글 상세</h2>
<div>
    <strong>제목:</strong> <span th:text="${board.boardTitle}"></span><br>
    <strong>작성자:</strong> <span th:text="${board.boardWriter}"></span><br>
    <strong>작성일:</strong> <span th:text="${#dates.format(board.createdAt, 'yyyy-MM-dd HH:mm')}"></span><br>
    <strong>조회수:</strong> <span th:text="${board.boardHits}"></span><br>
    <a th:href="@{/edit/check/{id}(id=${board.id})}">글 수정</a>
    <a th:href="@{/delete/check/{id}(id=${board.id})}">글 삭제</a>
    <hr>
    <strong>내용:</strong><br>
    <pre th:text="${board.boardContents}"></pre>
</div>
<a href="/list">목록으로</a>
</body>
</html>

해석

컨트롤러로 부터 board객체를 받아서 이를 이용해 구체적인 내용들을 출력한다.

5. 글 수정기능 만들기

글 수정기능은 detail.html에서 수정 클릭을 통해 가능하다.

<a th:href="@{/edit/check/{id}(id=${board.id})}">글 수정</a>

글을 입력할때 사용한 비밀번호를 /edit/check 페이지에서 검증한 뒤에, 실제 글을 수정할 수 있는 /edit 페이지로 이동한다.

(1) BoardController만들기

비밀번호 확인 구현코드(edit/check)

@GetMapping("/edit/check/{id}")
public String checkEditForm(@PathVariable Long id, Model model) {
    model.addAttribute("board", boardService.findById(id));
    return "edit-check";
}

해석

@PathVariable을 통해서, URL의 {id}값을 매개변수 id에 집어 넣는다. 그 후 boardService에서 행을 가리키는 매개변수를 찾아 모델에 추가하고, edit-check.html로 반환한다.

(2) BoardService 사용하기

public BoardDTO findById(Long id) {
    BoardEntity entity = boardRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("해당 글이 없습니다. id=" + id));
    return new BoardDTO(entity);
    }

해석

이전에 만든 findbyId를 그대로 사용한다. 인자로 받은 id를 이용해 boardRepository에서 그 id를 가리키는 행을 가져와 boardDTO타입으로 반환한다. 이는 다시 컨트롤러로 돌아가 모델에 그 객체를 추가한뒤 edit-check뷰로 보낸다.

(3) edit-check.html

구현코드

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>비밀번호 확인</title>
</head>
<body>
<h2>비밀번호 확인</h2>
<div th:if="${error}" th:text="${error}" style="color:red;"></div>
<form th:action="@{/edit/check/{id}(id=${board.id})}" method="post">
  <input type="password" name="boardPass" placeholder="비밀번호 입력" required>
  <input type="submit" value="확인">
</form>
<a th:href="@{/detail/{id}(id=${board.id})}">취소</a>
</body>
</html>

해석

edit-check.html은 비밀번호가 일치하는지 체크하기 위해 사용된다. 이를 위해 비밀번호를 입력할 수 있게 폼을 제공하고, 그 결과를 /edit/check/{id}주소로 post방식으로 전송한다.

(4) BoardController(/edit/check/{id})

구현 코드

@PostMapping("/edit/check/{id}")
public String checkEdit(@PathVariable Long id,
                        @RequestParam String boardPass,
                        Model model,
                        RedirectAttributes redirectAttributes) {
    BoardDTO board = boardService.findById(id);
    if (!board.getBoardPass().equals(boardPass)) {
        // 비밀번호 불일치 시 에러 메시지와 함께 비번 확인 폼 재출력
        redirectAttributes.addFlashAttribute("error", "비밀번호가 일치하지 않습니다.");
        return "redirect:/edit/check/" + id;
    }
    // 비밀번호 일치 → 수정 폼 이동
    model.addAttribute("board", board);
    return "edit";
}

해석

Post방식으로 form의 내용을 보내면 BoardController의 @Postmapping이 작동한다. 여기서 @PathVariable에 의해 매개변수 id에 URL의 id값이 그대로 입력된다. @RequestParam는 edit-check.html로 받은 비밀번호를 boardPass에 저장하겠다는 의미이며, 이를 사용하기 위해서는 뷰에서 지정한 inpun name과 매개변수명(boardPass)이 일치해야한다.

<input type="password" name="boardPass" placeholder="비밀번호 입력" required>

그 후 BoardService에서 findById를 통해 id인자와 같은 인스턴스(행)를 찾아 board에 넣고, board.getBoardPass()를 통해 BoardPass속성값을 가져올 수 있다. 이를 edit-check.html에서 가져온 boardPass와 같은지 비교하여 비밀번호가 일치하면, board(인스턴스)를 다시 모델에 담아 edit(수정)페이지로 보내며, 일치하지 않으면 error를 출력한다.

참고로 return값에는 {}동적 주소를 넣지 않아야한다. return은 url이 아닌 html파일이기 때문이다.

(5) edit.html

return edit을 통해 모델에 저장된 board 객체가 함께 /edit뷰로 이동한다. edit.html은 게시글을 수정할 form을 제공하고, 유저가 작성한 내용대로 수정이 일어나게끔 처리한다.

구현코드

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>게시글 수정</title>
</head>
<body>
<h2>게시글 수정</h2>
<form th:action="@{/edit/{id}(id=${board.id})}" method="post">
  제목: <input type="text" name="boardTitle" th:value="${board.boardTitle}" required><br>
  내용: <textarea name="boardContents" rows="10" cols="50" th:text="${board.boardContents}"></textarea><br>
  <input type="submit" value="수정">
</form>
<a th:href="@{/detail/{id}(id=${board.id})}">취소</a>
</body>
</html>

해설

사용자가 모든 내용을 입력후 수정버튼을 누르면, form태그에서 /edit/{board.id}주소로 post방식의 데이터를 보낸다.

(5) BoardController(/edit)

구현 코드

@PostMapping("/edit/{id}")
public String edit(@PathVariable Long id, @ModelAttribute BoardDTO boardDTO) {
    boardService.update(id, boardDTO);
    return "redirect:/detail/" + id;
}

해석

edit.html에서 얻은 id를 Post방식으로 받는다. @PathVariable을통해 URL에서 id값을 가져와 매개변수 id에 집어넣으며,@ModelAttribute 객체를 통해 edit.html에서 boardDTO의 객체를 전달받는다. 그 후, boardService의 update함수를 통해 boardDTO내에 저장된 내용을 업데이트 한다. 수정후에는 detail.html로 원래 글로 이동한다.

(6) boardService 만들기

구현코드

public void update(Long id, BoardDTO boardDTO) {
    // 1. 기존 게시글을 DB에서 가져옴
    BoardEntity entity = boardRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("해당 글이 없습니다. id=" + id));

    // 2. 필드값 변경 (원하는 필드만)
    entity.setBoardTitle(boardDTO.getBoardTitle());
    entity.setBoardContents(boardDTO.getBoardContents());

    // 3. 저장
    boardRepository.save(entity);
}

해석

BoardService에서 update기능을 만든것이다. Repository에서 id를 이용한 findByid함수로 인스턴스를 찾아 entity에 저장하고, 이 entity의 값들을 boardDTO의 값들로 제목, 내용 등을 업데이트한다.

6. 글 삭제기능 만들기

detail.html에서는 글 수정 뿐만이 아닌 삭제도 가능하다.

(1) BoardController 만들기

구현코드

// (1) 삭제 비번 입력 폼
@GetMapping("/delete/check/{id}")
public String checkDeleteForm(@PathVariable Long id, Model model) {
    model.addAttribute("board", boardService.findById(id));
    return "delete-check";
}

해석

먼저 get 방식으로 detail.html에서 아래처럼 URL에 {id}를 받아 접속한다.

<a th:href="@{/delete/check/{id}(id=${board.id})}">글 삭제</a>

이 {id}를 @PathVariable을 통해 매개변수 id에 집어넣는다. 그 후 board라는 이름으로 객체를 모델에 저장하여 delete-check.html로 전송한다.

(2) delete-check.html

구현코드

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>비밀번호 확인(삭제)</title>
</head>
<body>
<h2>비밀번호 확인(삭제)</h2>
<div th:if="${error}" th:text="${error}" style="color:red;"></div>
<form th:action="@{/delete/check/{id}(id=${board.id})}" method="post">
  <input type="password" name="boardPass" placeholder="비밀번호 입력" required>
  <input type="submit" value="확인">
</form>
<a th:href="@{/detail/{id}(id=${board.id})}">취소</a>
</body>
</html>

해석

delete-check html에서는 보내진 board객체에 데이터를 입력받아 post방식으로 /delete/check/id 주소로 반환시킨다.

(3) BoardController 만들기

구현코드

@PostMapping("/delete/check/{id}")
public String checkDelete(@PathVariable Long id,
                          @RequestParam String boardPass,
                          RedirectAttributes redirectAttributes) {
    BoardDTO board = boardService.findById(id);
    if (!board.getBoardPass().equals(boardPass)) {
        redirectAttributes.addFlashAttribute("error", "비밀번호가 일치하지 않습니다.");
        return "redirect:/delete/check/" + id;
    }
    // 비밀번호 일치 → 삭제
    boardService.delete(id);
    return "redirect:/list";
}

해석

Post방식으로 delete-check주소로 데이터를 반환받으면, @PathVariable을 통해 매개변수 id에 url의 {id}값을 입력받으며, @RequestParam을 통해 매개변수 boardPass에 delete-check.html에서 받은 boardPass값을 넣는다.

<input type="password" name="boardPass" placeholder="비밀번호 입력" required>

마지막 인자로 RedirectAttributes 클래스를 사용하는데, spring mvc에서는 redirect를 처리할때 기존의 model에 저장되어 있던 객체가 삭제된다. 이를 한번 세션에 저장했다가 재사용하기 위한 용도로 사용하였다.

이 코드에서 만약 비밀번호가 일치하지 않는다면, redirect:/delete/check/" + id; 주소로 처음부터 다시 Post명령을 내린다. 이 때 비밀번호가 일치하지 않는다는 정보를 error라는 이름의 속성명으로 뷰에 전달할 수 있다.

만약 RedirectAttributes 클래스를 사용하지 않았다면 비밀번호를 틀리지않고, 처음 접속해 비밀번호를 입력하지 않은 화면과 동일한 화면이 redirect로 출력되었을 것이다.

그렇게 비밀번호가 틀리면 redirect를 반복하다 비밀번호가 일치한다면, boardService의 delete를 실행한다.

(4) boardService 구현

코드구현

public void delete(Long id) {
    boardRepository.deleteById(id);
}

해석

boardRepository로가서 해당하는 id값(인스턴스)를 삭제한다.

profile
DB가 좋아요

0개의 댓글