JPA 상세 조회 - JPA sangse johoe

전 포스팅에서 설정해준 파일들을 이용해서 조회 기능을 구현해보려고 한다.

게시글 조회

DB에 데이터는 저장했지만, 저장된 데이터를 불러오지는 못했다. 저장된 데이터를 불러와 조회해보겠다.

Controller에서 '/list' 요청을 받았을 때, list.html로 매핑했었다.

JPA 상세 조회 - JPA sangse johoe

JPA 상세 조회 - JPA sangse johoe

list.html에서는 boardList라는 것으로 정보를 출력한다. 따라서, Controller에 boardList를 넘겨줘야 DB에 저장된 데이터를 볼 수 있다.

JPA 상세 조회 - JPA sangse johoe

Controller

Model을 통해 View에 데이터를 전달해 줄 것이다.

BoardDto를 이용해 DB에 저장된 데이터를 List로 불러올 것이다. 실제 로직은 Service에서 구현해준다.

만든 List를 boardList 라는 이름으로 View에 전달해준다.

JPA 상세 조회 - JPA sangse johoe

Service

Service에서 getBoardlist를 구현해준다. getBoardlist는 DB에 저장되어 있는 전체 데이터를 불러온다.

repository에서 모든 데이터를 가져와, 데이터 만큼 반복하면서, BoardDto 타입의 List에 데이터를 파싱하여 집어넣고, 완성된 BoardDto 타입의 List을 리턴해준다.

JPA 상세 조회 - JPA sangse johoe

실행 결과

애플리케이션을 실행하고, 글쓰기를 하면 DB에 저장되고, 해당 데이터를 메인 페이지에서 확인할 수 있다.

JPA 상세 조회 - JPA sangse johoe

게시글 Detail 페이지

각 게시글 별 Detail페이지를 구현해보겠다. detail.html로 연결시켜준다.

detail.html은 boardDto라는 값으로 데이터를 출력해준다.

JPA 상세 조회 - JPA sangse johoe

Controller

각 게시글을 클릭하면, '/post/id값'으로 요청을 한다.
따라서, 각 게시글의 id 값을 받아서 해당 게시글의 요소들만 Dto타입으로 만들어서 전달해줘야한다.

JPA 상세 조회 - JPA sangse johoe

@PathVariable을 통해 요청에 오는 id값을 받아 getPost로 전달한다. getPost는 각 게시글의 정보를 가져오는 기능인데 Service에서 구현해줄 것이다.

model을 통해 boardDto 타입의 데이터를 View에 전달해준다.

Service

게시글의 id 값을 받아 해당 게시글의 정보만 repository에서 findById로 가져온다.

그리고, BoardDto 타입으로 만들어 return 해준다.

JPA 상세 조회 - JPA sangse johoe

실행 결과

게시글 추가 후, 해당 게시글을 클릭하면 디테일 페이지가 보여짐을 볼 수 있다.

JPA 상세 조회 - JPA sangse johoe

reference: https://victorydntmd.tistory.com/327


본 JPA 게시판 프로젝트는 단계별(step by step)로 진행됩니다.


이전 글에서는 게시글 등록을 위한 write 페이지를 구현해 보았습니다.

이번 글에서는 게시글 상세 페이지인 view 페이지를 구현하게 되는데요.

게시글 상세 페이지에 필요한 대표적인 기능들은 다음과 같습니다.


1. 게시글 수정 페이지로 이동할 수 있는 기능

2. 게시글을 삭제하는 기능

3. 게시글의 조회 수를 증가시키는 기능


네, "백문이 불여일견이요, 백견이 불여일각이며, 백각이 불여일행"이지요.

(어디서 주워들은 건 있어가지고...)

그럼, 바로 시작해 보도록 할게요 :)

1. Entity 클래스에 조회 수 증가, 게시글 삭제 기능 추가하기

package com.study.board.entity;

