[스프링 부트] 28. 게시판 v4 SPA, API 구현

lhs's avatar
Nov 25, 2024
[스프링 부트] 28. 게시판 v4 SPA, API 구현

1. API Server

1. Resp.java

@AllArgsConstructor @Data public class Resp<T> { private Boolean success; private String msg; private T body; public static <T> Resp<T> ok(T body) { return new Resp<>(true, "성공", body); } public static <T> Resp<T> fail(String msg) { return new Resp<>(false, msg, null); } }
  • API 요청에 대한 응답을 JSON 형식으로 변환하여 반환하는 클래스
  • 제네릭 <T>를 사용하여 응답 본문(body)을 다양한 타입으로 처리할 수 있다.
  • okfail 메서드를 통해 간단하게 응답 객체를 생성할 수 있다.

2. BoardController.java

@CrossOrigin @RequiredArgsConstructor @RestController public class BoardController { private final BoardService boardService; @GetMapping("/api/board") public Resp<?> list() { List<BoardResponse.DTO> boardList = boardService.게시글목록보기(); return Resp.ok(boardList); } @PostMapping("/api/board") public Resp<?> save(@Valid @RequestBody BoardRequest.SaveDTO saveDTO, Errors errors) { boardService.게시글쓰기(saveDTO); return Resp.ok(null); } @PutMapping("/api/board/{id}") public Resp<?> update(@PathVariable Integer id, @Valid @RequestBody BoardRequest.UpdateDTO updateDTO, Errors errors) { boardService.게시글수정(id, updateDTO); return Resp.ok(null); } @DeleteMapping("/api/board/{id}") public Resp<?> delete(@PathVariable("id") Integer id) { boardService.게시글삭제(id); return Resp.ok(null); } @GetMapping("/api/board/{id}") public Resp<?> detail(@PathVariable("id") Integer id) { BoardResponse.DetailDTO boardDetail = boardService.게시글상세보기(id); return Resp.ok(boardDetail); } }
  • @CrossOrigin은 CORS를 처리하기 위한 어노테이션
  • Resp를 반환하며, 와일드카드(<?>)를 사용해 body에 다양한 타입의 데이터를 처리한다.

3. MyControllerAdvice.java

@RestControllerAdvice public class MyControllerAdvice { @ExceptionHandler(Exception400.class) public ResponseEntity<?> err400(Exception400 e) { ResponseEntity responseEntity = new ResponseEntity(Resp.fail(e.getMessage()), HttpStatus.BAD_REQUEST); return responseEntity; } @ExceptionHandler(Exception404.class) public ResponseEntity<?> err404(Exception404 e) { ResponseEntity responseEntity = new ResponseEntity(Resp.fail(e.getMessage()), HttpStatus.NOT_FOUND); return responseEntity; } }
  • 예외 처리 시 반환 타입을 ResponseEntity로 설정하고, body에는 Resp를 사용하여 메시지를 넣으며, 상태 코드를 설정하여 반환한다.

2. SPA(Single Page Application)

1. index.html

<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>blog</title> </head> <body> <nav> <ul> <li> <a href="javascript:void(0);" onclick="renderList()"></a> </li> <li> <a href="javascript:void(0);" onclick="renderSaveForm()">글쓰기</a> </li> </ul> </nav> <hr> <section id="root"> </section> <script src="js/rendering.js"></script> <script src="js/api.js"></script> <script src="js/main.js"></script> </body> </html>
  • 기존의 Mustache 페이지 대신 단일 HTML 파일만을 사용한다.
  • 하이퍼링크는 작동하지 않게 하고, onclick 이벤트를 사용한다.
  • JavaScript 파일을 분리하여 작성한다.

2. rendering.js

