Mysql hierarchy sql 계층구조 쿼리 2 : 그리기(Multi selectbox)

 

https://developyo.tistory.com/54

위의 쿼리에서 꺼내온 정보를 화면에 뿌려보자.

 

 

<1: 컨트롤러 코딩>

1. 가이드 목록을 출력하기 위한 controller

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/loadGuideList.do")
public ModelAndView loadGuideList(HttpServletRequest request, @RequestBody GuideVO param) throws Exception
{    
    ModelAndView mav = new ModelAndView("jsonView");
    
    List<GuideVO> guideList = service.selectGuideList(param);
    
    mav.addObject("guideList", guideList);    //가이드 목록
    mav.addObject("paramsData", param);
    
    return mav;
}
cs

 

2. 멀티 셀렉트박스 목록을 ajax로 불러오기 위한 controller

1
2
3
4
5
6
7
8
9
@RequestMapping("/getNodeList.do")
public ModelAndView getNodeGroup(HttpServletRequest request, @RequestBody NodeVO param) throws Exception
{    
    ModelAndView mav = new ModelAndView("jsonView");
    List<NodeVO> nodes = service.selectNodeList(param);    //가이드 카테고리
    mav.addObject("nodes", nodes);
 
    return mav;
}
cs

 

* Service, DAO, VO 는 별다른 내용이 없으므로 생략..

 

<2: script 코딩>

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
var loadData = {
        
        //가이드 목록 호출
        loadGuideList : function () {
            
            var totalSearchFlag = "N";    //가장 낮은 depth 의 선택 값이 전체인 경우 Y
            
            //현재 선택된 node_id 값 set (조회용)
            var nodeCnt = $(".nodeGroup").length;
            
            //조회
            var paramVal = "";
            for(var i=nodeCnt-1; i>=0; i--){
                //제일 우측(낮은 depth) 셀렉트 박스부터 제일 좌측(높은 depth) 로 반복문
                var val = $(".nodeGroup:eq("+i+")").val();
                var node_id = val.split("^")[0];
                if(node_id && node_id != ""){
                    //node_id 가 존재하는 경우(전체 선택이 아닌 경우)
                    paramVal = node_id;
                    break;
                } else {
                    //node_id 가 존재하지 않는 경우(전체 선택인 경우)
                    totalSearchFlag = "Y";
                    continue;
                }
            }
            
            $("form[name=frmGuide] input[name=totalSearchFlag]").val(totalSearchFlag);
            $("form[name=frmGuide] input[name=node_id]").val(paramVal);                    
        
            var params = {
                node_id : $("form[name=frmGuide] input[name=node_id]").val(),
                totalSearchFlag : $("form[name=frmGuide] input[name=totalSearchFlag]").val()
            };
            
             $.ajax({
                type    : "post",
                url        : "/stbcs/ipchk/loadGuideList.do",
                contentType: "application/json",
                dataType:"json",
                data     : JSON.stringify(params),    
                success    : function (data) {
                    drawData.drawGuideList(data);
                }
            }); 
        } ,
        
        //가이드 분류
        loadNodeList : function(selectBox){
                var obj = $(selectBox);
                var tmp = obj.val().split("^");
                var nodeId = tmp[0];
                var leaf_yn = tmp[1];
                var currentIdx = obj.parent(".nodeGrp").index();    //현재 node 의 idx        
                var nodeCnt = $(".nodeGrp").length;    //node 갯수                            
                
                if(nodeCnt > currentIdx){
                    for(var i=nodeCnt; i>=currentIdx; i--){    
                        console.log("remove idx : " + i);   
                        $(".nodeGrp:eq("+i+")").remove();     
                    }
                }
                
                if(leaf_yn == "Y"){
                    //마지막 노드가 아닌 경우 (자식 노드가 있는 경우)
                    var params = {
                            node_id : nodeId
                        };
                    
                    $.ajax({
                        type    : "post",
                        url        : "/getNodeList.do",
                        contentType: "application/json",
                        dataType:"json",
                        data     : JSON.stringify(params),    
                        success    : function (data) {
                            drawData.drawNodeList(data);
                        }
                    });
                }
        } 
}
var drawData = {
    drawNodeList : function (data, currentIdx){
                var nodeGroupTxt = "";
                
                try{
                    var node = data.node;
                    if(node != null && node.length > 0){
                        if($(".nodeGrp").length == 0){
                            nodeGroupTxt += "<span>분류&nbsp;&nbsp;</span>";
                        }
                        nodeGroupTxt += "<div class='selectbox small nodeGrp' style='margin-right:3px;' >";
                        nodeGroupTxt += "<select class='nodeGroup' name='node_id' onchange='javascript:loadData.loadNodeList(this);'>";
                        nodeGroupTxt += "<option value='^N'>전체</option>";
                        
                        $.each(node, function(index, item){
                            nodeGroupTxt += "<option value=" + item.node_id+"^"+item.leaf_yn + ">" + item.node_nm + "</option>";
                        });
                        
                        nodeGroupTxt += "</select>";
                        nodeGroupTxt += "</div>";
                    }
                } catch (error) {
                } finally {
                    $("#guideSearchDiv").append(nodeGroupTxt);
                }        
        }
}
cs

 