import java.time.LocalDateTime;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // PK

    private String title; // 제목

    private String content; // 내용

    private String writer; // 작성자

    private int hits; // 조회 수

    private char deleteYn; // 삭제 여부

    private LocalDateTime createdDate = LocalDateTime.now(); // 생성일

    private LocalDateTime modifiedDate; // 수정일

    @Builder
    public Board(String title, String content, String writer, int hits, char deleteYn) {
        this.title = title;
        this.content = content;
        this.writer = writer;
        this.hits = hits;
        this.deleteYn = deleteYn;
    }

    /**
     * 게시글 수정
     */
    public void update(String title, String content, String writer) {
        this.title = title;
        this.content = content;
        this.writer = writer;
        this.modifiedDate = LocalDateTime.now();
    }

    /**
     * 조회 수 증가
     */
    public void increaseHits() {
        this.hits++;
    }

    /**
     * 게시글 삭제
     */
    public void delete() {
        this.deleteYn = 'Y';
    }

}

게시글 등록/수정 구현하기에서 다루었던 JPA의 영속성 컨텍스트에 대해 기억하고 계시지요?

increaseHits( )와 delete( )는 update( )와 마찬가지로,

트랜잭션(Transaction)이 종료(Commit)되는 시점에 자동으로 update 쿼리가 실행됩니다 :)

2. BoardService 수정하기

package com.study.board.model;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.study.board.dto.BoardRequestDto;
import com.study.board.dto.BoardResponseDto;
import com.study.board.entity.Board;
import com.study.board.entity.BoardRepository;
import com.study.exception.CustomException;
import com.study.exception.ErrorCode;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class BoardService {

    private final BoardRepository boardRepository;

    /**
     * 게시글 생성
     */
    @Transactional
    public Long save(final BoardRequestDto params) {

        Board entity = boardRepository.save(params.toEntity());
        return entity.getId();
    }

    /**
     * 게시글 수정
     */
    @Transactional
    public Long update(final Long id, final BoardRequestDto params) {

        Board entity = boardRepository.findById(id).orElseThrow(() -> new CustomException(ErrorCode.POSTS_NOT_FOUND));
        entity.update(params.getTitle(), params.getContent(), params.getWriter());
        return id;
    }

    /**
     * 게시글 삭제
     */
    @Transactional
    public Long delete(final Long id) {

        Board entity = boardRepository.findById(id).orElseThrow(() -> new CustomException(ErrorCode.POSTS_NOT_FOUND));
        entity.delete();
        return id;
    }

    /**
     * 게시글 리스트 조회
     */
    public List<BoardResponseDto> findAll() {

        Sort sort = Sort.by(Direction.DESC, "id", "createdDate");
        List<Board> list = boardRepository.findAll(sort);
        return list.stream().map(BoardResponseDto::new).collect(Collectors.toList());
    }

    /**
     * 게시글 상세정보 조회
     */
    @Transactional
    public BoardResponseDto findById(final Long id) {

        Board entity = boardRepository.findById(id).orElseThrow(() -> new CustomException(ErrorCode.POSTS_NOT_FOUND));
        entity.increaseHits();
        return new BoardResponseDto(entity);
    }

}

delete( ) 메서드

@Transactional
public Long delete(final Long id) {
    Board entity = boardRepository.findById(id).orElseThrow(() -> new CustomException(ErrorCode.POSTS_NOT_FOUND));
    entity.delete();
    return id;
}

게시글의 제목, 내용, 작성자를 수정하는 update( )와 유사한 형태의 메서드입니다.

다만, 조회한 Entity를 수정하느냐, 삭제하느냐의 차이만을 가집니다.


다음은 클린 코드(Clean Code)라는 너무나도 고맙고, 훌륭한 책에서 배운(포함된) 내용이에요 :)

1. "하나의 기능(Method)은 한 가지 일만 해야 한다."

2. 코드는 잘 짜여진 문장처럼, 어이가 없을 정도로 읽기 쉽게 작성해야 한다."


너무 뜬금없이 코드에 대해 말씀드리게 되었는데요.

예전에는 CRUD에서 조회(Read)를 제외한 CUD 기능을 하나의 메서드로 처리하는 경우가 대다수였습니다.

굳이 보여드리자면 이러한 형태로 말이죠.

복잡하지 않은 코드이기는 하지만, 로직이 10줄이 훌쩍 넘어가 버렸지요?

/**
 * 유형별 쿼리 실행
 * @param type - 쿼리 유형
 * @param id - 게시글 번호
 */
