Notice
Recent Posts
Recent Comments
Link
«   2025/02   »
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
Archives
Today
Total
관리 메뉴

우리마의 웹 개발

[Spring]게시글 상세보기에서 댓글(대댓글) 출력 로직 정리 본문

WEB/Spring

[Spring]게시글 상세보기에서 댓글(대댓글) 출력 로직 정리

우리마 2021. 5. 4. 16:39

Spring환경에서 게시판으로 개발 연습을 하면서 댓글 출력에 대한 개인적인 로직을 정리하고자 합니다.

지극히 주관적인 로직과 코딩이라서 상당히 하드 할 수 있어요.

댓글에 대한 등록, 수정, 삭제에 대한 기능은 생략하고 출력에 대한 부분만 다루도록 할게요

지적사항, 더 좋은 방법 등 댓글 환영합니다.

개발 환경
Spring Framework 5.2.3
Tomcat 8.0
DB MySql8 , MyBatis3.5
Browser Chrome
IDE STS 3.9.11
결과 화면

DB  댓글 관리 테이블 구조

댓글 테이블 구조
게시글 테이블과의 관계

댓글을 작성할 때 어느 게시글에서 등록된 댓글인지 알기 위해 게시글 테이블(training_bbs)에서의 PK(idx) 값을 댓글 테이블(training_bbs_cmt)에서 FK(bbscmtidx)로 사용하였습니다.

게시글 상세보기 페이지 접속 시 댓글출력에 대한 프로세스

일단 저는 상세보기 최초 접속시 그 게시글에 해당하는 댓글을 최대 4개까지만(대댓글 포함) 출력하도록 하였습니다. 이후에 '댓글 더보기' 버튼 클릭 시 이미 div에append는 되어있지만(str2에 저장된 댓글 목록) display가 none인 div를 display : block으로 해서 출력해주는 방식 입니다.

HTML , JavaScript - 게시판 상세보기 페이지
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
<!-- 댓글작성 영역 -->
<div class="board_cmt">
    <div class="tit" style="margin-left: 6px;"><em id="totalCmt" class="bico_comment"></em>Comments</div>
     <div class="board_cmt_write">
         <div class="bx"> 
             <textarea id="cmtContent" placeholder="소중한 댓글을 작성해주세요^^" maxlength="150"></textarea>
         </div>
        <button id="btn_insert_cmt">등록</button>
     </div>
</div>
<!-- 댓글 목록 영역 -->
<div class="board_cmt_list" id="board_cmt_list" style="margin-left:6px;"></div>
<div style="text-align: center; margin: 20px 0px;" id="div_cmt_more">
<!-- 더보기 글자 hover 띄우기 -->
    <span class="cmt_more_guide" id="cmt_more_guide" style="display: none; position: absolute;"></span>
    <a href='javascript:void(0);' id='btn_cmt_more' style='position: relative;'>
        <img src="/home/img/ico_cmt_more_before.png" id="imgMore" style="cursor:pointer; width: 20px;">
    </a>
</div>
<!-- 더보기 눌렀을때 추가 되는 댓글 영역 -->
<div class="board_cmt_list" id="cmtMore" style="display:none;"></div>
<div style="text-align: center; display:none; margin: 20px 0px;" id="div_cmt_back">
    <span class="cmt_back_guide" id="cmt_back_guide" style="display: none; position: absolute;"></span>
    <a href='javascript:void(0);' id='btn_cmt_back' style='position: relative;'>
        <img src="/home/img/ico_cmt_back_before.png" id="imgBack" style="cursor:pointer; width: 20px;">
    </a>
</div>
 
