IT/development

[springBoot/html] table값 변경 후 DB update(feat.contenteditable)

알 수 없는 사용자 2022. 12. 17.

목차

[springBoot/html] table값 변경 후 DB update(feat.contenteditable)

현재 내부 프로젝트 진행하고 있는 것 중 아래와 같은 요구사항이 있어서 구현을 했고 이를 기록한다.

현재 개발하며 난이도에 상관없이 기록을 하고 있는데 이는 나중에 분명 나의 좋은 자산이 될거라 믿는다.

누군가에겐 쉬운 일이 또 누군가에겐 좌절감을 줄 수도 있기에 좌절을 줄이고 자신감을 늘리기 위함이다.

처음부터 잘하는 사람도 물론 있을 수 있겠지만 그런사람이 그렇게 많지 않다고 본다.

처음엔 어려웠던 게 해보면 나중에 쉬운게 된다.

구글에 존재하면 안해봐서 어려운 것이지 못할 건 없다.

그리고 구글에 없어도 문제를 해결 할 수 있어야 프로다.(구글에 없으면 시간이 더 오래 걸리겠지)

[springBoot/html] table값 변경 후 DB update(feat.contenteditable)

난 아직 부족한 직장인 개발자이기에 평범한 개발자가 되기위해 끊임없이 기록하고 공부할 것이다.


요구사항은 관리자페이지에서 그리드로 데이터를 수정할 수 있어야 된다.

기존에 해왔던 데이터의 키값으로 수정 페이지에서 수정을 하는 방식이 아니라 그리드 내에서 인라인으로 수정을 하는 것이다. 예를 들면 아래와 같은 기능이다.

 

jqGrid Loading Data - Inline Editing with on Row Click

 ...

www.guriddo.net

그리드의 행 선택 후 데이터를 수정하면 DB에 데이터가 반영이 되어야 한다.

해본적 없는 이 기능을 어떻게 구현하면 될까?여러 고민 끝에 아래의 프로세스 정의를 내렸다.


1. 화면단에서는 테이블의 td요소를 편집한다.

2. 저장 버튼을 누를 때 table tr을 루프 돌려 이를 저장한 다음 서버로 전송한다.

3. 서버에서 데이터를 받아서 여러번 DB 업데이트를 수행한다.

이 때 여러번 업데이트 수행하는 방법이 java단에서 업데이트를 여러번 수행하는 것과 mybatis에서 수행하는 것이 있는데 난 후자를 택했다.

이를 위해선 아래처럼 프로퍼티 수정이 필요하다.

#mariadb
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
#allowMultiQueries=true 이렇게 설정을 꼭 해야 한다.
spring.datasource.url=jdbc:log4jdbc:mariadb://localhost:3307/test?characterEncoding=UTF-8&allowMultiQueries=true
spring.datasource.username=test
spring.datasource.password=1234

mybatis.mapper-locations=mybatis-mapper/**/*.xml

contenteditable

구글링 결과 html의 contenteditable속성을 이용하면 입력태그(input, textarea 등)가 아닌 태그를 편집할 수 있도록 해준다. 이놈을 이용할 것이다.

아래에서 테스트 가능하다.

코드를 치기엔 귀찮지만 정확한 예제 테스트를 위해서 코드를 치겠다.

시간관계상 화면 디자인은 없다. 😅

프로젝트 생성 후 간단하게 board 테이블을 하나 만들었고 목록 조회, 등록, 다건 수정만 구현하겠다.

CREATE TABLE `study_board`(
                          `board_seq` bigint auto_increment,
                          `title` varchar (30),
                          `contents` varchar (100),
                          `name` varchar (30),
                          `reg_date` timestamp,
                          `update_date` timestamp,
                          primary key(board_seq)
);

BoardVo

package study.ex1.vo;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class BoardVo {

    private Long boardSeq;
    private String title;
    private String contents;
    private String name;
    private String regDate;
    private String updateDate;
}

mapper xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="study.ex1.mapper.BoardMapper">
    <resultMap id="boardMap" type="study.ex1.vo.BoardVo">
        <id column="board_seq" property="boardSeq"/>
        <result column="title" property="title"/>
        <result column="contents" property="contents"/>
        <result column="name" property="name"/>
        <result column="reg_date" property="regDate"/>
        <result column="update_date" property="updateDate"/>
    </resultMap>
        
    <!-- 원래는 조회 시 *는 지양해야 하지만 예제니까 *로 함 -->
    <select id="findContentsList" resultMap="boardMap">
        SELECT * FROM study_board
    </select>

    <!-- 게시글 등록 -->
    <insert id="insertContents" parameterType="study.ex1.vo.BoardVo">
        INSERT INTO study_board
        (
              title
             ,contents
             ,name
             ,reg_date
             ,update_date
        )
        VALUES
        (
             #{title}
            ,#{contents}
            ,#{name}
            ,SYSDATE()
            ,SYSDATE()
        )
    </insert>

    <!-- 다중 업데이트 -->
    <update id="updateContentsList" parameterType="list">
        <foreach collection="list" item="item" index="index" separator=";">
            UPDATE study_board
            SET   title     = #{item.title}
                 ,contents  = #{item.contents}
                 ,name      = #{item.name}
                 ,update_date = SYSDATE()
            WHERE board_seq = #{item.boardSeq}
        </foreach>
    </update>
                            
