본문 바로가기
개발/Spring

[SpringBoot] 메뉴 계층형 구조 만들기(CheckBox, 전체 선택/해제)

by 코딩하는 흰둥이 2024. 7. 31.

https://greed-yb.tistory.com/275

 

[SpringBoot] DataTable 이용하여 추가,수정,삭제 구현하기

https://greed-yb.tistory.com/252 [SpringBoot] DataTable(JavaScript) - 적용하기이전글 이어서https://greed-yb.tistory.com/251 [SpringBoot] Thymeleaf - layout 적용하기https://greed-yb.tistory.com/218#google_vignette [Spring] Bootstrap SB Admin

greed-yb.tistory.com

이전글에 기능에 연관되어 있어서 참고 바란다

 

 

완성 화면

 

 

 

html
<style>
	/* 선택된 권한 dataTable 행의 배경색 */
    tr.selected {
        background-color: #f1f1f1;
    }
</style>



<div class="col-sm-8">
    <!-- DataTales Example -->
    <div class="card shadow mb-4">
        <div class="card-header py-3" style="text-align: left">
            <div class="row">
                <div class="col-6">
                    <h5 class="m-0 font-weight-bold text-dark">메뉴</h5>
                </div>
                <div class="col-6">
                    <button id="menuSave" class="btn btn-info float-right"
                            type="button" onclick="menuSave()">저장
                    </button>
                </div>
            </div>
        </div>
        <div class="card-body">
            <div class="form-group">
                <table class="table asm table-bordered" id="treeMenuTable" style="width:100%">
                    <tbody style="border: none">
                    <tr>
                        <td>
                            <input onclick='selectAllcheck(this)' type='checkbox' name='selectall' id='selectAll'> 전체
                        </td>
                    </tr>
                    </tbody>

                </table>
            </div>
        </div>
    </div>
</div>

<td> </td> 안에 있는

<input onclick='selectAllcheck(this)' type='checkbox' name='selectall' id='selectAll'> 전체

코드는 DB에 있는 데이터를 가져오기 전에 일부로 미리 생성해 놓은 건데

상황에 맞게 추가 및 지우고 사용하면 되겠다

 

 

javascript
/**
 * 메뉴 체크 변수
 */
let menu;           // 체크 된 메뉴 확인 용 변수    
let menuArr;        // 체크 된 메뉴를 담기 위한 변수
let roleId;         // 선택된 권한의 id 변수


// 권한 정보 DataTable에 넣기
function RoleInit(data){
    role_dataTable = $("#role_dataTable").DataTable({
        data: data,                                                     // 가져온 데이터
        "paging": true,                                                 // 페이징
        "lengthMenu": [[5,10, 50, 100, -1], [5,10, 50, 100, "All"] ], // 목록 개수
        "pageLength": 5,                                               // 보여지는 기본 개수
        "searching": true,                                              // 검색 기능
        "info": true,                                                   // 정보 표시
        // "scrollX": false,                                               // 가로 스크롤(true , false)
        // "scrollY": false,                                               // 새로 스크롤(false , px 단위 : 200 )
        "ordering": false,                                               // 정렬 기능
        // "order": [0 , "ASC"],                                           // 정렬 기준
        "language": {                                                   // 언어 설정
            decimal:        "",
            emptyTable:     "검색된 데이터가 없습니다.",
            info:           "Showing _START_ to _END_ of _TOTAL_ entries",
            infoEmpty:      "Showing 0 to 0 of 0 entries",
            infoFiltered:   "(filtered from _MAX_ total entries)",
            infoPostFix:    "",
            thousands:      ",",
            lengthMenu:     " _MENU_ ",
            loadingRecords: "Loading...",
            processing:     "Processing...",
            search:         "Search",
            zeroRecords:    "항목이 존재하지 않습니다",
            paginate: {
                "first":      "처음",
                "last":       "마지막",
                "next":       "다음",
                "previous":   "이전"
            },
            aria: {
                sortAscending:  ": activate to sort column ascending",
                sortDescending: ": activate to sort column descending"
            }
        },
        "columnDefs": [
        ],
        "columns":[
            {
                data: "roleId",
                render: function (data, type, row){
                    return data;
                }
            },
            {
                data: "roleName",
                render: function (data, type, row){
                    return '<input class="form-control form-control-user" id="roleName" value=' + data + '>';
                }
            },
            {
                data: null,
                render: function (data, type, row){
                    return '<a name="save" class="btn btn-success btn-circle mx-1 my-1"><i class="fas fa-save"> </i></a>' +
                        '<a name="del" class="btn btn-danger btn-circle"><i class="fas fas fa-trash"> </i></a>';
                }
            }
        ]
    });

    // 버튼 제어
    $("#role_dataTable tbody").on("click", "a", function (){
      let tr = $(this).closest("tr");
      let row = role_dataTable.row(tr);
      let aName = $(this).attr('name');

      if(aName == 'save'){
          saveRole(tr);
      }else if(aName == 'del'){
          deleteRole(row);
      }
    });



    /* 선택된 tr 행 색상 변경 및 권한에 해당하는 메뉴 정보 가져오기*/
    $('#role_dataTable tbody').on('click', 'tr', function () {
        let tr = $(this).closest('tr');
        let row = role_dataTable.row(tr);

        if (tr.hasClass('selected')) {
            tr.removeClass('selected');
            roleId = null;
        } else {
            role_dataTable.$('tr.selected').removeClass('selected');//테이블에서 선택된 것 삭제
            tr.addClass('selected');
            
            //선택 ajax 호출
            roleId = row.data().roleId;
            selectMenuList(roleId);
        }
    });
}