<script type="text/javascript">
/* 버튼 세팅 */
 var btn_insert_cmt = document.getElementById("btn_insert_cmt");
 btn_insert_cmt.onclick = function(){insertCmt();}
 /* Model 값 세팅 */
 var idx = ${bbsidx};
 var userid = `${userid}`;
 $(function(){
    //댓글 목록 출력
    selectBBScmt();
     /* 댓글 더보기 화살표 hover */
     $("#btn_cmt_more").hover(function(){
         $("#imgMore").attr("src","/home/img/ico_cmt_more_after.png");
         $("#cmt_more_guide").css("display","");
     }, function(){
         $("#imgMore").attr("src","/home/img/ico_cmt_more_before.png");
         $("#cmt_more_guide").css("display","none");
     });
     /* 댓글 접기 화살표 hover*/
     $("#btn_cmt_back").hover(function(){
         $("#imgBack").attr("src","/home/img/ico_cmt_back_after.png");
         $("#cmt_back_guide").css("display","");
     }, function(){
         $("#imgBack").attr("src","/home/img/ico_cmt_back_before.png");
         $("#cmt_back_guide").css("display","none");
     });
});
//댓글 목록 출력 ajax
  function selectBBScmt(){
     var str1 = ""//최초 댓글 4개를 append할 변수
     var str2 = ""//댓글이 5개이상일때 추가 append할 변수
     var strDelAndMod = "";//댓글 수정, 삭제 html string을 담는 변수
     var replyImg = "";//대댓글 이미지 html string을 담는 변수
     var inputWidthClass = "";//대댓글이 달릴때 대댓글의 width값을 담는 변수
     var mlpClass=""//대댓글이 달릴때 replyImg가 그려질 위치를 정하는 변수
     
      $.ajax({
            type : "POST",  
            url : "/selectBBScmt",       
            dataType : "json",   
            data : "bbscmtidx="+idx,
            error : function(){
                Rnd.alert("통신 에러","error","확인",function(){});
            },
            success : function(jdata) {
                if(jdata.length < 4){ // 댓글이 4개 이하 일때 처음에는 댓글을 최대 4개까지만 보여줌
                    for(var i=0; i<jdata.length; i++){
                        if(userid == jdata[i].userid){
                            //수정 삭제 답글 버튼 본인이 적은거
                            strDelAndMod = "<em onclick=\"cmtModify("+jdata[i].idx+")\" style='cursor:pointer;'>&nbsp;&nbsp;수정</em>&nbsp;<em onclick=\"cmtDelete("+jdata[i].idx+")\" style='cursor:pointer;'>삭제</em>&nbsp;<em onclick=\"cmtReply('"+jdata[i].userid+"_"+jdata[i].idx+"','I')\"style='cursor:pointer;'>답글</em>";
                        }
                        else {
                            strDelAndMod = "&nbsp;<em onclick=\"cmtReply('"+jdata[i].userid+"_"+jdata[i].idx+"','I')\"style='cursor:pointer;'>답글</em>";
                        }
                        if(jdata[i].level > 1){
                            //대댓글 이미지 , input width class 
                            replyImg = "<img src='/home/img/comment_reply.png' class='commentReply'>";
                            inputWidthClass = "class='width96'";
                            if(jdata[i].level == 2){mlpClass = "";}
                            else if(jdata[i].level == 3){mlpClass = "class='mlp30'";}
                            else if(jdata[i].level >= 4){mlpClass = "class='mlp60'";}
                        }
                        else{
                            replyImg = "";
                            inputWidthClass = "class='width100'";
                            mlpClass="";
                        }
                        str1 += "<ul><li><div class='desc'style='border-top: 2px solid black;'>작성날짜&nbsp;<strong>"+jdata[i].regdate+"</strong>&nbsp;&nbsp;작성자&nbsp;<strong>"+jdata[i].userid+"</strong>"
                            +strDelAndMod
                            +"</div></li>"
                            +"<li style='line-height:37px;' "+mlpClass+">"+replyImg
                            +"<input type='text' "+inputWidthClass+"name=\""+jdata[i].idx+"\" value=\""+jdata[i].content+"\" disabled=''style='font-size:15px;font-weight:bold;background-color: #f6f6f6;border: 1px solid #f6f6f6;'></li>"
                            +"<li id=\""+jdata[i].idx+"\" style='display:none;'>"
                            +"<a href='javascript:void(0);' onclick='cmtModCancel(this)' orgcmt=\""+jdata[i].content+"\" class='btn_d btn_red'>취소</a>&nbsp;"
                            +"<a href='javascript:void(0);' onclick=\"cmtModAri("+jdata[i].idx+")\" class='btn_d btn_build'>완료</a></li>"
                            +"<li id=\""+jdata[i].userid+"_"+jdata[i].idx+"\" style='display:none;'>"
                            +"<img src='/home/img/comment_reply.png' class='commentReply'>"
                            +"<textarea class='replyTextarea' id='"+jdata[i].upidx+"_"+jdata[i].idx+"'></textarea>"
                            +"<div class='replySubmitDiv'>"
                            +"<a href='javascript:void(0);' onclick=\"cmtReply('"+jdata[i].userid+"_"+jdata[i].idx+"','C')\" class='btn_d btn_red'>취소</a>&nbsp;"
                            +"<a href='javascript:void(0);' onclick=\"insertReplyCmt('"+jdata[i].upidx+"_"+jdata[i].idx+"','"+jdata[i].bbscmtidx+"')\" class='btn_d btn_build'>등록</a></div></li></ul>";
                        }
                    //댓글 목록 출력
                    $("#board_cmt_list").empty();
                    $("#board_cmt_list").append(str1);
                }
                else//댓글이 5개 이상일때 
                    for(var i=0; i<4; i++){//기존 4개 먼저 보여짐
                        if(userid == jdata[i].userid){
                            strDelAndMod = "<em onclick=\"cmtModify("+jdata[i].idx+")\" style='cursor:pointer;'>&nbsp;&nbsp;수정</em>&nbsp;<em onclick=\"cmtDelete("+jdata[i].idx+")\" style='cursor:pointer;'>삭제</em>&nbsp;<em onclick=\"cmtReply('"+jdata[i].userid+"_"+jdata[i].idx+"','I')\"style='cursor:pointer;'>답글</em>";
                        }
                        else {
                            strDelAndMod = "&nbsp;<em onclick=\"cmtReply('"+jdata[i].userid+"_"+jdata[i].idx+"','I')\"style='cursor:pointer;'>답글</em>";
                        }
                        if(jdata[i].level > 1){
                            replyImg = "<img src='/home/img/comment_reply.png' class='commentReply'>";
                            inputWidthClass = "class='width96'";
                            if(jdata[i].level == 2){mlpClass = "";}
                            else if(jdata[i].level == 3){mlpClass = "class='mlp30'";}
                            else if(jdata[i].level >= 4){mlpClass = "class='mlp60'";}
                        }
                        else{
                            replyImg = "";
                            inputWidthClass = "class='width100'";
                            mlpClass="";
                        }
                        str1+= "<ul><li><div class='desc'style='border-top: 2px solid black;'>작성날짜&nbsp;<strong>"+jdata[i].regdate+"</strong>&nbsp;&nbsp;작성자&nbsp;<strong>"+jdata[i].userid+"</strong>"
                            +strDelAndMod
                            +"</div></li>"
                            +"<li style='line-height:37px;' "+mlpClass+">"+replyImg
                            +"<input type='text' "+inputWidthClass+"name=\""+jdata[i].idx+"\" value=\""+jdata[i].content+"\" disabled=''style='font-size:15px;font-weight:bold;background-color: #f6f6f6;border: 1px solid #f6f6f6;'></li>"
                            +"<li id=\""+jdata[i].idx+"\" style='display:none;'>"
                            +"<a href='javascript:void(0);' onclick='cmtModCancel(this)' orgcmt=\""+jdata[i].content+"\" class='btn_d btn_red'>취소</a>&nbsp;"
                            +"<a href='javascript:void(0);' onclick=\"cmtModAri("+jdata[i].idx+")\" class='btn_d btn_build'>완료</a></li>"
                            +"<li id=\""+jdata[i].userid+"_"+jdata[i].idx+"\" style='display:none;'>"
                            +"<img src='/home/img/comment_reply.png' class='commentReply'>"
                            +"<textarea class='replyTextarea' id='"+jdata[i].upidx+"_"+jdata[i].idx+"'></textarea>"
                            +"<div class='replySubmitDiv'>"
                            +"<a href='javascript:void(0);' onclick=\"cmtReply('"+jdata[i].userid+"_"+jdata[i].idx+"','C')\" class='btn_d btn_red'>취소</a>&nbsp;"
                            +"<a href='javascript:void(0);' onclick=\"insertReplyCmt('"+jdata[i].upidx+"_"+jdata[i].idx+"','"+jdata[i].bbscmtidx+"')\" class='btn_d btn_build'>등록</a></div></li></ul>";
                        }
                    for(var i=4; i<jdata.length; i++){ // 더보기 누르면 이미 그려노은거 추가하는거
                        if(userid == jdata[i].userid){
                            strDelAndMod = "<em onclick=\"cmtModify("+jdata[i].idx+")\" style='cursor:pointer;'>&nbsp;&nbsp;수정</em>&nbsp;<em onclick=\"cmtDelete("+jdata[i].idx+")\" style='cursor:pointer;'>삭제</em>&nbsp;<em onclick=\"cmtReply('"+jdata[i].userid+"_"+jdata[i].idx+"','I')\"style='cursor:pointer;'>답글</em>";
                        }
                        else {
                            strDelAndMod = "&nbsp;<em onclick=\"cmtReply('"+jdata[i].userid+"_"+jdata[i].idx+"','I')\"style='cursor:pointer;'>답글</em>";
                        }
                        if(jdata[i].level > 1){
                            replyImg = "<img src='/home/img/comment_reply.png' class='commentReply'>";
                            inputWidthClass = "class='width96'";
                            if(jdata[i].level == 2){mlpClass = "";}
                            else if(jdata[i].level == 3){mlpClass = "class='mlp30'";}
                            else if(jdata[i].level >= 4){mlpClass = "class='mlp60'";}
                        }
                        else{
                            replyImg = "";
                            inputWidthClass = "class='width100'";
                            mlpClass="";
                        }
                        str2 += "<ul style='margin-left:6px;'><li><div class='desc'style='border-top: 2px solid black;'>작성날짜&nbsp;<strong>"+jdata[i].regdate+"</strong>&nbsp;&nbsp;작성자&nbsp;<strong>"+jdata[i].userid+"</strong>"
                            +strDelAndMod
                            +"</div></li>"
                            +"<li style='line-height:37px;' "+mlpClass+">"+replyImg
                            +"<input type='text' "+inputWidthClass+"name=\""+jdata[i].idx+"\" value=\""+jdata[i].content+"\" disabled=''style='font-size:15px;font-weight:bold;background-color: #f6f6f6;border: 1px solid #f6f6f6;'></li>"
                            +"<li id=\""+jdata[i].idx+"\" style='display:none;'>"
                            +"<a href='javascript:void(0);' onclick='cmtModCancel(this)' orgcmt=\""+jdata[i].content+"\" class='btn_d btn_red'>취소</a>&nbsp;"
                            +"<a href='javascript:void(0);' onclick=\"cmtModAri("+jdata[i].idx+")\" class='btn_d btn_build'>완료</a></li>"
                            +"<li id=\""+jdata[i].userid+"_"+jdata[i].idx+"\" style='display:none;'>"
                            +"<img src='/home/img/comment_reply.png' class='commentReply'>"
                            +"<textarea class='replyTextarea' id='"+jdata[i].upidx+"_"+jdata[i].idx+"'></textarea>"
                            +"<div class='replySubmitDiv'>"
                            +"<a href='javascript:void(0);' onclick=\"cmtReply('"+jdata[i].userid+"_"+jdata[i].idx+"','C')\" class='btn_d btn_red'>취소</a>&nbsp;"
                            +"<a href='javascript:void(0);' onclick=\"insertReplyCmt('"+jdata[i].upidx+"_"+jdata[i].idx+"','"+jdata[i].bbscmtidx+"')\" class='btn_d btn_build'>등록</a></div></li></ul>";
                        }
                    
                    //처음 댓글 목록 
                    $("#board_cmt_list").empty();
                    $("#board_cmt_list").append(str1);
                    
                    //펼치기 했을때 댓글 목록
                    $("#cmtMore").empty();
                    $("#cmtMore").append(str2);
                    // 더보기 버튼 눌렀을때
                    $("#btn_cmt_more").on("click",function(){
                        $("#div_cmt_more").css("display","none");
                        $("#cmtMore").slideDown();
                        $("#div_cmt_back").css("display","");
                        $("#wrap").animate({ // 스크롤 제일 아래로 애니메이션
                            scrollTop : 10000
                        }, 800);
                    });
                    //접기 버튼 눌렀을때
                    $("#btn_cmt_back").on("click",function(){
                        $("#cmtMore").slideUp(function(){
                            $("#div_cmt_back").css("display","none");
                            $("#div_cmt_more").css("display","");
                        });
                    });
                }
                $("#totalCmt").empty();
                if(jdata.length == 0){
                    totalCmt = 0;
                    $("#imgMore").css("display","none");
                }
                else {
                    $("#imgMore").css("display","");
                    var totalCmt = jdata[0].totalCmt;
                    }
                $("#totalCmt").append(" "+totalCmt+" ");
            }
        });
     }
