Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[비트캠프] 88일차(19주차1일) - Spring Framework: myapp-64(라이브러리 적용: Handlebars, Bootstrap) 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Spring Framework, Spring Boot

[비트캠프] 88일차(19주차1일) - Spring Framework: myapp-64(라이브러리 적용: Handlebars, Bootstrap)

끈기JK 2023. 3. 13. 18:24

 

### 64. Back-end 와 Front-end 분리하기: 클라이언트 렌더링 방식으로 전환

 

@PutMapping 에 멱등성(idempotent) 적용

 

우선, WEB-INF 의 defs, jsp, thymeleaf, tiles 폴더 삭제한다.

PUT request 는 여러번 시도 하더라도 결과가 동일하다 = 멱등(idempotent)하다.

PUT, DELETE 를 /boards/{no} 형식으로 요청하도록 변경한다.

package bitcamp.myapp.controller;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import bitcamp.myapp.service.BoardService;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
import bitcamp.myapp.vo.Member;
import bitcamp.util.ErrorCode;
import bitcamp.util.RestResult;
import bitcamp.util.RestStatus;

@RestController
@RequestMapping("/boards")
public class BoardController {

  // 입력: POST   => /boards
  // 목록: GET    => /boards
  // 조회: GET    => /boards/{no}
  // 변경: PUT    => /boards/{no}
  // 삭제: DELETE => /boards/{no}

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("BoardController 생성됨!");
  }

  @Autowired private ServletContext servletContext;
  @Autowired private BoardService boardService;

  @PostMapping
  public Object insert(
      Board board,
      List<MultipartFile> files,
      HttpSession session) throws Exception{

    Member loginUser = (Member) session.getAttribute("loginUser");

    Member writer = new Member();
    writer.setNo(loginUser.getNo());
    board.setWriter(writer);

    List<BoardFile> boardFiles = new ArrayList<>();
    for (MultipartFile file : files) {
      if (file.isEmpty()) {
        continue;
      }

      String filename = UUID.randomUUID().toString();
      file.transferTo(new File(servletContext.getRealPath("/board/upload/" + filename)));

      BoardFile boardFile = new BoardFile();
      boardFile.setOriginalFilename(file.getOriginalFilename());
      boardFile.setFilepath(filename);
      boardFile.setMimeType(file.getContentType());
      boardFiles.add(boardFile);
    }
    board.setAttachedFiles(boardFiles);

    boardService.add(board);

    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @GetMapping
  public Object list(String keyword) {
    log.debug("BoardController.list() 호출됨!");

    // MappingJackson2HttpMessageConverter 가 jackson 라이브러리를 이용해
    // 자바 객체를 JSON 문자열로 변환하여 클라이언트로 보낸다.
    // 이 컨버터를 사용하면 굳이 UTF-8 변환을 설정할 필요가 없다.
    // 즉 produces = "application/json;charset=UTF-8" 를 설정하지 않아도 된다.
    return new RestResult()
        .setStatus(RestStatus.SUCCESS)
        .setData(boardService.list(keyword));
  }

  @GetMapping("{no}")
  public Object view(@PathVariable int no) {
    Board board = boardService.get(no);
    if (board != null) {
      return new RestResult()
          .setStatus(RestStatus.SUCCESS)
          .setData(board);
    } else {
      return new RestResult()
          .setStatus(RestStatus.FAILURE)
          .setErrorCode(ErrorCode.rest.NO_DATA);
    }
  }

  @PutMapping("{no}")
  public Object update(
      @PathVariable int no,
      Board board,
      List<MultipartFile> files,
      HttpSession session) throws Exception {

    Member loginUser = (Member) session.getAttribute("loginUser");

    // URL 의 번호와 요청 파라미터의 번호가 다를 경우를 방지하기 위해
    // URL의 번호를 게시글 번호로 설정한다.
    board.setNo(no);

    Board old = boardService.get(board.getNo());
    if (old.getWriter().getNo() != loginUser.getNo()) {
      return new RestResult()
          .setStatus(RestStatus.FAILURE)
          .setErrorCode(ErrorCode.rest.UNAUTHORIZED)
          .setData("권한이 없습니다.");
    }

    List<BoardFile> boardFiles = new ArrayList<>();
    for (MultipartFile file : files) {
      if (file.isEmpty()) {
        continue;
      }

      String filename = UUID.randomUUID().toString();
      file.transferTo(new File(servletContext.getRealPath("/board/upload/" + filename)));

      BoardFile boardFile = new BoardFile();
      boardFile.setOriginalFilename(file.getOriginalFilename());
      boardFile.setFilepath(filename);
      boardFile.setMimeType(file.getContentType());
      boardFile.setBoardNo(board.getNo());
      boardFiles.add(boardFile);
    }
    board.setAttachedFiles(boardFiles);

    boardService.update(board);

    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @DeleteMapping("{no}")
  public Object delete(@PathVariable int no, HttpSession session) {
    Member loginUser = (Member) session.getAttribute("loginUser");

    Board old = boardService.get(no);
    if (old.getWriter().getNo() != loginUser.getNo()) {
      return new RestResult()
          .setStatus(RestStatus.FAILURE)
          .setErrorCode(ErrorCode.rest.UNAUTHORIZED)
          .setData("권한이 없습니다.");
    }
    boardService.delete(no);

    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @DeleteMapping("{boardNo}/files/{fileNo}")
  public Object filedelete(
      @PathVariable int boardNo,
      @PathVariable int fileNo,
      HttpSession session) {
    Member loginUser = (Member) session.getAttribute("loginUser");
    Board old = boardService.get(boardNo);

    if (old.getWriter().getNo() != loginUser.getNo()) {
      return new RestResult()
          .setStatus(RestStatus.FAILURE)
          .setErrorCode(ErrorCode.rest.UNAUTHORIZED)
          .setData("권한이 없습니다.");

    } else {
      boardService.deleteFile(fileNo);
      return new RestResult()
          .setStatus(RestStatus.SUCCESS);
    }
  }

}

 

insert(@RequestBody Student student) 에서 index.html 의 <input name="name"> 이 있으면 index.js 에서 JSON 형식으로 변환한 formData 요청할 때 "name" 으로 요청이 된다. 이를 "name" 프로퍼티가 있는 Student 객체에 @RequestBody 붙이면 값을 매핑해준다.

package bitcamp.myapp.controller;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import bitcamp.myapp.service.StudentService;
import bitcamp.myapp.vo.Student;
import bitcamp.util.RestResult;
import bitcamp.util.RestStatus;

@RestController
@RequestMapping("/students")
public class StudentController {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("StudentController 생성됨!");
  }

  @Autowired private StudentService studentService;

  @PostMapping
  public Object insert(@RequestBody Student student) {
    studentService.add(student);
    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @GetMapping
  public Object list(String keyword) {
    return new RestResult()
        .setStatus(RestStatus.SUCCESS)
        .setData(studentService.list(keyword));
  }

  @GetMapping("{no}")
  public Object view(@PathVariable int no) {
    return new RestResult()
        .setStatus(RestStatus.SUCCESS)
        .setData(studentService.get(no));
  }

  @PutMapping("{no}")
  public Object update(
      @PathVariable int no,
      @RequestBody Student student) {

    log.debug(student);

    // 보안을 위해 URL 번호를 게시글 번호로 설정한다.
    student.setNo(no);

    studentService.update(student);
    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @DeleteMapping("{no}")
  public Object delete(@PathVariable int no) {
    studentService.delete(no);
    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }
}

 

TeacherController.java 도 동일하다.

package bitcamp.myapp.controller;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import bitcamp.myapp.service.TeacherService;
import bitcamp.myapp.vo.Teacher;
import bitcamp.util.RestResult;
import bitcamp.util.RestStatus;

@RestController
@RequestMapping("/teachers")
public class TeacherController {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("TeacherController 생성됨!");
  }

  @Autowired private TeacherService teacherService;

  @PostMapping
  public Object insert(@RequestBody Teacher teacher) {
    teacherService.add(teacher);
    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @GetMapping
  public Object list() {
    return new RestResult()
        .setStatus(RestStatus.SUCCESS)
        .setData(teacherService.list());
  }

  @GetMapping("{no}")
  public Object view(@PathVariable int no) {
    return new RestResult()
        .setStatus(RestStatus.SUCCESS)
        .setData(teacherService.get(no));
  }

  @PutMapping("{no}")
  public Object update(
      @PathVariable int no,
      @RequestBody Teacher teacher) {

    log.debug(teacher);

    teacher.setNo(no);
    teacherService.update(teacher);

    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

  @DeleteMapping("{no}")
  public Object delete(@PathVariable int no) {
    teacherService.delete(no);
    return new RestResult()
        .setStatus(RestStatus.SUCCESS);
  }

}

 

RootConfig.java에서 TilesConfigurer 및 templateResolver, templateEngine 메서드 필요 없으므로 삭제한다.

package bitcamp.myapp.config;

import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;


@ComponentScan(
    value = "bitcamp.myapp",
    excludeFilters = {
        @Filter(
            type = FilterType.REGEX,
            pattern = {"bitcamp.myapp.controller.*"})
    })
@PropertySource("classpath:/bitcamp/myapp/config/jdbc.properties")
@MapperScan("bitcamp.myapp.dao")
@EnableTransactionManagement
public class RootConfig {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("RootConfig 생성됨!");
  }

  @Bean
  public DataSource dataSource(
      @Value("${jdbc.driver}") String jdbcDriver,
      @Value("${jdbc.url}") String url,
      @Value("${jdbc.username}") String username,
      @Value("${jdbc.password}") String password) {
    log.trace("DataSource 생성됨!");
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName(jdbcDriver);
    ds.setUrl(url);
    ds.setUsername(username);
    ds.setPassword(password);
    return ds;
  }

  @Bean
  public PlatformTransactionManager transactionManager(DataSource dataSource) throws Exception {
    log.trace("PlatformTransactionManager 객체 생성! ");
    return new DataSourceTransactionManager(dataSource);
  }

  @Bean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext appCtx) throws Exception {
    log.trace("SqlSessionFactory 객체 생성!");

    // Mybatis 로깅 기능을 활성화시킨다.
    org.apache.ibatis.logging.LogFactory.useLog4J2Logging();

    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource);
    factoryBean.setTypeAliasesPackage("bitcamp.myapp.vo");
    factoryBean.setMapperLocations(appCtx.getResources("classpath*:bitcamp/myapp/mapper/*Mapper.xml"));
    return factoryBean.getObject();
  }
}

 

AppConfig.java, AdminConfig.java 에서 JSP, Tiles, Thymeleaf Resolver 메서드 필요 없으므로 삭제한다.

package bitcamp.myapp.config;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import bitcamp.myapp.controller.StudentController;
import bitcamp.myapp.controller.TeacherController;
import bitcamp.myapp.web.interceptor.AuthInterceptor;

//@Configuration

@ComponentScan(
    value = "bitcamp.myapp.controller",
    excludeFilters = {
        @Filter(
            type = FilterType.ASSIGNABLE_TYPE,
            classes = {StudentController.class, TeacherController.class})
    })

@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("AppConfig 생성됨!");
  }

  @Bean
  public MultipartResolver multipartResolver() {
    log.trace("MultipartResolver 생성됨!");
    return new StandardServletMultipartResolver();
  }

  // WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    log.trace("AppConfig.addInterceptors() 호출됨!");
    registry.addInterceptor(new AuthInterceptor()).excludePathPatterns("/auth/**");
  }
}
package bitcamp.myapp.config;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import bitcamp.myapp.controller.AuthController;
import bitcamp.myapp.controller.BoardController;
import bitcamp.myapp.controller.DownloadController;
import bitcamp.myapp.web.interceptor.AdminCheckInterceptor;
import bitcamp.myapp.web.interceptor.AuthInterceptor;