/**
 * 메뉴 리스트 가져오기
 */
function menuList(){
    $.ajax({
        url : "/menuList",
        type : "GET",
        success : function (data) {
            if(data != null){
                for (let i = 0; i < data.length; i++) {
                    if(data[i].parentId == 0){
                        $("#treeMenuTable").append("<tr data-tt-id=\"" + data[i].id + "\" data-tt-parent-id=\"" + data[i].parentId + "\"><td style='border: 0;' id=\"" + data[i].id + "\"><span class='fas fa-plus plusIcon' style='display:none; border: 0;'></span><span class='fas fa-minus plusIcon'></span>" + "&nbsp;" + "<input onclick='checkMenu(this)' type='checkbox' name='checkMenu' id=\"" + data[i].id + "\">" + "&nbsp;" + data[i].name + "</td></tr>");
                    }else{
                        $("#" + data[i].parentId).append("<tr data-tt-id=\"" + data[i].id + "\" data-tt-parent-id=\"" + data[i].parentId + "\"><td style='width:100%; border: 0;'>" + "&nbsp;&nbsp;&nbsp;&nbsp;" + "<input onclick='checkMenu(this)' type='checkbox' name='checkMenu' id=\"" + data[i].id + "\">" + "&nbsp;" + data[i].name + "</td></tr>");
                    }
                }
            }
        },
        error(e){
            console.log("error : "+ e);
        }
    });
}


/**
 * 테이블의 + - 아이콘 클릭시 제어
 */
$("#treeMenuTable").on("click", ".plusIcon", function () {
    var obj = $(this);
    if (obj.hasClass("fa-plus")) {
        obj.hide();
        obj.next().show();
        obj.parent().children().children().show();
    } else {
        obj.hide();
        obj.prev().show();
        obj.parent().children().children().hide()
    }
});


/**
 * 체크 박스 클릭 시 부모/자식 체크 박스 여부
 * @param checkbox
 */
function checkMenu(checkbox) {
    menu = document.getElementsByName("checkMenu")
    menuArr = [];

    // 전체 체크 되어 있는 상태에서 자식데이터의 체크박스 해제시 전체 체크박스 해제
    const selectall = document.querySelector('input[name="selectall"]');
    if (checkbox != null && checkbox.checked === false) {
        selectall.checked = false;
    }

    // 체크시 자식데이터가 있으면 전체 체크
    let childMenu = $("#" + checkbox.id).find("tr").find("td").find("input");
    if (childMenu.length > 0 && checkbox.checked == true) {
        for (let i = 0; i < childMenu.length; i++) {
            childMenu[i].checked = true;
        }
    } else if (childMenu.length > 0 && checkbox.checked == false) {
        for (let i = 0; i < childMenu.length; i++) {
            childMenu[i].checked = false;
        }
    }

    // 체크시 부모가 있다면 부모 체크
    // 자식 노드이면서 부모 노드가 false 인 경우
    if(checkbox.parentNode.parentNode.dataset.ttParentId == checkbox.parentNode.parentNode.parentNode.id && checkbox.parentNode.parentNode.parentNode.childNodes[3].checked == false){
        checkbox.parentNode.parentNode.parentNode.childNodes[3].checked = true;
    }
    // 자식 노드이면서 부모 노드가 true 일때 자식노드가 모두 체크 해제되면 부모노드도 체크 해제하기
    else if(checkbox.parentNode.parentNode.dataset.ttParentId == checkbox.parentNode.parentNode.parentNode.id && checkbox.parentNode.parentNode.parentNode.childNodes[3].checked == true){

        let childCheck = $("#" + checkbox.parentNode.parentNode.parentNode.id).find("tr").find("td").find("input");
        if (childCheck.length > 0){
            let countCheck = 0;
            for (let i = 0; i < childCheck.length; i++) {
                if(childCheck[i].checked == false){
                    countCheck++;
                }
            }
            if (countCheck == childCheck.length){
                checkbox.parentNode.parentNode.parentNode.childNodes[3].checked = false;
            }
        }

    }
    // 체크되어 있는 데이터
    for (let i = 0; i < menu.length; i++) {
        if (menu[i].checked == true) {
            menuArr.push(menu[i].id);
        }
    }
}