</script>
 
 
cs

굉장히 난잡한데 HTML에서 중요한 점은 최초에 접속했을 때 보이는 댓글 목록 영역 div와(<div class="board_cmt_list" id="board_cmt_list" style="margin-left:6px;">) 댓글 더보기를 눌렀을 때

나타나는 div 영역(<div class="board_cmt_list" id="cmtMore" style="display:none;">)을 따로 두었다는 것입니다.

Javascript 부분에서는 버튼 세팅, Model값 세팅 등을 하고 $(function(){});

안에 selectBBScmt() 함수를 호출해줍니다.

/selectBBScmt Controller를 호출해 해당 게시글에 대한 댓글들의 정보를 가져옵니다.

그리고 74번 Line부터 시작하는 댓글의 개수에 따라 if ~ else 문으로 나눠요.

76번 Line의 if else는 댓글이 자신이 적은 댓글만 수정/삭제 버튼이 있어야 하기 때문에 그 값을 세팅하는 부분입니다.

83번 Line의 if else는 대댓글의 이미지 세팅과 대댓글의 레벨에 따라서 들여 써져야 된다는 점을 고려해서 css에 정의된 class값을 다르게 주었습니다. 예를 들어 mlp30 은 margin-left : 30px입니다.

총댓글의 개수가 4개 이하라면 str1 만 append 됩니다.