//@Configuration

@ComponentScan(
    value = "bitcamp.myapp.controller",
    excludeFilters = {
        @Filter(
            type = FilterType.ASSIGNABLE_TYPE,
            classes = {AuthController.class, BoardController.class, DownloadController.class})
    })
@EnableWebMvc // 프론트 컨트롤러 각각에 대해 설정해야 한다.
public class AdminConfig implements WebMvcConfigurer {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("AdminConfig 생성됨!");
  }

  // WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    log.trace("AdminConfig.addInterceptors() 호출됨!");
    registry.addInterceptor(new AuthInterceptor());
    registry.addInterceptor(new AdminCheckInterceptor());
  }
}

 

 

 

Handlebars 설치

 

Handlebars 라이브러리 이점은 가독성, 유지보수성, 재사용성, 성능, 확장성이다.

javascript 파일에 html 태그를 추가하지 않고, html 파일에 HTML 태그를 넣어놓고 javascript 에서 fetch 로 데이터 가져올때 태그에 값 집어넣는다.

 

 

cmd 로 webapp 폴더에서 아래 명령어 실행한다.

C:\Users\bitcamp\git\bitcamp-ncp\myapp\app-server\src\main\webapp>npm install handlebars --save

node_modules 에 handlebars 폴더 생성된다.

 

 