public boolean executeQueryByType(final String type, final Long id, final BoardRequestDto params) {

    // INSERT
    if (StringUtils.equals(type, "INSERT")) {
        boardRepository.save(params.toEntity());
        return true;
    }

    Board entity = boardRepository.findById(id).orElseThrow(() -> new CustomException(ErrorCode.POSTS_NOT_FOUND));

    switch (type) {
    case "UPDATE": // UPDATE
        entity.update(params.getTitle(), params.getContent(), params.getWriter());
        break;

    case "DELETE": // DELETE
        entity.delete();
        break;
    }

    return true;
}

update( )와 delete( )는 단지 수정이냐, 삭제냐의 차이만을 가지고 있지만,

"하나의 기능(Method)은 한 가지 일만 해야 한다."라는 말이 너무나도 와닿았습니다.

여러분과 저 모두, 모든 개발자들이 지향할 수 있는 방식의 코딩 습관을 들였으면 하는 바람으로

감히 말씀드려 보았습니다 :)

(GONE대는 절대로 아니랍니다 하하하...)

findById( ) 메서드

@Transactional
public BoardResponseDto findById(final Long id) {
    Board entity = boardRepository.findById(id).orElseThrow(() -> new CustomException(ErrorCode.POSTS_NOT_FOUND));
    entity.increaseHits();
    return new BoardResponseDto(entity);
}

update( ), delete( )와 유사하지만, 게시글의 조회 수를 증가시킨 후에 게시글 정보를 리턴합니다.

예전에 말씀드렸듯이, Entity 클래스는 절대로 요청(Request)이나 응답(Response)에 사용되어서는 안 돼요 :)

3. BoardApiController 수정하기

package com.study.board.controller;

import java.util.List;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.study.board.dto.BoardRequestDto;
import com.study.board.dto.BoardResponseDto;
import com.study.board.model.BoardService;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class BoardApiController {

    private final BoardService boardService;

    /**
     * 게시글 생성
     */
    @PostMapping("/boards")
    public Long save(@RequestBody final BoardRequestDto params) {
        return boardService.save(params);
    }

    /**
     * 게시글 수정
     */
    @PatchMapping("/boards/{id}")
    public Long update(@PathVariable final Long id, @RequestBody final BoardRequestDto params) {
        return boardService.update(id, params);
    }

    /**
     * 게시글 삭제
     */
    @DeleteMapping("/boards/{id}")
    public Long delete(@PathVariable final Long id) {
        return boardService.delete(id);
    }

    /**
     * 게시글 리스트 조회
     */
    @GetMapping("/boards")
    public List<BoardResponseDto> findAll() {
        return boardService.findAll();
    }

    /**
     * 게시글 상세정보 조회
     */
    @GetMapping("/boards/{id}")
    public BoardResponseDto findById(@PathVariable final Long id) {
        return boardService.findById(id);
    }

}

API 컨트롤러에서는 BoardService의 delete( )와 findById( ) 메서드와 URI만 매핑해주면 되겠지요? :)

4. API 호출해보기

상세페이지를 구현하기 전에 Advanced REST client를 이용해서 API 호출 결과를 보여드리도록 할게요.

먼저, 게시글 상세정보를 조회하는 findById( ) 메서드의 호출 결과입니다.

URI의 마지막에는 board 테이블의 PK인 id 값을 지정해 주시면 됩니다.

JPA 상세 조회 - JPA sangse johoe
findById( ) 호출 결과

두 번째는 게시글 정보를 수정하는 update( ) 메서드입니다.

Request Method를 PATCH로, Body content type을 application/json으로 설정하고,

JSON을 파라미터로 전달해 주시면 update( ) 메서드가 실행됩니다.

JPA 상세 조회 - JPA sangse johoe
update( ) 호출 방법

우선, 42번 라인에서 Board 테이블의 entity를 조회합니다.

JPA 상세 조회 - JPA sangse johoe
update( ) 디버깅 모드

43번 라인에서 Board Entity의 update( ) 메서드가 실행되면, 객체의 값이 변경되겠지요?

JPA 상세 조회 - JPA sangse johoe
Board Entity - update( )

마지막으로 id를 리턴해주는 순간에 JPA의 영속성 컨텍스트에 의해서 update 쿼리가 실행됩니다.

JPA 상세 조회 - JPA sangse johoe
update 쿼리 실행 결과

다시 1번 게시글을 조회해보면, 게시글 정보가 수정된 것을 확인하실 수 있어요 :)

JPA 상세 조회 - JPA sangse johoe
findById( ) 호출 결과