※ Multi selectbox

94line : selectbox에 onchange 이벤트를 걸어 selectbox 값이 변화할 때, 자신의 loadNodeList(this) 함수를 호출한다.

57~62line : 총 selectbox 갯수가 현재 선택된 selectbox의 index보다 큰 경우 현재 선택된 selectbox 보다 낮은 depth(우측에있는)의 selectbox를 지운다.

ex: 1, 2, 3 depth 의 selectbox 를 특정 값으로 선택해 놓고, 1번째 selectbox 의 값을 다른값으로 바꿔버린 경우 2, 3번째 selectbox tag는 .remove()로 제거한다 (remove는 싱크(동기)방식이므로 ajax 호출 순서가 보장된다)

64~80line :

selectbox에서 선택된 값이 자식노드가 있는 경우(leaf_yn == 'Y') 특정 노드를 기준으로 한 자식 노드 목록을 가져오기 위해 getNodeList.do ajax 호출

84~108line : ajax 호출을 통해 가져온 노드 목록을 selectbox로 표현

98line : 조작시 필요한 값인 노드아이디와 자식노드유무 데이터를 ^ 문자를 구분으로하여 value 값으로 넣어준다(option value = node_id^leaf_yn)

106line : 멀티셀렉트박스가 위치할 곳에 만들어준 selectbox를 .append 로 붙여넣는다

 

※ 가이드 목록(Multi selectbox 를 조건으로 걸어 조회한 결과 리스트)

4~46 line : 가이드 목록 ajax 호출

13~26 line :

가장 낮은 depth(제일 오른쪽)의 selectbox 부터 가장 높은 depth (제일 왼쪽)의 selectbox 까지 for 문을 돈다.

node_id^leaf_yn (노드아이디^자식노드존재유무) 값이 담겨있는 option value 를 꺼낸다.

node_id 가 존재하는 경우 전체 조회가 아니므로 해당 값을 server 쪽에 ajax 로 전달하면 된다. (for문을 끝낸다)

node_id 가 존재하지 않는 경우 전체 조회이므로 서버에 전달할 전체 조회 flag 값(totalSearchFlag)를 Y 로 바꿔준 후, for문을 계속 돌린다(continue;)

* 전체 조회 flag 값(totalSearchFlag)은 subquery(3-2 쿼리작성부분의 3번 쿼리) 를 사용할 경우 필요 없다. foreach를 사용할 경우 분기처리를 위해 필요. 

* 조회한 가이드 목록을 화면에 그리는 코딩은 별다를게 없으므로 생략..

 

반응형

Mysql hierarchy sql 계층구조 쿼리 : 설계 및 쿼리작성(Multi selectbox)

 

고객들이 열람할 가이드 정보가 있고,

가이드 정보의 카테고리를 N depth 로 분류(멀티 셀렉트박스 사용)하여 조회할 수 있게 하여 다음과 같은 화면을 만들어 보자.

<1: 테이블 설계>

1. '전체' 옵션의 유무

case 1 멀티 셀렉트박스에 '전체'라는 옵션이 없는 경우 :

마지막 셀렉트박스의 value(node_id)를 기준으로 가이드를 조회

case 2 멀티 셀렉트박스에 '전체'라는 옵션이 존재하는 경우 :

'전체'가 선택된 마지막 셀렉트박스 직전의 value(node_id)를 기준으로 모든 자손 node_id를 찾아 조회해야한다.

 