handlebars 참고 사이트 : https://handlebarsjs.com/guide/

 

 

 

Handlebars 실습 예제 소스

 

 

eomcs-java\eomcs-web\app\src\main\resources\static\handlebars

 

 

exam01.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>exam</title>
</head>
<body>
  <h1>HandlebarsJS 라이브러리 준비(handlebarsjs.com)</h1>
<pre>
1. 'npm install handlebars --save' 실행
3. handlebars.min.js 자바스크립트 추가
</pre>

<script src="../node_modules/handlebars/dist/handlebars.min.js"></script>

</body>
</html>

 

 

exam02.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>exam</title>
</head>
<body>
  <h1>Handlebars 사용전</h1>
  <button id="btn1" type="button">데이터를 가져와서 출력하기</button>
<div id="d1"></div>

<script src="../node_modules/handlebars/dist/handlebars.min.js"></script>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<script>
var data = {
  no:1, 
  title:"제목1", 
  writer:"홍길동"
};

// 태그에 삽입할 HTML 내용을 다음과 같이 자바스크립트 코드로 작성한다. 
var str = "<table border='1'>" + 
"<tbody>" + 
"<tr>" + 
"  <th>번호</th>" + 
"  <td>" + data.no + "</td>" + 
"</tr>" + 
"<tr>" + 
"  <th>제목</th>" + 
"  <td>" + data.title + "</td>" + 
"</tr>" + 
"<tr>" + 
"  <th>작성자</th>" + 
"  <td>" + data.writer + "</td>" + 
"</tr>" + 
"</tbody>" + 
"</table>"; 

var btn1Button = document.querySelector("#btn1");
btn1Button.onclick = (event) => {
  document.querySelector("#d1").innerHTML = str;
};