/**
 * 체크박스 모두 선택/해제
 * @param selectAll
 */
function selectAllcheck(selectAll) {
    const checkboxes = document.getElementsByName('checkMenu');

    checkboxes.forEach((checkbox) => {
        checkbox.checked = selectAll.checked
    })
    menu = document.getElementsByName("checkMenu")
    menuArr = [];

    // 체크되어 있는 데이터
    for (let i = 0; i < menu.length; i++) {
        if (menu[i].checked == true) {
            menuArr.push(menu[i].id);
        }
    }
}


/**
 * 선택한 권한의 메뉴 정보 가져오기
 * @param id
 */
function selectMenuList(id) {
    $.ajax({
        type: 'GET',
        url: '/selectMenuList',
        data: {id: id},
        dataType: 'json',
        success: function (data) {
            let inputCount = $("#treeMenuTable").find("input").length;
            menuArr = [];

            for (let i = 0; i < inputCount; i++) {
                $("#treeMenuTable").find("input")[i].checked = false;
            }
            if(data != ''){
                for (let i = 0; i < data.length; i++) {
                    for (let j = 0; j < inputCount; j++) {
                        if ($("#treeMenuTable").find("input")[j].id == data[i].id) {
                            $("#treeMenuTable").find("input")[j].checked = true;
                            menuArr.push(data[i].id);
                        }
                    }
                }
            }
        },error(e){
        }
    })
}


// 선택된 메뉴 저장하기
function menuSave() {
    if (roleId == null || roleId == '') {
        alert("권한을 선택해주세요.");
    }else if(menuArr == null || menuArr == ''){
        alert("메뉴를 선택해주세요.");
    }else{
        $.ajax({
            type: 'POST',
            url: '/saveMenuList',
            data: {"roleId": roleId , "menuArr" : menuArr},
            success: function (data) {
                if(data == "success") {
                    alert("저장 완료 하였습니다.");
                }else {
                    alert("저장 오류가 발생하였습니다.");
                }
            },error(e){
                alert("오류가 발생하였습니다.");
            }

        })
    }
}


$(document).ready(function() {
    // 권한 정보 가져오기
    roleInfo();
    
    // 메뉴 정보 가져오기
    menuList();
});

 

function menuList() 으로 페이지 시작 시 메뉴 데이터를 가져온다

ajax 로 가져온 데이터를 html 에 treeMenuTable table 태그에 append 해주는 방식이다

append 하는 tr 태그에 data 속성을 이용하여 메뉴의 id 및 해당 메뉴의 부모 id 값을 넣어서 가지고 있는다

 

 

 

 

controller
import com.example.practice.service.role.RoleService;
import com.example.practice.vo.RoleVo;
import com.example.practice.vo.menu.MenuVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

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

@RestController
public class RoleController {

    @Autowired
    private RoleService roleService;
    
    
 /**
     * 메뉴 정보 가져오기
     * @return
     * @throws Exception
     */
    @GetMapping("/menuList")
    public List<MenuVo> menuList() throws Exception{
        List<MenuVo> vo = roleService.menuList();
        return vo;
    }


    /**
     * 체크된 권한의 메뉴 정보 가져오기
     * @param id
     * @return
     * @throws Exception
     */
    @GetMapping("/selectMenuList")
    public List<MenuVo> selectMenuList(@RequestParam String id) throws Exception{
        List<MenuVo> vo = roleService.selectMenuList(id);
        return vo;
    }