댓글의 개수가 5개부터는 4번째 댓글까지의 string과 5번째부터의 string을 따로 변수에 저장합니다(각각 str1, str2)

그리고 str1, str2 둘 다 각각의 div아래에 append를 한 후 '더보기' 버튼과 '접기'버튼을 각각 눌렀을 때 css의 display 속성만 바꿔 줍니다. (=>185 Line부터 206 Line)

그 아래에는 총 댓글 개수의 값을 세팅하는 부분입니다.

Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//게시글 상세보기 - 댓글 목록 출력
    @RequestMapping("/selectBBScmt")
    @ResponseBody
    public List<Map<String,Object>> selectBBScmt(@RequestParam Map<String,Object> commandMap){
        logger.info("request: /selectBBScmt");
        List<Map<String,Object>> resultMap = null;
        int totalCmt = 0;
        try {
            int bbsidx = Integer.parseInt(commandMap.get("bbscmtidx").toString());
            
            resultMap = service.selectBBScmt(commandMap);
            totalCmt = service.getTotalCmt(bbsidx);//전체 댓글 개수
            resultMap.get(0).put("totalCmt", totalCmt);
        } catch (Exception e) {
            logger.debug(e.getMessage());
        }
        return resultMap;
    }
 
cs

9번 Line에서 bbsidx는 해당 게시글의 일련번호이고 Map안에 담긴 value는 string이나 integer가 아니므로. toString()한 값을. parseInt 해서 int형으로 만들어줍니다.

