IT/development

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

알 수 없는 사용자 2022. 12. 17. 13:00
반응형

목차

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

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

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

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

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

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

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

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


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

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

     

    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 등)가 아닌 태그를 편집할 수 있도록 해준다. 이놈을 이용할 것이다.

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

    See the Pen Untitled by SangYeop Lee (@devLsy) on CodePen.

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

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

    프로젝트 생성 후 간단하게 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번의 데이터 정상적으로 수정됨

    간단히 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에 잘 반영이 된다.)

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

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

    반응형