</mapper>

mapper interface

package study.ex1.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import study.ex1.vo.BoardVo;

import java.util.List;

@Repository @Mapper
public interface BoardMapper {

    //게시글 목록 조회
    List<BoardVo> findContentsList();
    //게시글 등록
    void insertContents(BoardVo boardVo);
    //게시글 리스트 업데이트
    void updateContentsList(List<BoardVo> boardVoList);
}

service

package study.ex1.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import study.ex1.mapper.BoardMapper;
import study.ex1.vo.BoardVo;

import java.util.List;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class BoardService {

    private final BoardMapper boardMapper;

    /**
     * 게시글 목록 조회
     * @return
     */
    public List<BoardVo> findContentsList() {
        return boardMapper.findContentsList();
    }

    /**
     * 게시글 등록
     * @param boardVo
     */
    @Transactional
    public void insertContents(BoardVo boardVo) throws Exception{
        boardMapper.insertContents(boardVo);
    }

    /**
     * 게시글 다건 수정
     * @param boardVoList
     */
    @Transactional
    public void updateContentsList(List<BoardVo> boardVoList) throws Exception{
        boardMapper.updateContentsList(boardVoList);
    }
}

jUnit 등록, 조회, 수정 테스트

package study.ex1.service;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
import study.ex1.vo.BoardVo;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Slf4j
@Transactional
class BoardServiceTest {

    @Autowired BoardService boardService;

    @Test
    @DisplayName("게시글등록")
    @Commit
    public void 게시글등록() throws Exception {
        for (int i = 0; i < 11; i++) {
            BoardVo boardVo = new BoardVo();
            boardVo.setTitle("제목 : " + i);
            boardVo.setContents("내용 : " + i);
            boardVo.setName("아이언맨");

            boardService.insertContents(boardVo);
        }
    }

    @Test
    @DisplayName("검색")
    @Commit
    public void 검색() throws Exception {
        boardService.findContentsList();
    }

    @Test
    @DisplayName("수정")
    @Commit
    public void 수정() throws Exception {
        List<BoardVo> boardVoList = new ArrayList<>();
        BoardVo boardVo1 = new BoardVo();
        boardVo1.setBoardSeq(1L);
        boardVo1.setTitle("천둥의 신");
        boardVo1.setContents("천둥의 신 토르");
        boardVo1.setName("thor");

        BoardVo boardVo2 = new BoardVo();
        boardVo2.setBoardSeq(2L);
        boardVo2.setTitle("장난의 신");
        boardVo2.setContents("장난의 신 로키");
        boardVo2.setName("loki");

        boardVoList.add(boardVo1);
        boardVoList.add(boardVo2);

        boardService.updateContentsList(boardVoList);

    }

}

최종 DB결과

11건 데이터 등록 했고 그 중 1번과 2번의 데이터 정상적으로 수정됨

[springBoot/html] table값 변경 후 DB update(feat.contenteditable) - undefined - contenteditable

간단히 CRU 테스트 완료 했고 이제 화면단 작업

board_list.html(thymeleaf)

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!--  bootstrap, jquery cdn load -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
    <script  src="http://code.jquery.com/jquery-latest.min.js"></script>
    <title>게시글 목록</title>
</head>
<body>
<div class="container">
    <h2>게시글 목록</h2>
    <div>
        <table class="table table-striped">
            <thead>
            <tr>
                <th>번호</th>
                <th>제목</th>
                <th>내용</th>
                <th>작성자</th>
                <th>작성일</th>
                <th>수정일</th>
            </tr>
            </thead>
            <tbody>
            <tr id="board_tr" th:each="item : ${list}">
                <td th:text="${item.boardSeq}"></td>
                <!--  제목,내용,작성자는 편집 가능 옵션 부여-->
                <td contenteditable="true" th:text="${item.title}"></td>
                <td contenteditable="true" th:text="${item.contents}"></td>
                <td contenteditable="true" th:text="${item.name}"></td>
                <td th:text="${item.regDate}"></td>
                <td th:text="${item.updateDate}"></td>
            </tr>
            </tbody>
        </table>
        <button type="button" id="updateBtn" class="btn btn-primary">수정하기</button>
    </div>