</script>
</body>
</html>

 

 

exam03.html

.html 맨 밑에 handlebars 를 위한 <script> 를 준비한다. type="text/x-handlebars-template" 으로 한다.

HTML 태그 작성 후 원하는 객체의 필드명을 {{ }} 안에 적는다.

그 후 아래에 handlebars 라이브러리를 <script src=" "> 으로 불러온다.

handlebars 적용할 HTML 소스를 객체로 가져와서 Handlerbars.compile( ) 안에 넣어 함수를 준비한다. 함수 매개변수에 data 를 넣어 html 태그를 만들고 이를 원하는 곳에 집어넣는다.

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>exam</title>
  <style>
  #d1 {
    border: 1px solid red;
    min-height: 200px;
  }
  </style>
</head>
<body>
  <h1>Handlebars 사용후</h1>
  <button id="btn1" type="button">script 태그의 HTML을 읽어서 div에 넣기</button>
<div id="d1"></div>

<script id="t1" type="text/x-handlebars-template">
<table border="1">
<tbody>
  <tr>  
    <th>번호</th>  
    <td>{{no}}</td>
  </tr>
  <tr>  
    <th>제목</th>  
    <td>{{title}}</td>
  </tr>
  <tr>  
    <th>작성자</th>  
    <td>{{writer}}</td>
  </tr>
</tbody>
</table>
</script>

<script src="../node_modules/handlebars/dist/handlebars.min.js"></script>
<script>
var data = {
  no:1, 
  title:"제목1", 
  writer:"홍길동"
};

document.querySelector("#btn1").onclick = () => {
  //1) Handlebars 템플릿 엔진이 사용할 HTML 소스를 준비한다.
  //   - 보통 script 태그에 작성한 HTML 소스를 가져온다.
  var templateSrc = document.querySelector("#t1").innerHTML;
  
  //2) 템플릿 소스와 데이터를 가지고 HTML을 생성할 함수를 준비한다.
  var htmlGenerator = Handlebars.compile(templateSrc);
  
  //3) 템플릿을 처리하는 함수를 호출한다.
  //   - 이 함수는 파라미터로 받은 데이터를 사용하여 템플릿 소스의 지정된 위치에 값을 삽입한다.
  //   - 그리고 최종 생성된 HTML을 리턴한다.
  var html = htmlGenerator(data);
  
  //4) 템플릿 함수가 리턴한 HTML을 div 태그에 넣는다.
  document.querySelector("#d1").innerHTML = html;
};

</script>
</body>
</html>

 

 

 

### 64. Back-end 와 Front-end 분리하기: 클라이언트 렌더링 방식으로 전환

 

### 64. Back-end 와 Front-end 분리하기: 클라이언트 렌더링 방식으로 전환
- 요청 핸들러에서 JSON을 리턴하는 방법
- 웹 페이지에서 JSON을 받아 처리하는 방법
- Handlebars 자바스크립트 템플릿 라이브러리 사용법
- Bootstrap의 모달상자 사용법

 

내용이 입력될 태그를 <script> 태그 안에 handlebars 문법으로 작성한다. 이는 밑에서 javascript 에서 template 으로 쓰이는 재료이다.

밑에 <script> 로 handlebars 라이브러리 불러들인다.

밑에 fetch(...) 로 받은 객체에서 값 꺼내 template 내용 채워넣는다. 원하는 태그의 innerHTML 로 설정한다.