    /**
     * 선택한 권한에 메뉴 넣기
     * @param roleId
     * @param menuArr
     * @return
     */
    @PostMapping("/saveMenuList")
    public String saveMenuList(@RequestParam String roleId , @RequestParam(value = "menuArr[]") ArrayList<String> menuArr){
        System.err.println("roleId : " + roleId);
        System.err.println("menuArr : " + menuArr.toString());

        try {
            roleService.deleteMenuList(roleId);
            for (int i = 0; i < menuArr.size(); i++) {
                roleService.insertMenuList(roleId, menuArr.get(i));
            }

            return "success";
        }catch (Exception e){
            e.printStackTrace();
            return "fail";
        }
    }
}

 

 

 

vo
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MenuVo {

    private Long id;

    private Long parentId;

    private String name;

    private Long ord;

    private String icon;
}

 

service
import com.example.practice.vo.RoleVo;
import com.example.practice.vo.menu.MenuVo;

import java.util.List;

public interface RoleService {

    /**
     * 권한 정보 가져오기
     * @return
     * @throws Exception
     */
    List<RoleVo> roleInfo() throws Exception;


    /**
     * 권한 정보 추가하기
     * @param vo
     * @throws Exception
     */
    void roleAdd(RoleVo vo) throws Exception;


    /**
     * 권한 정보 수정하기
     * @param vo
     * @throws Exception
     */
    void roleUpdate(RoleVo vo) throws Exception;

    /**
     * 권한 정보 삭제하기
     * @param roleId
     * @throws Exception
     */
    void roleDelete(String roleId) throws Exception;


    /**
     * 메뉴 정보 가져오기
     * @return
     * @throws Exception
     */
    List<MenuVo> menuList() throws Exception;


    /**
     * 선택한 권한의 메뉴 정보 가져오기
     * @param id
     * @return
     * @throws Exception
     */
    List<MenuVo> selectMenuList(String id) throws Exception;


    /**
     * 권한에 연결되어있는 메뉴 삭제
     * @param roleId
     * @throws Exception
     */
    void deleteMenuList(String roleId) throws Exception;

    /**
     * 선택한 권한에 메뉴 넣기
     * @param roleId
     * @param menuId
     */
    void insertMenuList(String roleId , String menuId);

}

 

 

 

mapper.class
import com.example.practice.vo.RoleVo;
import com.example.practice.vo.menu.MenuVo;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface RoleMapper {

    /**
     * 권한 정보 가져오기
     * @return
     * @throws Exception
     */
    List<RoleVo> roleInfo() throws Exception;

    /**
     * 권한 정보 추가하기
     * @param vo
     * @throws Exception
     */
    void roleAdd(RoleVo vo) throws Exception;

    /**
     * 권한 정보 수정하기
     * @param vo
     * @throws Exception
     */
    void roleUpdate(RoleVo vo) throws Exception;

    /**
     * 권한 정보 삭제하기
     * @param roleId
     * @throws Exception
     */
    void roleDelete(String roleId) throws Exception;

    /**
     * 메뉴 정보 가져오기
     * @return
     * @throws Exception
     */
    List<MenuVo> menuList() throws Exception;

    /**
     * 선택한 권한의 메뉴 정보 가져오기
     * @param id
     * @return
     * @throws Exception
     */
    List<MenuVo> selectMenuList(String id) throws Exception;


    /**
     * 권한에 연결되어있는 메뉴 삭제
     * @param roleId
     * @throws Exception
     */
    void deleteMenuList(String roleId) throws Exception;


    /**
     * 선택한 권한에 메뉴 넣기
     * @param roleId
     * @param menuId
     */
    void insertMenuList(String roleId , String menuId);

}

 

 