예를 들어,

case 1 ex) 고객지원 > 고객지원sub > 고객지원sub의sub 와 같이 마지막 select box에 특정 값이 선택된 경우 :

고객지원sub의sub 의 value(node_id) 를 기준으로 가이드 조회

case 2 ex) 고객지원 > 고객지원sub > 전체 와 같이 마지막 select box가 '전체'로 선택된 경우 :

고객지원sub을 부모 노드로 삼는 모든 자손 노드를 기준으로 가이드 조회

 

2. 동적 depth 여부

case 1 : depth 가 특정 값으로 고정인 경우, 셀렉트박스용 분류 코드테이블을 따로 두고 join 을 몇번 걸면 해결이 된다

case 2 : depth 가 동적(N)인 경우, join 이 N 번 되어야 하므로 단순한 join으로 해결 할 수 없다..

 

전체 옵션이 존재하고 depth 가 동적(N)인 경우를 전재하고 설계 및 개발해보자.

 

 

<2: 테이블 생성>

1. 메인테이블 생성 : 가이드 정보 테이블 생성

가이드 정보 테이블은 제목, 가이드의 노드 아이디, 상세 내용만 지니고 있다.

1
2
3
4
5
6
7
8
CREATE TABLE `guide_info` (
  `SEQ` int(11NOT NULL AUTO_INCREMENT COMMENT '일련번호',
  `TITLE` varchar(100NOT NULL COMMENT '제목',
  `GUIDE_ID` int(11NOT NULL COMMENT '가이드 노드 아이디',
  `TEXT_ITEM_1` text COMMENT '가이드 내용',
  PRIMARY KEY (`SEQ`)
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='고객지원 가이드 게시판';
 
cs

 

2. 서브테이블 생성 : 가이드 분류 정보를 따로 지니고 있는 노드 정보 테이블 생성

1
2
3
4
5
6
7
8
9
CREATE TABLE `node_info` ( 
  `NODE_ID` int(11NOT NULL AUTO_INCREMENT COMMENT '노드아이디'
  `PNODE_ID` int(11NOT NULL COMMENT '부모노드아이디'
  `NODE_PATH` varchar(200DEFAULT NULL COMMENT '노드경로'
  `NODE_NM` varchar(20DEFAULT NULL COMMENT '노드명'
  `DEPTH` int(11DEFAULT '1' COMMENT '노드DEPTH'
  `LEAF_YN` varchar(1DEFAULT 'N' COMMENT '자식노드존재유무'
  PRIMARY KEY (`NODE_ID`
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='노드 정보'
cs

 

생성된 테이블 :

NODE_ID(노드아이디)와 PNODE_ID(부모 노드 아이디)가 서로 참조하는 관계의 테이블 구조.

 

샘플 데이터 :

 

<3: 쿼리 작성>

필요한 쿼리 종류는 크게 두가지로 나눠볼 수 있다.

1. 셀렉트박스의 구성요소인 노드정보(node_info TABLE)를 조회하는 쿼리 : 멀티셀렉트박스를 그리기 위한 데이터를 뽑는 쿼리

2. 선택된 셀렉트박스 값들을 기준으로 가이드 목록(guide_info TABLE)을 조회한 결과 : 조회 결과를 출력하기 위한 데이터를 뽑는 쿼리

 

3-1. 셀렉트 박스의 구성요소인 노드정보를 조회하는 쿼리 :

N번째(Ndepth) 셀렉트박스가 특정값으로 선택된 경우 N+1번째(N+1depth) 셀렉트박스 조회

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SELECT  node_id
      , pnode_id
      , node_nm
      , depth
      , leaf_yn
  FROM  node_info 
 WHERE  1=1
<choose>
    <when test='node_id neq null and node_id neq ""'> 
       AND  pnode_id = #{node_id}
    </when>
    <otherwise>  
       AND  pnode_id = 1
    </otherwise>
</choose>
 
 
cs

* 8~15line : node_id 가 없는 경우, 아무것도 선택되지 않은 경우(화면에 최초 진입한 상태)이며 최고 조상이라 할 수 있는 node_id 1 을 기준으로 자식 노드를 구한다

 

3-2. 선택된 셀렉트박스 값들을 기준으로 가이드 목록을 조회한 결과

1) 마지막 셀렉트박스가 특정 값으로 선택된 경우 : 단순 join 으로 해결

2) 마지막 셀렉트박스가 '전체'로 선택된 경우 : 아래와 같이 해결

 

ORACLE 은 CONNECT BY 함수를 사용하여 트리구조의 데이터를 출력 할 수 있겠지만,

MYSQL 은 CONNECT BY 함수를 지원하지 않는다.

 

다음과 같이 특정 노드 밑에 있는 모든 자손, 특정 노드 상위에 있는 모든 조상 노드를 구할 수 있다.

 

1. 특정 노드의 자손 구하기

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT node_id
     , pnode_id
     , depth
     , leaf_yn
     , node_path
 FROM  (
        SELECT * 
          FROM node_info
      ORDER BY pnode_id, node_id
        ) node_sorted,
       (SELECT @pv := #{node_id}) initial
WHERE   FIND_IN_SET(pnode_id, @pv)
  AND   LENGTH(@pv := concat(@pv, ',', node_id))
cs

 

2. 특정 노드의 조상 구하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SELECT   nid AS node_id
       , pnode_id
       , leaf_yn
       , depth
       , node_path
  FROM (
        SELECT  @nid AS nid,
              (
               SELECT  @nid := pnode_id
                 FROM  node_info
                WHERE  node_id = nid
               ) AS pnode_id
               , leaf_yn
               , depth
               , node_path
          FROM  (
                 SELECT  @nid := #{node_id}
                ) vars
                , node_info
         WHERE  @nid <> 0
        ) as tmp
cs

 

3. 특정 분류 밑의 모든 가이드 목록 조회 (특정 노드아이디 기준 모든 자손 노드를 구하는 1번 쿼리를 sub query로 사용)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SELECT  seq, 
        title, 
        (SELECT node_path FROM node_info where node_id = gi.guide_id) as guide_path
  FROM  guide_info gi
 WHERE  1=1
   AND  gi.guide_id IN  (SELECT * FROM (
                                             SELECT   node_id
                                               FROM  (SELECT node_id, pnode_id
                                                        FROM node_info
                                                    ORDER BY pnode_id, node_id
                                                     ) node_sorted,
                                                     (select @pv := #{node_id}) initial
                                              WHERE   FIND_IN_SET(pnode_id, @pv)
                                                AND   LENGTH(@pv := concat(@pv, ',', node_id))
                            ) as temp
                            )
ORDER BY seq
cs

 

※ 위와 같이 subquery 를 사용하여 sql 에서 처리하지 않고, 소스레벨에서 처리하고 싶은 경우 foreach 를 사용하여 다음과 같이 처리할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SELECT   gi.title, gi.guide_id, gi.text_item_1, ni.node_path AS rs
  FROM   guide_info gi
       , node_info ni
 WHERE   1=1
   AND   ni.node_id = gi.guide_id
<if test = 'sch_word neq null and sch_word neq ""'>
   AND   title LIKE CONCAT('%',#{sch_word},'%')
</if>
<if test = 'totalSearchFlag neq null and totalSearchFlag neq ""'>
  <choose>
     <when test = 'totalSearchFlag eq "Y"'>
      AND   ni.node_id IN 
       <foreach item="node_id_grp" index="i" collection="node_id_grp" separator="," open="(" close=")">
        #{node_id_grp}
       </foreach>
      </when>
      <otherwise>
        AND   ni.node_id = #{node_id}
      </otherwise>
  </choose>
</if>
 
cs

* totalSearchFlag 는 selectBox 에서 '전체'가 선택되었음을 판별하기 위해 사용되는 값

* 1번 쿼리(특정노드의 모든 자손 구하기)로 뽑아온 모든 자손 노드를 필드명이 node_id_grp 인 List 자료형 에 담아 sql mapper 로 넘겨주어 사용

* open 은 for 문이 맨처음 돌기 전에, close 는 for문이 끝까지 돌고나서, separator 는 for 문이 한 번 씩 돌 때마다 적용된다.

* foreach(for문)로 작성된 쿼리 결과물은 다음과 같다

  (생략) ~ AND ni.node_id IN (2, 6)

 

 

 

위 쿼리를 사용해 가져온 데이터를 화면에 그려보자.

https://developyo.tistory.com/55?category=689339

반응형

+ Recent posts