<!-- board/list.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>비트캠프 - NCP 1기</title>
  </head>
  <body>
    <h1>게시판</h1>

    <div><a href="form.html">새 글</a></div>

    <table id="board-table" border="1">
      <thead>
        <tr>
          <th>번호</th>
          <th>제목</th>
          <th>작성자</th>
          <th>작성일</th>
          <th>조회수</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>

    <form action="list" method="get">
      <input type="text" name="keyword" value="" />
      <button>검색</button>
    </form>

    <script id="tr-template" type="text/x-handlebars-template">
      {{#each this}}
        <tr>
          <td>{{no}}</td>
          <td><a href="view.html?no={{no}}">
              {{#if title}}{{title}}{{else}}제목없음{{/if}}</a></td>
          <td>{{writer.name}}</td>
          <td>{{createdDate}}</td>
          <td>{{viewCount}}</td>
        </tr>
      {{/each}}
    </script>

    <script src="../node_modules/handlebars/dist/handlebars.js"></script>
    <script>
      // 템플릿으로 사용할 HTML을 준비한다.
      const html = document.querySelector("#tr-template").innerHTML;

      // HTML을 가지고 템플릿 엔진을 생성한다.
      const templateEngine = Handlebars.compile(html);

      fetch("../app/boards")
        .then((response) => {
          return response.json();
        })
        .then((result) => {
          document.querySelector("#board-table > tbody").innerHTML =
            templateEngine(result.data);
        });
    </script>
  </body>
</html>

 

student 관련 index.html 에서 .css, .js 로 분리한다.

index.html 에 handlebars 적용한다.

getStudent(event) 를 전역 유효 범위(global scope) 에서 찾으므로 index.js 에 작성되어 있어도 사용 가능하다.

<!-- student/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>비트캠프 - NCP 1기</title>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <h1>학생</h1>

    <aside>
      <div>
        <input type="text" name="keyword" value="" />
        <button id="btn-search" type="button">검색</button>
      </div>
      <table id="student-table" border="1">
        <thead>
          <tr>
            <th>번호</th>
            <th>이름</th>
            <th>재직</th>
            <th>전화</th>
          </tr>
        </thead>
        <tbody></tbody>
      </table>
    </aside>

    <article>
      <form id="student-form" action="update" method="post">
        <table border="1">
          <tr class="edit">
            <th>번호</th>
            <td><input id="f-no" type="text" name="no" readonly /></td>
          </tr>
          <tr>
            <th>이름</th>
            <td><input id="f-name" type="text" name="name" /></td>
          </tr>
          <tr>
            <th>이메일</th>
            <td><input id="f-email" type="email" name="email" /></td>
          </tr>
          <tr>
            <th>암호</th>
            <td><input id="f-password" type="password" name="password" /></td>
          </tr>
          <tr>
            <th>전화</th>
            <td><input id="f-tel" type="tel" name="tel" /></td>
          </tr>
          <tr>
            <th>우편번호</th>
            <td><input id="f-postNo" type="text" name="postNo" /></td>
          </tr>
          <tr>
            <th>기본주소</th>
            <td>
              <input id="f-basicAddress" type="text" name="basicAddress" />
            </td>
          </tr>
          <tr>
            <th>상세주소</th>
            <td>
              <input id="f-detailAddress" type="text" name="detailAddress" />
            </td>
          </tr>
          <tr>
            <th>재직여부</th>
            <td>
              <input
                id="f-working"
                type="checkbox"
                name="working"
                value="true"
              />
              재직중
            </td>
          </tr>
          <tr>
            <th>성별</th>
            <td>
              <input type="radio" name="gender" value="M" /> 남
              <input type="radio" name="gender" value="W" checked /> 여
            </td>
          </tr>
          <tr>
            <th>전공</th>
            <td>
              <select id="f-level" name="level">
                <option value="0">비전공자</option>
                <option value="1">준전공자</option>
                <option value="2">전공자</option>
              </select>
            </td>
          </tr>
          <tr class="edit">
            <th>등록일</th>
            <td><span id="f-createdDate"></span></td>
          </tr>
        </table>

        <div>
          <button id="btn-insert" type="button" class="input">추가</button>
          <button id="btn-update" type="button" class="edit">변경</button>
          <button id="btn-delete" type="button" class="edit">삭제</button>
          <button id="btn-cancel" type="reset" class="edit">취소</button>
        </div>
      </form>
    </article>

    <script id="tr-template" type="text/x-handlebars-template">
      {{#each this}}
        <tr data-no="{{no}}" onclick="getStudent(event)">
          <td>{{no}}</td>
          <td>{{name}}</td>
          <td>{{#if working}}예{{else}}아니오{{/if}}</td>
          <td>{{tel}}</td>
        </tr>
      {{/each}}
    </script>

    <script src="../node_modules/handlebars/dist/handlebars.js"></script>
    <script src="index.js"></script>
  </body>
</html>

 

/* index.css */
aside {
  /*border: 1px solid red;*/
  box-sizing: border-box; 
  float: left;
  width: 260px;
}

article {
  /*border: 1px solid black;*/
  box-sizing: border-box;
  margin-left: 270px;
  margin-top: 50px;  
}

#student-table > tbody > tr:hover { 
  background-color: navy;
  color: white;
}

.invisible { 
  display: none;
}

 

목록에서 변경, 삭제, 취소 버튼 보이도록 showInput() 정의하고 실행한다.

e.classList.remove("invisible") 에서 class="invisible" 이 없으면 그냥 무시되므로 에러가 발생하지 않는다.

// 목록에서 변경, 삭제, 취소 버튼 보이도록 정의하고 실행한다.
showInput();
getStudents();

const html = document.querySelector("#tr-template").innerHTML;
const templateEngine = Handlebars.compile(html);

function showInput() {
  let el = document.querySelectorAll(".input");
  for (let e of el) {
    e.classList.remove("invisible");
  }

  el = document.querySelectorAll(".edit");
  for (let e of el) {
    e.classList.add("invisible");
  }
}

function showEdit() {
  let el = document.querySelectorAll(".input");
  for (let e of el) {
    e.classList.add("invisible");
  }

  el = document.querySelectorAll(".edit");
  for (let e of el) {
    e.classList.remove("invisible");
  }
}

// 검색 키보드 입력 마다 SQL 질의하여 검색 결과 목록 바뀌도록 한다.
document.querySelector("input[name='keyword']").onkeyup = (e) => {
  getStudents(e.target.value);
};

// 검색 버튼 클릭시 검색 결과 목록 나오도록 한다.
document.querySelector("#btn-search").onclick = () => {
  getStudents(keyword);
};

// keyword 있을 경우 QueryString 을 함께 요청한다.
function getStudents(keyword) {
  let qs = "";
  if (keyword) {
    qs = `?keyword=${keyword}`;
  }

  fetch("../admin/students" + qs)
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      document.querySelector("#student-table > tbody").innerHTML =
        templateEngine(result.data);
    });
}

function getLevelTitle(level) {
  switch (level) {
    case 0:
      return "비전공자";
    case 1:
      return "준전공자";
    case 2:
      return "전공자";
    default:
      return "기타";
  }
}

function getStudent(e) {
  let no = e.currentTarget.getAttribute("data-no");

  fetch("../admin/students/" + no)
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      if (result.status == "failure") {
        alert("학생을 조회할 수 없습니다.");
        return;
      }

      let student = result.data;
      console.log(student);
      document.querySelector("#f-no").value = student.no;
      document.querySelector("#f-name").value = student.name;
      document.querySelector("#f-email").value = student.email;
      document.querySelector("#f-tel").value = student.tel;
      document.querySelector("#f-postNo").value = student.postNo;
      document.querySelector("#f-basicAddress").value = student.basicAddress;
      document.querySelector("#f-detailAddress").value = student.detailAddress;
      document.querySelector("#f-working").checked = student.working;
      document.querySelector(
        `input[name="gender"][value="${student.gender}"]`
      ).checked = true;
      document.querySelector("#f-level").value = student.level;
      document.querySelector("#f-createdDate").innerHTML = student.createdDate;

      showEdit();
    });
}

document.querySelector("#btn-insert").onclick = () => {
  const form = document.querySelector("#student-form");
  const formData = new FormData(form);

  let json = JSON.stringify(Object.fromEntries(formData));

  fetch("../admin/students", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: json,
  })
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      if (result.status == "success") {
        location.reload();
      } else {
        alert("입력 실패!");
        console.log(result.data);
      }
    })
    .catch((exception) => {
      alert("입력 중 오류 발생!");
      console.log(exception);
    });
};