serviceImpl
import com.example.practice.mapper.role.RoleMapper;
import com.example.practice.service.role.RoleService;
import com.example.practice.vo.RoleVo;
import com.example.practice.vo.menu.MenuVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class RoleServiceImpl implements RoleService {

    @Autowired
    private RoleMapper roleMapper;

    @Override
    public List<RoleVo> roleInfo() throws Exception {
        return roleMapper.roleInfo();
    }

    @Override
    public void roleAdd(RoleVo vo) throws Exception {
        roleMapper.roleAdd(vo);
    }

    @Override
    public void roleUpdate(RoleVo vo) throws Exception {
        roleMapper.roleUpdate(vo);
    }

    @Override
    public void roleDelete(String roleId) throws Exception {
        roleMapper.roleDelete(roleId);
    }

    @Override
    public List<MenuVo> menuList() throws Exception {
        return roleMapper.menuList();
    }

    @Override
    public List<MenuVo> selectMenuList(String id) throws Exception {
        return roleMapper.selectMenuList(id);
    }

    @Override
    public void deleteMenuList(String roleId) throws Exception {
        roleMapper.deleteMenuList(roleId);
    }

    @Override
    public void insertMenuList(String roleId, String menuId) {
        roleMapper.insertMenuList(roleId, menuId);
    }
}

 

 

 

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="com.example.practice.mapper.role.RoleMapper">
    
    <!-- 메뉴 정보 가져오기 -->
    <select id="menuList" resultType="com.example.practice.vo.menu.MenuVo">
        SELECT
            ID,
            PARENT_ID,
            NAME,
            ORD,
            ICON
        FROM MENU
        START WITH PARENT_ID = 0
        CONNECT BY PRIOR  ID = PARENT_ID
        ORDER SIBLINGS BY ORD
    </select>

    <!-- 선택된 권한의 메뉴 정보를 가져온다 -->
    <select id="selectMenuList" resultType="com.example.practice.vo.menu.MenuVo">
        SELECT
            M.ID AS ID,
            M.PARENT_ID,
            M.NAME,
            M.ORD,
            M.ICON
        FROM MENU M INNER JOIN MENU_LINK ML
                               ON M.ID = ML.MENU_ID
        WHERE ML.ROLE_ID = #{id}
            START WITH (M.PARENT_ID = 0 AND ML.ROLE_ID = #{id})
        CONNECT BY PRIOR  M.ID = M.PARENT_ID
        ORDER SIBLINGS BY M.ORD
    </select>


    <!-- 권한에 연결되어있는 메뉴 삭제 -->
    <delete id="deleteMenuList">
        DELETE FROM MENU_LINK WHERE ROLE_ID = #{roleId}
    </delete>

    <!-- 선택한 권한에 메뉴 넣기 -->
    <insert id="insertMenuList">
        INSERT INTO MENU_LINK (
                            ID,
                            ROLE_ID,
                            MENU_ID
        )VALUES(
        (SELECT NVL(MAX(ID) , 0) + 1 AS ID FROM MENU_LINK),
        #{roleId},
        #{menuId}
        )
    </insert>
    
</mapper>

 

 

 

구현 내용

1. 해당 글은 Bootstrap 을 이용하고 있다

2. 메뉴를 계층형으로 출력하려고 한다

3. 권한에 따라 메뉴를 설정하려고 한다

4. 부모 메뉴의 체크박스 체크 및 해제에 따라 전체 선택 및 해제를 구현하려고 한다

 

 

 

 

 

DB

https://greed-yb.tistory.com/276

 

Oracle - 계층형 쿼리

생성CREATE TABLE MENU(ID NUMBER PRIMARY KEY,PARENT_ID NUMBER , NAME VARCHAR2(20),ORD NUMBER,ICON VARCHAR2(50))    부모 추가-- 부모INSERT INTO MENU( ID, PARENT_ID, NAME, ORD, ICON)VALUES( (SELECT NVL(MAX(ID), 0) +1 AS ID FROM MENU), 0, '설정',

greed-yb.tistory.com

이전글 상태에서 작업하였다

 

 

 

-- Menu 테이블의 Id 와 Role 테이블의 Id를 가지고 있는 테이블
CREATE TABLE MENU_LINK(
ID		NUMBER PRIMARY KEY,
ROLE_ID  	NUMBER,
MENU_ID 	NUMBER
)

 

데이터 예시

 

 

 

 

TEST

 

구현중인 화면

 

 

 

 

 

1. 권한 dataTable 선택

 

권한 선택 시 DB에 있는 데이터가

제대로 불러와지고 체크까지 되는 것을 확인하였다

 

전체 CheckBox 선택/해제에 따라 메뉴의 CheckBox 가 동작되며,

부모의 CheckBox 선택/해제에 따라 자식 메뉴의 CheckBox 도 같이 동작한다

자식 메뉴의 CheckBox 의 여부에 따라 부모의 CheckBox 도 같이 동작한다

 

 


내용이 많이 길어져서 TEST 구현 부분은 건너 뛰려고 한다

뭔가 너무 하드코딩같은 느낌이긴한데...DOM 구조 공부는 엄청 된 것 같다

댓글