// list 디자인 function renderList() { clear(); let dom = ` <table border="1"> <thead> <tr> <th>번호</th> <th>제목</th> <th></th> </tr> </thead> <tbody id="list-box"> </tbody> </table> `; root.innerHTML = dom; sendList(); } function renderListItem(board) { let dom = ` <td>${board.id}</td> <td>${board.title}</td> <td><a href="javascript:void(0);" onclick="renderDetail(${board.id})">상세보기</a></td> `; let item = document.createElement("tr"); item.innerHTML = dom; return item; } // detail 디자인 async function renderDetail(id) { clear(); let board = await sendDetail(id); state = board; let dom = ` <form> <button type="button" onclick="sendDelete(${board.id});">삭제</button> </form> <form> <button type="button" onclick="renderUpdateForm(${board.id});">수정</button> </form> <div> 번호: ${board.id}<br> 제목: ${board.title}<br> 내용: ${board.content}<br> 작성일: ${board.createdAt}<br> </div> ` root.innerHTML = dom; } // saveForm 디자인 function renderSaveForm() { clear(); let dom = ` <form> <input type="text" id="title" placeholder="제목"><br> <input type="text" id="content" placeholder="내용"><br> <button type="button" onclick="sendSave();">글쓰기</button> </form> `; root.innerHTML = dom; } // updateForm 디자인 async function renderUpdateForm(id) { clear(); let dom = ` <form> <input type="number" value="${state.id}" readonly><br> <input type="text" id="title" placeholder="제목" value="${state.title}"><br> <input type="text" id="content" placeholder="내용" value="${state.content}"><br> <input type="text" value="${state.createdAt}"" readonly><br> <button type="button" onclick="sendUpdate(${state.id});">수정</button> </form> ` root.innerHTML = dom; } // 화면 초기화 function clear() { root.innerHTML = ""; }
  • renderList()
    • 게시글 목록을 렌더링하는 함수
    • clear() 함수로 화면을 초기화한 후, dom 변수에 HTML을 구성하여 root에 삽입한다.
    • sendList() 함수에서 게시글 목록을 받아와 화면을 채운다.
  • renderListItem(board)
    • 각 게시글의 내용을 객체 형태로 만들어 반환하는 함수
    • dom 변수에 HTML을 구성하고, createElement 함수를 사용해 tr 태그 안에 해당 객체를 넣는다.
  • renderDetail(id)
    • 선택한 게시글의 상세 정보를 렌더링하는 함수
    • clear() 함수로 화면을 초기화하고, sendDetail() 함수로 게시글 데이터를 받아온다.
    • 받은 board 객체는 state에 저장해 다른 함수에서 활용할 수 있도록 한다.
    • dom 변수에 HTML을 구성하여 root에 삽입한다.
  • renderSaveForm()
    • 게시글 작성 화면을 렌더링하는 함수
    • clear() 함수로 화면을 초기화한 후, dom 변수에 HTML을 구성하여 root에 삽입한다.
  • renderUpdateForm(id)
    • 게시글 수정 화면을 렌더링하는 함수
    • clear() 함수로 화면을 초기화하고, renderDetail(id)에서 저장한 state를 활용하여 수정할 게시글 데이터를 채운다.
    • dom 변수에 HTML을 구성하여 root에 삽입한다.

3. api.js

async function sendList() { // 1. API 요청 let response = await fetch("http://localhost:8080/api/board"); let responseBody = await response.json(); // 2. 응답 처리 let boards = responseBody.body; let listBox = document.querySelector("#list-box"); boards.forEach(board => { let item = renderListItem(board); listBox.append(item); }); } async function sendDetail(id) { // 1. API 요청 let response = await fetch(`http://localhost:8080/api/board/${id}`); let responseBody = await response.json(); // 2. 응답 처리 return responseBody.body; } async function sendSave() { // 1. 사용자 입력값 받기 let board = { title: document.querySelector("#title").value, content: document.querySelector("#content").value, }; // 2. JSON 변환 let requestBody = JSON.stringify(board); // 3. API 요청 let response = await fetch("http://localhost:8080/api/board", { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, body: requestBody }); let responseBody = await response.json(); // 4. 응답 처리 if (responseBody.success) { renderList(); } else { alert(responseBody.msg); } } async function sendDelete(id) { // 1. API 요청 let response = await fetch(`http://localhost:8080/api/board/${id}`, { method: "DELETE" }); let responseBody = await response.json(); // 2. 응답 처리 if (responseBody.success) { renderList(); } else { alert(responseBody.msg); } } async function sendUpdate(id) { // 1. 사용자 입력값 받기 let board = { title: document.querySelector("#title").value, content: document.querySelector("#content").value, }; // 2. JSON 변환 let requestBody = JSON.stringify(board); // 3. API 요청 let response = await fetch(`http://localhost:8080/api/board/${id}`, { method: "PUT", headers: { "Content-Type": "application/json; charset=utf-8" }, body: requestBody }); let responseBody = await response.json(); // 4. 응답 처리 if (responseBody.success) { renderDetail(id); } else { alert(responseBody.msg); } }
  • 비동기 함수 앞에 await를 붙인다.
  • 비동기 함수를 사용하는 함수 앞에는 async를 붙인다.
  • sendList()
    • API GET 요청을 fetch 함수로 보내고, 결과를 response에 저장한다.
    • response를 JSON 객체로 변환하여 responseBody에 저장한다.
      • json 함수는 비동기 함수이므로 앞에 await를 붙인다.
    • responseBodybodyboards에 저장하고, listBox를 가져와 forEach 문을 사용해 renderListItem(board)를 호출하여 각 항목을 listBox에 추가한다.
  • sendDetail(id)
    • API GET 요청을 보내고, 응답을 처리하여 내용을 반환한다.
  • sendSave()
    • 입력 값을 board 객체로 만든 후 JSON.stringify(board) 함수를 사용해 JSON 문자열로 변환한다.
    • API POST 요청의 body에 JSON 데이터를 추가하여 전송하고, 응답을 처리한다.
  • sendDelete(id)
    • API DELETE 요청을 보내고, 응답을 처리한다.
  • sendUpdate(id)
    • 입력 값을 board 객체로 만든 후 JSON.stringify(board) 함수를 사용해 JSON 문자열로 변환한다.
    • API PUT 요청의 body에 JSON 데이터를 추가하여 전송하고, 응답을 처리한다.

4. main.js

// state let state = {}; // init let root = document.querySelector("#root"); renderList2();
  • 초기화를 하는 스크립트
  • 스크립트 중 가장 마지막에 로드되어야 한다.
Share article

LHS's Study Space