document.querySelector("#btn-update").onclick = () => {
  const form = document.querySelector("#student-form");
  const formData = new FormData(form);

  // FormData ==> Query String
  // 방법1)
  //let qs = [...formData.entries()].map(x => `${encodeURIComponent(x[0])}=${encodeURIComponent(x[1])}`).join('&');
  // 방법2)
  //let qs = new URLSearchParams(formData).toString();
  //console.log(qs);

  let json = JSON.stringify(Object.fromEntries(formData));
  //console.log(json);

  fetch("../admin/students/" + document.querySelector("#f-no").value, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      //"Content-Type": "application/x-www-form-urlencoded"
    },
    //body: formData
    body: json,
    //body: qs
  })
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      if (result.status == "success") {
        alert("변경 했습니다.");
        location.reload();
      } else {
        alert("변경 실패!");
        console.log(result.data);
      }
    })
    .catch((exception) => {
      alert("변경 중 오류 발생!");
      console.log(exception);
    });
};

document.querySelector("#btn-delete").onclick = () => {
  fetch("../admin/students/" + document.querySelector("#f-no").value, {
    method: "DELETE",
  })
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      if (result.status == "success") {
        location.reload();
      } else {
        alert("학생 삭제 실패!");
      }
    })
    .catch((exception) => {
      alert("학생 삭제 중 오류 발생!");
      console.log(exception);
    });
};

document.querySelector("#btn-cancel").onclick = () => {
  showInput();
};

// entries ==> query string
function toQueryStringFromEntries(entries) {
  let qs = "";
  for (let [key, value] of entries) {
    if (qs.length > 0) {
      qs += "&";
    }
    qs += encodeURIComponent(key) + "=" + encodeURIComponent(value);
  }
  return qs;
}

function toQueryStringFromEntries2(entries) {
  let arr = [];
  for (let entry of entries) {
    arr.push(entry);
  }

  //console.log(arr);

  let arr2 = arr.map(
    (x) => `${encodeURIComponent(x[0])}=${encodeURIComponent(x[1])}`
  );
  //console.log(arr2);

  let str = arr2.join("&");
  //console.log(str);

  return str;
}

function toQueryStringFromEntries3(entries) {
  let arr = [...entries];

  //console.log(arr);

  let arr2 = arr.map(
    (x) => `${encodeURIComponent(x[0])}=${encodeURIComponent(x[1])}`
  );
  //console.log(arr2);

  let str = arr2.join("&");
  //console.log(str);

  return str;
}

 

 

 

Bootstrap 추가

 

모달 상자 사용을 위해 bootstrap 라이브러리 추가한다. 

C:\Users\bitcamp\git\bitcamp-ncp\myapp\app-server\src\main\webapp>npm install bootstrap --save

 

bootstrap 설정 참고 사이트 : https://getbootstrap.com/docs/5.3/getting-started/introduction/

 