세 번째는 게시글을 삭제 처리하는 delete( ) 메서드입니다.

Request Method만 DELETE로 변경해 주시면 됩니다.

JPA 상세 조회 - JPA sangse johoe
delete( ) 호출 방법

delete( )는 update( )와 동일하지만, 단지 실행되는 쿼리가 다를 뿐입니다.

update( )는 제목(title), 내용(content), 작성자(writer) 컬럼의 값을 변경하고,

delete( )는 삭제 여부(delete_yn)를 'Y'로 변경합니다.

JPA 상세 조회 - JPA sangse johoe
update 쿼리 실행 결과

5. 상세 페이지 구현하기

package com.study.board.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/board")
public class BoardPageController {

    /**
     * 게시글 리스트 페이지
     */
    @GetMapping("/list")
    public String openBoardList() {
        return "board/list";
    }

    /**
     * 게시글 등록 페이지
     */
    @GetMapping("/write")
    public String openBoardWrite(@RequestParam(required = false) final Long id, Model model) {
        model.addAttribute("id", id);
        return "board/write";
    }

    /**
     * 게시글 상세 페이지
     */
    @GetMapping("/view/{id}")
    public String openBoardView(@PathVariable final Long id, Model model) {
        model.addAttribute("id", id);
        return "board/view";
    }

}

BoardPageController에 openBoardView( ) 메서드를 추가해 주시고,

openBoardWrite( ) 메서드의 파라미터 영역을 코드 블럭과 동일하게 변경해 주세요 :)

(여러분은 @RequestParam, @PathVariable, Model에 대해서 다들 아실 거예요, 그렇죠?! 맞죠?!)

JPA 상세 조회 - JPA sangse johoe
templates 폴더 구조

컨트롤러에 페이지를 추가로 매핑했으니, HTML 파일을 추가해줄 차례입니다.

templates/board 폴더에 view.html을 추가해 주세요.

(만약, 페이지에 접근해 보신다면, 우선은 URI를 강제로 지정해서 접근해 주세요!)

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorator="layout">

    <th:block layout:fragment="content">
    <div class="card-content">
        <form class="form-horizontal form-view">
    		<div class="form-group">
    			<label for="inp-type-1" class="col-sm-2 control-label">제목</label>
    			<div class="col-sm-10"><p id="title" class="form-control"></p></div>
    		</div>

    		<div class="form-group">
    			<label for="inp-type-2" class="col-sm-2 control-label">이름</label>
    			<div class="col-sm-10"><p id="writer" class="form-control"></p></div>
    		</div>

    		<div class="form-group">
    			<label for="inp-type-5" class="col-sm-2 control-label">내용</label>
    			<div class="col-sm-10"><p id="content" class="form-control"></p></div>
    		</div>

    		<div class="form-group">
    			<label for="inp-type-5" class="col-sm-2 control-label">등록일</label>
    			<div class="col-sm-10"><p id="createdDate" class="form-control"></p></div>
    		</div>

    		<div class="form-group">
    			<label for="inp-type-5" class="col-sm-2 control-label">조회 수</label>
    			<div class="col-sm-10"><p id="hits" class="form-control"></p></div>
    		</div>
    	</form>

    	<div class="btn_wrap text-center">
    		<a href="javascript: void(0);" onclick="goList();" class="btn btn-default waves-effect waves-light">뒤로가기</a>
    		<a href="javascript: void(0);" onclick="goWrite();" class="btn btn-primary waves-effect waves-light">수정하기</a>
    		<button type="button" onclick="deleteBoard();" class="btn btn-danger waves-effect waves-light">삭제하기</button>
    	</div>
    </div>
    <!-- /.card-content -->
    </th:block>

</html>

form-group - div

form-group 클래스가 지정된 div의 두 번째 자식 요소를 보시면,

각각의 p 태그에 제목, 이름, 내용, 등록일, 조회 수의 id가 지정되어 있는데요.

우리는, 작성할 JS 함수에서 id 값을 기준으로 값을 렌더링(매핑)하게 됩니다.

뒤로가기, 수정하기, 삭제하기 버튼

추후에 페이지네이션 기능을 구현했을 때, 페이지 번호나 검색 조건 등의 유지를 위해서

JS 이벤트로 처리합니다.