Service와 DAO는 생략하고 아래는 댓글 목록 출력 SQL문입니다.

SQL
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
    <!-- 댓글 목록 출력 -->
    <select id="selectBBScmt" resultType="map">
 
select 
      list.level as level
    , list.idx as idx
    , ifnull(list.upidx,0) as upidx
    , list.bbscmtidx as bbscmtidx
    , list.content as content 
    , list.userid as userid 
    , date_format(list.regdate,'%Y-%m-%d') as regdate
from(
    with recursive cte as(
        select
              1 as level
            , cmt.idx
            , cmt.upidx
            , concat('',cmt.idx) as sort 
            , cmt.bbscmtidx
            , cmt.content
            , cmt.userid
            , cmt.regdate
        from training_bbs_cmt cmt
        where 1=1
        and cmt.upidx is null
        union all 
        select
              cte.level+1 as level
            , cmt.idx
            , cmt.upidx
            , concat('',cte.sort,cmt.idx) as sort 
            , cmt.bbscmtidx
            , cmt.content
            , cmt.userid
            , cmt.regdate
        from training_bbs_cmt cmt, cte
        where 1=1
        and cte.idx = cmt.upidx
    ) select * from cte
    where cte.bbscmtidx=#{bbscmtidx} 
    order by cte.sort, cte.idx
) list
    </select>
 
cs

with recursive를 이용한 계층 쿼리를 사용했습니다. level 값은 테이블 칼럼에 있는 값이 아닌 계층을 가리기 위한 임의의 값입니다. 이 level값을 이용해서 대댓글 작성 시 css 클래스 값이 결정됩니다.


Java Script부분에서 중복되는 내용 너무 많았는데 for문을 돌면서 DB에서 가져온 리스트들의 각각의 값을 세팅해야 되는 상황이 많아서 하드 한 코딩이 된 것 같아요..

질문, 더 좋은 방법 등 댓글 환영합니다.

 

즐겁지 않은 즐거운 하세요.

 
Comments