Bootstrap에서 제공하는 그리드 시스템을 적용하기 위해 아래 <div class="container"> 로 감싼다. 이는 반응형 웹 디자인 적용할때 편하다.

 

 

 

### 64. Back-end 와 Front-end 분리하기: 클라이언트 렌더링 방식으로 전환

 

### 64. Back-end 와 Front-end 분리하기: 클라이언트 렌더링 방식으로 전환
- 요청 핸들러에서 JSON을 리턴하는 방법
- 웹 페이지에서 JSON을 받아 처리하는 방법
- Handlebars 자바스크립트 템플릿 라이브러리 사용법
- Bootstrap의 모달상자 사용법

 

teacher 는 목록 클릭시 모달창 나오도록 한다.

<head> 에 bootstrap 불러온다. 그 밑에 우리가 작성한 css 불러온다.

<body> 의 자식을 <div class="container"> 로 감싼다.

bootstrap 스타일 적용을 위해 class="btn btn-primary btn-secondary" 등을 지정한다.

 

handlebars 코드 아래쪽에 둔다.

{{#each this}} 에서 list 넘어오는데 반복문 돌며 객체 각각이 this 에 들어간다.

<!-- teacher/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>비트캠프 - NCP 1기</title>
    <link
      rel="stylesheet"
      href="../node_modules/bootstrap/dist/css/bootstrap.css"
    />
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <div class="container">
      <h1>강사</h1>

      <aside>
        <div>
          <button id="btn-new" class="btn btn-primary">새 강사</button>
        </div>
        <table id="teacher-table" border="1">
          <thead>
            <tr>
              <th>번호</th>
              <th>이름</th>
              <th>전화</th>
              <th>학위</th>
              <th>전공</th>
              <th>시강료</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </aside>

      <!-- Modal -->
      <div
        class="modal fade"
        id="teacherModal"
        tabindex="-1"
        aria-labelledby="exampleModalLabel"
        aria-hidden="true"
      >
      <form id="teacher-form" action="update" method="post">
        <div class="modal-dialog">
          <div class="modal-content">
            <div class="modal-header">
              <h1 class="modal-title fs-5" id="exampleModalLabel">
                강사
              </h1>
              <button
                type="button"
                class="btn-close"
                data-bs-dismiss="modal"
                aria-label="Close"
              ></button>
            </div>
            <div class="modal-body">
              
                <table border="1">
                  <tr class="edit">
                    <th>번호</th>
                    <td><input id="f-no" type="text" name="no" readonly /></td>
                  </tr>
                  <tr>
                    <th>이름</th>
                    <td><input id="f-name" type="text" name="name" /></td>
                  </tr>
                  <tr>
                    <th>이메일</th>
                    <td><input id="f-email" type="email" name="email" /></td>
                  </tr>
                  <tr>
                    <th>암호</th>
                    <td>
                      <input id="f-password" type="password" name="password" />
                    </td>
                  </tr>
                  <tr>
                    <th>전화</th>
                    <td><input id="f-tel" type="tel" name="tel" /></td>
                  </tr>
                  <tr>
                    <th>학위</th>
                    <td>
                      <select id="f-degree" name="degree">
                        <option value="1">고졸</option>
                        <option value="2">전문학사</option>
                        <option value="3">학사</option>
                        <option value="4">석사</option>
                        <option value="5">박사</option>
                        <option value="0">기타</option>
                      </select>
                    </td>
                  </tr>
                  <tr>
                    <th>학교</th>
                    <td><input id="f-school" type="text" name="school" /></td>
                  </tr>
                  <tr>
                    <th>전공</th>
                    <td><input id="f-major" type="text" name="major" /></td>
                  </tr>
                  <tr>
                    <th>강의료(시급)</th>
                    <td><input id="f-wage" type="number" name="wage" /></td>
                  </tr>
                  <tr class="edit">
                    <th>등록일</th>
                    <td><span id="f-createdDate"></span></td>
                  </tr>
                </table>
              </form>
            </div>
            <div class="modal-footer">
              <button
                id="btn-insert"
                type="button"
                class="input btn btn-primary"
              >
                추가
              </button>
              <button
                id="btn-update"
                type="button"
                class="edit btn btn-primary"
              >
                변경
              </button>
              <button
                id="btn-delete"
                type="button"
                class="edit btn btn-primary"
              >
                삭제
              </button>
              <button
                id="btn-cancel"
                type="reset"
                class="edit btn btn-secondary"
                data-bs-dismiss="modal"
              >
                취소
              </button>
            </div>
          </div>
        </div>
      </form>
      </div>
    </div>

    <script id="tr-template" type="text/x-handlebars-template">
      {{#each this}}
        <tr data-no="{{no}}" onclick="getTeacher(event)">
          <td>{{no}}</td>
          <td>{{name}}</td>
          <td>{{tel}}</td>
          <td>{{#degreeLabel}}{{degree}}{{/degreeLabel}}</td>
          <td>{{major}}</td>
          <td>{{wage}}</td>
        </tr>
      {{/each}}
    </script>

    <script src="../node_modules/handlebars/dist/handlebars.js"></script>
    <script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="index.js"></script>
  </body>
</html>

 

/* index.css */
aside {
  /*border: 1px solid red;*/
  box-sizing: border-box;
  float: left;
  width: 450px;
}

article {
  /*border: 1px solid black;*/
  box-sizing: border-box;
  margin-left: 460px;
}

#teacher-table > tbody > tr:hover {
  background-color: navy;
  color: white;
}

.invisible {
  display: none;
}

 

handlebars 확장 태그 등록한다.

목록 클릭시 모달창 띄울수 있게 코드 삽입한다.

// index.js
showInput();
getTeachers();

// handlebars 확장 태그 등록
Handlebars.registerHelper("degreeLabel", function (options) {
  return getDegreeLabel(parseInt(options.fn(this)));
});

const html = document.querySelector("#tr-template").innerHTML;
const templateEngine = Handlebars.compile(html);

function showInput() {
  let el = document.querySelectorAll(".input");
  for (let e of el) {
    e.classList.remove("invisible");
  }

  el = document.querySelectorAll(".edit");
  for (let e of el) {
    e.classList.add("invisible");
  }
}

function showEdit() {
  let el = document.querySelectorAll(".input");
  for (let e of el) {
    e.classList.add("invisible");
  }

  el = document.querySelectorAll(".edit");
  for (let e of el) {
    e.classList.remove("invisible");
  }
}

function getTeachers(keyword) {
  let qs = "";
  if (keyword) {
    qs = `?keyword=${keyword}`;
  }

  fetch("../admin/teachers" + qs)
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      document.querySelector("#teacher-table > tbody").innerHTML =
        templateEngine(result.data);
    });
}

function getDegreeLabel(degree) {
  switch (degree) {
    case 1:
      return "고졸";
    case 2:
      return "전문학사";
    case 3:
      return "학사";
    case 4:
      return "석사";
    case 5:
      return "박사";
    default:
      return "기타";
  }
}

function getTeacher(e) {
  let no = e.currentTarget.getAttribute("data-no");

  fetch("../admin/teachers/" + no)
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      if (result.status == "failure") {
        alert("강사를 조회할 수 없습니다.");
        return;
      }

      let teacher = result.data;
      document.querySelector("#f-no").value = teacher.no;
      document.querySelector("#f-name").value = teacher.name;
      document.querySelector("#f-email").value = teacher.email;
      document.querySelector("#f-tel").value = teacher.tel;
      document.querySelector("#f-degree").value = teacher.degree;
      document.querySelector("#f-school").value = teacher.school;
      document.querySelector("#f-major").value = teacher.major;
      document.querySelector("#f-wage").value = teacher.wage;
      document.querySelector("#f-createdDate").innerHTML = teacher.createdDate;

      showEdit();

      const modal = new bootstrap.Modal("#teacherModal", {});
      modal.show();
    });
}

document.querySelector("#btn-insert").onclick = () => {
  const form = document.querySelector("#teacher-form");
  const formData = new FormData(form);

  let json = JSON.stringify(Object.fromEntries(formData));

  fetch("../admin/teachers", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: json,
  })
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      if (result.status == "success") {
        location.reload();
      } else {
        alert("입력 실패!");
        console.log(result.data);
      }
    })
    .catch((exception) => {
      alert("입력 중 오류 발생!");
      console.log(exception);
    });
};