<th:block layout:fragment="script">
<script th:inline="javascript">
/*<![CDATA[*/

    	window.onload = () => {
     		findBoard();
  	}

      /**
       * 게시글 조회
       */
      function findBoard() {

          const id = /*[[ ${id} ]]*/;

          fetch(`/api/boards/${id}`).then(response => {
          	if (!response.ok) {
      			throw new Error('Request failed...');
      	    }
          	return response.json();

         	}).then(json => {
         		console.table(json);
          	json.createdDate = moment(json.createdDate).format('YYYY-MM-DD HH:mm:ss');

          	Object.keys(json).forEach(key => {
           		const elem = document.getElementById(key);
           		if (elem) {
            			elem.innerText = json[key];
           		}
          	});

         	}).catch(error => {
          	alert('게시글 정보를 찾을 수 없습니다.');
          	goList();
         	});
      }

      /**
       * 뒤로가기
       */
      function goList() {
      	location.href = '/board/list';
      }

      /**
       * 수정하기
       */
      function goWrite() {
      	location.href = `/board/write?id=[[ ${id} ]]`;
      }

      /**
       * 삭제하기
       */
      function deleteBoard() {

      	const id = /*[[ ${id} ]]*/;

          if ( !confirm(`${id}번 게시글을 삭제할까요?`) ) {
          	return false;
          }

          fetch(`/api/boards/${id}`, {
          	method: 'DELETE',
            	headers: { 'Content-Type': 'application/json' },

          }).then(response => {
          	if (!response.ok) {
             		throw new Error('Request failed...');
            	}

            	alert('삭제되었습니다.');
            	goList();

          }).catch(error => {
          	alert('오류가 발생하였습니다.');
          });
  	}

  /*]]>*/
  </script>
</th:block>

다음은 JS 소스 코드입니다.

각 함수들에 대해서 가볍게 설명해 드리고, 전체 소스도 제공해 드릴게요 :)

findBoard( ) 함수

JPA 상세 조회 - JPA sangse johoe
json을 console로 출력한 결과

컨트롤러에서 전달받은 게시글 번호(id)를 이용해서 게시글을 조회합니다.

json.createdDate = moment(json.createdDate).format('YYYY-MM-DD HH:mm:ss');

먼저, json에 담겨있는 등록일(createdDate)의 포맷을 변경합니다.

Object.keys(json).forEach(key => {
    const elem = document.getElementById(key);
    if (elem) {
        elem.innerText = json[key];
    }
});

Object.keys( ) 함수를 이용하면, 객체에 담겨있는 모든 프로퍼티의 Key 값을 배열로 리턴해 주는데요.

각각의 Key는 id부터 modifiedDate까지 게시판 테이블의 모든 컬럼을 포함하고 있겠지요?

우리는 elem이라는 이름의 변수에, 앞에서 말씀드렸던 id 값이 지정된 p 태그를 저장하고,

엘리먼트가 있는 경우, 해당 엘리먼트의 텍스트를 json[key]로 렌더링해 줍니다.

JS에서는 json[key]와 같이, Key 값을 통해서 객체의 Value에 접근할 수 있습니다.

document.getElementById('title').innerText = json.title;
document.getElementById('content').innerText = json.content;
document.getElementById('writer').innerText = json.writer;
document.getElementById('createdDate').innerText = json.createdDate;
document.getElementById('hits').innerText = json.hits;

이런 식으로 각각의 엘리먼트에 접근해서 값을 렌더링 하는,

바보(과거의 도뎡이) 같은 코드를 쓰지 않아도 되는 것이지요 :)

goList( ) 함수

사용자를 리스트 페이지로 보내버리는 함수입니다.

goWrite( ) 함수

사용자를 게시글 등록(수정) 페이지로 보내버리는 함수입니다.

deleteBoard( ) 함수

단지, 게시글을 삭제하는 delete( ) 메서드를 호출하는 역할을 하는 녀석입니다.

게시글이 삭제된 후에는 게시글 리스트 페이지로 리다이렉트 합니다.

method를 'DELETE'로 선언한 것만 기억해 주시면 되겠습니다 :)

6. 등록 페이지 수정하기

/**
 * 게시글 등록(생성/수정)
 */