</div> <!-- /.container -->

<script>
    $(document).ready(function () {
        // 업데이트 버튼 클릭 시 update_contents_list() 호출
        $("#updateBtn").on("click", function () {
            update_contents_list();
        });

        function update_contents_list() {
            // 객체 담을 배열
            let tableArr = new Array();
            $("tr#board_tr").each(function (index, item) {
                let td = $(this).children();
                // 테이블 객체
                let td_obj = {
                    boardSeq : td.eq(0).text(),
                    title: td.eq(1).text(),
                    contents : td.eq(2).text(),
                    name : td.eq(3).text()
                };
                // 배열에 객체를 저장
                tableArr.push(td_obj);
            });

            // 비동기로 서버에 업데이트 호출
            $.ajax({
                type : "POST",
                url : "/board/update",
                data: JSON.stringify(tableArr),
                dataType: "JSON",
                contentType: "application/json; charset=UTF-8",
                // 서버로 배열을 넘길 때는 반드시 아래 옵션을 붙이라고 하지만 내 경우는 JSON화한 스트링으로 넘기기 때문에 아래가 없어도 정상 작동한다.
                // traditional: true,
                success : function(data){
                    if(data.resultCode === "01")
                        alert("수정 되었습니다.");
                    location.reload();
                },
                error : function(XMLHttpRequest, textStatus, errorThrown){
                    alert("통신 실패.");
                }
            });
        }
    });
    // End of $(document).ready...
</script>

</body>
</html>

화면에서 하는 일은 간단하다. study_board테이블에 저장된 데이터를 가져와서 뿌리고 제목, 내용, 작성자 편집이 가능하고 수정하기 버튼 클릭 시 번호, 제목, 내용, 작성자를 객체타입으로 배열에 담아서 JSON 변환 후 서버로 넘긴다. 

화면에서 넘기는 데이터 형태는 아래와 같다.(배열안에 객체가 담겨 있다.)

[{"boardSeq":"1","title":"천둥의 신","contents":"망치의 신 토르","name":"thor"},{"boardSeq":"2","title":"장난의 신","contents":"장난의 신 로키","name":"loki"},{"boardSeq":"3","title":"제목 : 2","contents":"내용 : 2","name":"아이언맨"},{"boardSeq":"4","title":"제목 : 3","contents":"내용 : 3","name":"아이언맨"},{"boardSeq":"5","title":"제목 : 4","contents":"내용 : 4","name":"아이언맨"},{"boardSeq":"6","title":"제목 : 5","contents":"내용 : 5","name":"아이언맨"},{"boardSeq":"7","title":"제목 : 6","contents":"내용 : 6","name":"아이언맨"},{"boardSeq":"8","title":"제목 : 7","contents":"내용 : 7","name":"아이언맨"},{"boardSeq":"9","title":"제목 : 8","contents":"내용 : 8","name":"아이언맨"},{"boardSeq":"10","title":"제목 : 9","contents":"내용 : 9","name":"아이언맨"},{"boardSeq":"11","title":"제목 : 10","contents":"내용 : 10","name":"아이언맨"}]

서버에서 이를 @RequestBody로 받아서 업데이트를 수행한다.

JSON 타입의 데이터를 받을 땐 반드시 위 에노테이션을 선언해야 한다. 아니면 값이 null로 들어온다.

controller

package study.ex1.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import study.ex1.service.BoardService;
import study.ex1.vo.BoardVo;

import java.util.HashMap;
import java.util.List;

@Controller
@Slf4j
@RequiredArgsConstructor
@RequestMapping(value = "board")
public class BoardController {

    private final BoardService boardService;

    @GetMapping(value = "/list")
    public String list(Model model) {
        List<BoardVo> contentsList = boardService.findContentsList();
        model.addAttribute("list", contentsList);
        return "/board/board_list";
    }

    @PostMapping(value = "/update")
    @ResponseBody
    public HashMap<String, Object> updateContents(@RequestBody List<BoardVo> boardVoList) {
        HashMap<String, Object> resultMap = new HashMap<>();
        try {
            boardService.updateContentsList(boardVoList);
            resultMap.put("resultCode", "01");
            resultMap.put("result", "success");
        } catch (Exception e) {
            log.info("에러났으니 잘 보고 고쳐~!");
            log.info("exception : {}", e.getMessage());
        }
        return resultMap;
    }
}

아래는 테스트 라이브 동영상이다.(테이블의 데이터를 수정한 내용이 DB에 잘 반영이 된다.)

막상 해보고 나면 정말 간단하지만 기록한다.

이 코드는 좋은 코드가 아니고 당연히 더 좋은 로직이 있을 것이다.

[springBoot/html] table값 변경 후 DB update(feat.contenteditable) - undefined - contenteditable

댓글