document.querySelector("#btn-update").onclick = () => {
  const form = document.querySelector("#teacher-form");
  const formData = new FormData(form);

  let json = JSON.stringify(Object.fromEntries(formData));

  fetch("../admin/teachers/" + document.querySelector("#f-no").value, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
    },
    body: json,
  })
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      if (result.status == "success") {
        alert("변경 했습니다.");
        location.reload();
      } else {
        alert("변경 실패!");
        console.log(result.data);
      }
    })
    .catch((exception) => {
      alert("변경 중 오류 발생!");
      console.log(exception);
    });
};

document.querySelector("#btn-delete").onclick = () => {
  fetch("../admin/teachers/" + document.querySelector("#f-no").value, {
    method: "DELETE",
  })
    .then((response) => {
      return response.json();
    })
    .then((result) => {
      if (result.status == "success") {
        location.reload();
      } else {
        alert("강사 삭제 실패!");
      }
    })
    .catch((exception) => {
      alert("강사 삭제 중 오류 발생!");
      console.log(exception);
    });
};

document.querySelector("#btn-cancel").onclick = () => {
  showInput();
};

document.querySelector("#btn-new").onclick = () => {
  const modal = new bootstrap.Modal("#teacherModal", {});
  modal.show();
};

 

 

 


 

 

조언

 

*누가 어떤 라이브러리를 많이 알고 잘 사용하느냐에 프로젝트의 질이 달려있다.

*오픈소스 라이브러리는 지구상에 2가지 형태이다. CSS 사용(예: bootstrap)하거나 javascript 사용(예: jQuery)하거나

 

 


 

과제

 

/