function save() {

	if ( !isValid() ) {
		return false;
	}

	const form = document.getElementById('form');
	const params = {
		title: form.title.value,
		writer: form.writer.value,
		content: form.content.value,
		deleteYn: 'N'
	};

	const id = /*[[ ${id} ]]*/;
	const uri = ( id ) ? `/api/boards/${id}` : '/api/boards';
	const method = ( id ) ? 'PATCH' : 'POST';

	fetch(uri, {
		method: method,
		headers: { 'Content-Type': 'application/json' },
		body: JSON.stringify(params)

	}).then(response => {
		if (!response.ok) {
			throw new Error('Request failed...');
		}

		alert('저장되었습니다.');
		location.href = '/board/list';

	}).catch(error => {
		alert('오류가 발생하였습니다.');
	});
}

게시글 수정 처리를 위해서 write.html의 save( ) 함수를 수정해 주어야 합니다.

기존 코드에서 id, uri, method라는 이름의 변수가 추가되었는데요.

id는 상세 페이지와 마찬가지로 컨트롤러에서 전달받은 게시글 번호가 되겠지요?

우리는 URI상의 id 포함 여부를 기준으로 수정(PATCH), 생성(POST)을 구분합니다.

window.onload = () => {
    findBoard();
}

/**
 * 게시글 조회
 */
function findBoard() {

    const id = /*[[ ${id} ]]*/;

    if ( !id ) {
    	return false;
    }

    fetch(`/api/boards/${id}`).then(response => {
    	if (!response.ok) {
			throw new Error('Request failed...');
	    }
    	return response.json();

   	}).then(json => {
   		const form = document.getElementById('form');
   		form.title.value = json.title;
   		form.content.value = json.content;
   		form.writer.value = json.writer;

   	}).catch(error => {
    	alert('게시글 정보를 찾을 수 없습니다.');
    	location.href = '/board/list';
   	});
}

상세 페이지와 마찬가지로, 게시글 수정의 경우에는 기존 데이터를 보여주어야 합니다.

여기서는 Object.keys( )를 이용하지 않았는데요.

필드 개수가 많지 않은 상황에서는 반복문보다 효율적일 듯해요 :)

7. 리스트 페이지 수정하기

/**
 * 게시글 리스트 조회
 */
function findAll() {

	fetch('/api/boards').then(response => {
		if (response.ok) {
 					return response.json();
		}
	}).then(json => {
		let html = '';

		if (!json.length) {
			html = '<td colspan="5">등록된 게시글이 없습니다.</td>';
		} else {
			json.forEach((obj, idx) => {
				html += `
					<tr>
  						<td>${json.length - idx}</td>
  						<td class="text-left">
  							<a href="javascript: void(0);" onclick="goView(${obj.id})">${obj.title}</a>
  						</td>
  						<td>${obj.writer}</td>
  						<td>${moment(obj.createdDate).format('YYYY-MM-DD HH:mm:ss')}</td>
  						<td>${obj.hits}</td>
					</tr>
				`;
			});
		}

		document.getElementById('list').innerHTML = html;
	});
}

/**
 * 게시글 조회
 */
function goView(id) {
	location.href = `/board/view/${id}`;
}

findAll( ) 함수

제목을 클릭했을 때, 상세 페이지로 이동할 수 있도록,

a 태그에 goView라는 이름의 이벤트가 추가되었습니다.

goView( ) 함수

전달받은 게시글 번호(id)를 이용해서 게시글 상세 페이지로 이동합니다.

8. 애플리케이션 실행해보기

네, 이로써 상세 페이지 작업이 모두 끝이 났습니다. (짝짝짝)

이제, 테스트를 해봐야겠지요?

우선, 신규 게시글을 하나 등록해 볼게요.

JPA 상세 조회 - JPA sangse johoe

좋아요, 잘 나옵니다!

JPA 상세 조회 - JPA sangse johoe

제목을 클릭했더니, 상세 페이지로 이동했어요. (아이 순조로워~~)

JPA 상세 조회 - JPA sangse johoe

수정하기 버튼을 클릭해서, 등록(수정) 페이지로 이동했어요.

JPA 상세 조회 - JPA sangse johoe

제목, 이름, 내용을 변경한 상태에서 저장해 볼게요!

JPA 상세 조회 - JPA sangse johoe

좋아요, 정상적으로 변경됐습니다 :)

JPA 상세 조회 - JPA sangse johoe

상세 페이지에서도 제대로 출력되고 있구요.

JPA 상세 조회 - JPA sangse johoe

마지막! 삭제하기 버튼을 클릭해 볼게요.

JPA 상세 조회 - JPA sangse johoe

취소를 클릭하면 로직이 종료되고, 확인을 클릭하면 게시글이 정상적으로 삭제됩니다.

JPA 상세 조회 - JPA sangse johoe

에에에?! 리스트 페이지에 삭제된 게시글이 보이고 있어요..

JPA 상세 조회 - JPA sangse johoe

9. BoardRepository에 메서드 추가하기

package com.study.board.entity;

import java.util.List;

import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardRepository extends JpaRepository<Board, Long> {

    /**
     * 게시글 리스트 조회 - (삭제 여부 기준)
     */
    List<Board> findAllByDeleteYn(final char deleteYn, final Sort sort);

}

당연한 이야기이지만, 삭제 여부(deleteYn)가 'N'인,

즉 삭제되지 않은 데이터만 조회하는 기능이 필요합니다.

BoardRepository에 deleteYn을 파라미터로 전달받는 다음의 메서드를 추가해 주세요.

10. BoardService에 메서드 추가하기

/**
 * 게시글 리스트 조회 - (삭제 여부 기준)
 */
public List<BoardResponseDto> findAllByDeleteYn(final char deleteYn) {

    Sort sort = Sort.by(Direction.DESC, "id", "createdDate");
    List<Board> list = boardRepository.findAllByDeleteYn(deleteYn, sort);
    return list.stream().map(BoardResponseDto::new).collect(Collectors.toList());
}

기존에 정의해둔 findAll( ) 밑에 다음의 메서드를 추가해 주세요.

11. BoardApiController 수정하기

/**
 * 게시글 리스트 조회
 */
@GetMapping("/boards")
public List<BoardResponseDto> findAll(@RequestParam final char deleteYn) {
    return boardService.findAllByDeleteYn(deleteYn);
}

기존의 findAll( ) 메서드를 다음과 같이 변경해 주세요.

파라미터로 deleteYn을 전달받고, BoardService의 findAllByDeleteYn을 호출하는 형태로 변경되었습니다.

12. list.html 수정하기

/**
 * 게시글 리스트 조회
 */
function findAll() {

	fetch('/api/boards?deleteYn=N').then(response => {
		if (response.ok) {
 		    return response.json();
		}
	}).then(json => {
		let html = '';

		if (!json.length) {
			html = '<td colspan="5">등록된 게시글이 없습니다.</td>';
		} else {
			json.forEach((obj, idx) => {
				html += `
					<tr>
  						<td>${json.length - idx}</td>
  						<td class="text-left">
  							<a href="javascript: void(0);" onclick="goView(${obj.id})">${obj.title}</a>
  						</td>
  						<td>${obj.writer}</td>
  						<td>${moment(obj.createdDate).format('YYYY-MM-DD HH:mm:ss')}</td>
  						<td>${obj.hits}</td>
					</tr>
				`;
			});
		}

		document.getElementById('list').innerHTML = html;
	});
}

list.html의 findAll( ) 함수가 호출하는 URI만 변경해 주시면 되는데요.

기존 URI에서 "deleteYn=N" 쿼리 스트링(Query String)이 추가되었습니다.

13. 리스트 페이지 확인해보기

JPA 상세 조회 - JPA sangse johoe

이제, 삭제한 2번 게시글은 리스트 페이지에 보이지 않네요 :)

마무리

이렇게 해서, 게시글 상세 페이지(수정/삭제) 구현이 끝이 났습니다. (짝짝짝)

다음 글에서는 리스트 페이지에 페이지네이션을 적용해볼 예정인데요.

일반적인 폼 서브밋 방식이 아닌,

비동기 방식으로, 필요한 데이터만 조회해서 화면에 렌더링 하는 방법을 설명해 드리려 합니다.

오늘도 방문해 주셔서 정말 감사드립니다!

다음 글에서 뵐게요 :)


진행에 어려움을 겪으시는 분들이 계실 수 있으니, 프로젝트를 첨부해 드리도록 하겠습니다.

application.properties의 데이터베이스 정보만 내 PC 환경과 일치하도록 변경해서 사용해 주세요 :)


Board.zip

2.15MB