I T H

[프로젝트] 20. 게시판 구현 / mariadb 재귀쿼리 (FINAL) 본문

Spring ArtGallery Project

[프로젝트] 20. 게시판 구현 / mariadb 재귀쿼리 (FINAL)

thdev 2024. 1. 24. 09:52

TH 갤러리의 파이널

마지막 화면을 구현하고자 함.

 

간단한 게시판을 만들고자 하며

댓글까지 입력이 가능하도록 구현 

 

- MariaDB 재귀쿼리를 이용하여 게시판 (트리형태)  형태의 출력물 구현 
- 글 쓴 사용자에 따라 답글달기 / 수정,삭제를 진행할 수 있도록 구분

 

[ incJs.jsp ] - 파일 하단에 추가

<!-- tui grid -->
<script type="text/javascript" src="https://uicdn.toast.com/tui.code-snippet/v1.5.0/tui-code-snippet.js"></script>
<script type="text/javascript" src="https://uicdn.toast.com/tui.pagination/v3.3.0/tui-pagination.js"></script>
<script src="https://uicdn.toast.com/tui-grid/latest/tui-grid.js"></script>

 

[ incCss.jsp ] - tuigrid 관련 추가 

<link rel="stylesheet" href="https://uicdn.toast.com/tui.pagination/v3.3.0/tui-pagination.css" />

 

[ board.jsp ]

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
	
<!DOCTYPE html>
<html lang="kr">

<head>
    <meta charset="UTF-8">
    <meta name="description" content="">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- The above 4 meta tags *must* come first in the head; any other head content must come *after* these tags -->

    <!-- Title  -->
    <title>ART - 게시판</title>

	<!-- import CSS -->
	<%@include file="/resources/inc/incCss.jsp"%>
	
	<!-- jQuery modal 팝업창 사용 CSS -->
	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css" />
</head>

<body>

    <!-- ##### Main Content Wrapper Start ##### -->
    <div class="main-content-wrapper d-flex clearfix">

        <!-- import Header -->
		<%@include file="/resources/inc/incHeader.jsp"%>

        <div class="products-catagories-area section-padding-100">
        	<!-- 게시글 리스트 그리드 영역 -->
            <div id="boardView" class="container-fluid">
                <div class="row">
                    <div class="col-12 col-lg-12">
                        <div class="checkout_details_area mt-50 clearfix">
                            <div class="cart-title">
                                <h4>게시판</h4>
                            </div>
                        </div>
                    </div>
                    
                    <!-- 조회 조건 -->
                    <div class="col-md-3 mb-3">
                    	<small>글 제목</small>
                        <input type="text" class="form-control" id="txtBoardTitle" value="" placeholder="">
                    </div>
                    <div class="col-md-3 mb-3">
                    	<small>작성자</small>
                        <input type="text" class="form-control" id="txtIdName" value="" placeholder="">
                    </div>
                    
                    <!-- 버튼 -->
                    <div class="col-12 col-lg-12" style="text-align: right;">
                    	<a id="btnSearch" class="btn amado-btn-custS w-10">조회</a>
                    	<a id="btnSave" class="btn amado-btn-custI w-10">글쓰기</a>
                    </div>
                </div>
                
                <div id="grid"></div>
            </div>
            
            <!-- 1개의 게시글 보는 영역 -->
	        <div id="boardViewDetail" class="container-fluid" style="display: none;">
	        	<div class="row">
                    <div class="col-12 col-lg-12">
                        <div class="checkout_details_area mt-50 clearfix">
                            <div class="cart-title">
                                <h4>게시글 상세보기</h4>
                            </div>
                        </div>
                    </div>
                    
                    <div class="col-12 col-lg-12" style="text-align: right;">
                    	<a id="btnBoardReply" class="btn amado-btn-custS w-10">답글쓰기</a>
                    	<a id="btnBoardModify" class="btn amado-btn-custE w-10">수정</a>
                    	<a id="btnBoardRemove" class="btn amado-btn-custR w-10">삭제</a>
                    	<a id="btnReturn" class="btn amado-btn-custI w-10">목록으로</a>
                    </div>
                    
                    <div class="col-12 col-lg-12">
                    	<input id="descId" type="hidden"> <!-- 선택한 게시글의 아이디 (수정 시 사용) -->
                    	<input id="descLevel" type="hidden"> <!-- 선택한 게시글의 글 수준레벨 0 : 원글 / 1~ 이상 : 답글 -->
                    	
                    	<table class="table table-bordered">
                    		<tr>
                    			<td id="descNo" style="width: 10%;">-</td>
                    			<td id="descTitle" style="width: 40%;">-</td>
                    			<td id="descWriter" style="width: 25%;">-</td>
                    			<td id="descDate" style="width: 25%;">-</td>
                    		</tr>
                    		<tr>
                    			<td colspan="4">
                    				<pre id="descText"></pre>
                    			</td>
                    		</tr>
                    	</table>
                    </div>
                </div>
	        </div>
        </div>
        
        <!-- 글 등록 팝업창 -->
        <div id="ex1" class="modal">
        	<p id="popTitle">글 등록</p>
			<div class="row">
				<div class="col-md-12 mb-3">
					<small>아이디</small> 
					<input type="text" class="form-control" id="txtPopId" placeholder="아이디 입력" readOnly="readOnly">
					<input type="hidden" id="txtPopParentId" value="0"> <!-- 댓글인지 확인하기 위해 0이면 원글 -->
					<input type="hidden" id="txtPopLevel" value="0"> <!-- 글 레벨을 알기 위해 사용 -->
				</div>
				<div class="col-md-12 mb-3">
					<small>제목</small>
					<input type="text" class="form-control popTxt" id="txtPopBoardTitle" value="" placeholder="제목 입력">
				</div>
                <div class="col-md-12 mb-3">
                	<small>내용</small>
                	<textarea id="txtPopBoardDesc" class="form-control popTxt" rows="10">
                	</textarea>
                </div>
			</div>
			<hr>
			<a id="btnSaveBoard" class="btn amado-btn-custS w-10">저장</a>
			<a id="btnModifyBoard" class="btn amado-btn-custS w-10" style="display: none;">수정</a>
		  	<a href="#" class="btn amado-btn-custE w-10" rel="modal:close">닫기</a>
		</div>
    </div>
    <!-- ##### Main Content Wrapper End ##### -->

    <!-- import Footer -->
	<%@include file="/resources/inc/incFooter.jsp"%>

    <!-- import JS -->
	<%@include file="/resources/inc/incJs.jsp"%>
	
	<!-- jQuery modal 팝업창 사용 라이브러리 -->
	<!-- https://github.com/kylefox/jquery-modal -->
	<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js"></script>
	
	<!-- page script -->
	<script src="/resources/views/board.js"></script>

</body>

</html>

 

[ board.js ]

/*******************************************************************************
 * board.js
 * @author thkim
 * @since 2022
 * @DESC 게시판 화면 스크립트
 ******************************************************************************/
(function() {

	function Board() {

		/* 
		 * private variables
		 */
		var userId = "";
		var grid = null; // 그리드 객체를 담기위한 변수 

		/* 
		 * 초기화 메소드
		 */
		function _init() {
			// 이벤트 처리 함수 호출 
			bindEvent();
			
			// 세션에서 로그인한 사용자 아이디 가져오기
			findSessionInfo();

			// 등록된 게시글 리스트 조회 
			findBoardList();
		}

		function bindEvent() {
			// 조회 버튼 클릭 이벤트
			$("#btnSearch").on("click", function() {
				findBoardList();
			});
			
			// 글쓰기 버튼 클릭 이벤트 - 팝업창을 오픈하여 게시글을 등록할 수 있다.
			$("#btnSave").on("click", function() {
				openModal("1");
			});
			
			// 게시글 등록 버튼 클릭 이벤트 
			$("#btnSaveBoard").on("click", function() {
				saveBoardInfo();
			});
			
			// 답글 쓰기 버튼 클릭 이벤트 
			$("#btnBoardReply").on("click", function() {
				openModal("2");
			});
			
			// 수정 버튼 클릭 이벤트 
			$("#btnBoardModify").on("click", function() {
				openModal("3");
			});
			
			// 삭제 버튼 클릭 이벤트 - 체크박스에 체크한 항목의 게시글을 삭제할 수 있도록 한다.
			$("#btnBoardRemove").on("click", function() {
				var obj = {
					boardId: $("#descId").val()
				}
				saveBoardInfoRemove(obj);
			});
			
			// 목록으로 가기 버튼 클릭 이벤트 
			$("#btnReturn").on("click", function() {
				$("#boardViewDetail").hide();
				$("#boardView").show(); 
				
				// 재조회
				findBoardList();
			});
			
			// 팝업창 내에서 수정 버튼 클릭 이벤트 
			$("#btnModifyBoard").on("click", function() {
				saveModifyBoardInfo();
			});
			
		}
		
		/*
		 * 세션에서 사용자 아이디 가져오기 
		 */
		function findSessionInfo() {
			var obj = {
			};
			
			// 로그인한 세션이 존재하는지 체크 후 아이디 정보를 가져온다. 
			cfFind("/findSession", obj, function(data) {
				userId = data.sessionId;
				
				$("#txtPopId").val(userId); // 등록 팝업에 아이디 세팅해놓음
			}, true, "POST");
		} 
		
		/*
		 * 등록된 게시글 리스트 조회
		 */
		function findBoardList() {
			// 조회용 파라미터 세팅
			// 사용자아이디 or 사용자명으로 조회하거나
			// 게시글 제목으로 조회하거나
			// 두 조건 모두 조회거나 할 때 사용됨
			var obj = {
				userIdName: $("#txtIdName").val(),
				boardTitle: $("#txtBoardTitle").val()
			};
			
			cfFind("/board/findBoardList", obj, function(data) {
				
				// 글 번호를 세팅 
				// 조회한 리스트에 NUM_IDX 컬럼이 추가되는 것임
				$.each(data, function(idx, node) {
					node["NUM_IDX"] = (idx + 1);
				});
				
				// 그리드 만들기 (표)
				setGrid(data);
			}, true, "POST");
		}
		
		/*
		 * 게시글 정보 삭제 
		 */ 
	    function saveBoardInfoRemove(obj) {
			var result = confirm('삭제하시겠습니까?'); // confirm message : 예/아니오 응답을 받기위한 메시지창
			if(result) {
				cfSave("/board/saveBoardInfoRemove", obj, function(data) {
					if(data.success) {
						alert("게시글 정보가 삭제되었습니다.");
						
						$("#boardViewDetail").hide(); 
						$("#boardView").show();
						// 게시글 정보 재조회
						findBoardList(); 
					} else {
						alert("게시글 정보 삭제 실패하였습니다. 확인 후 다시 등록하시기 바랍니다.");
					}
				});
			}
		}
		
		/*
		 * 신규 입력한 게시글 / 답글 정보 저장
		 */ 
	    function saveBoardInfo() {
			// 파라미터 세팅 
			var obj = setParam();
			if(!obj) { return; } // 입력되지 않은 값들이 있으면 (false 가 리턴되어 오는 경우) 
								 // 아래 내용은 패스됨
		
			var result = confirm('저장하시겠습니까?'); // confirm message : 예/아니오 응답을 받기위한 메시지창
			if(result) {
				cfSave("/board/saveBoardInfo", obj, function(data) {
					if(data.success) {
						alert("등록되었습니다.");
						
						// 각 항목을 초기화 
						$(".popTxt").val("");
						// modal 팝업창 닫기 
						$.modal.close();
						$("#boardViewDetail").hide(); 
						$("#boardView").show();
						
						// 게시글 정보 재조회
						findBoardList(); 
					} else {
						alert("등록 실패하였습니다. 확인 후 다시 등록하시기 바랍니다.");
					}
				});
			}
		}
		
		/**
	     * 수정 팝업창에서 수정 버튼 클릭한 경우 
	     * 입력한 정보로 데이터를 업데이트 한다.
		 */
		function saveModifyBoardInfo() {
			var boardTitle = $("#txtPopBoardTitle").val();
			var boardDesc = $("#txtPopBoardDesc").val();
			
			if(boardTitle == null || boardTitle == "") { // 게시글 제목이 입력되지 않은 경우 
				alert("제목을 입력하시기 바랍니다.");
				return false;
			}
			if(boardDesc == null || boardDesc == "") { // 게시글 내용이 입력되지 않은 경우
				alert("내용을 입력하시기 바랍니다.");
				return false;
			}
			
			var obj = {
				boardId: $("#descId").val(),
				boardTitle: boardTitle,
				boardDesc: boardDesc
			}
			
			var result = confirm('글 정보를 수정하시겠습니까?'); // confirm message : 예/아니오 응답을 받기위한 메시지창
			if(result) {
				cfSave("/board/saveModifyBoardInfo", obj, function(data) {
					if(data.success) {
						alert("글 정보가 수정되었습니다.");
						
						// 각 항목을 초기화 
						$(".popTxt").val("");
						// modal 팝업창 닫기 
						$.modal.close();
						// 게시글 정보 재조회
						findBoardList(); 
					} else {
						alert("글 정보 수정 실패하였습니다. 확인 후 다시 수정하시기 바랍니다.");
					}
				});
			}
		}
		
		/*
		* grid setting
		*/
		function setGrid(data) {
			// 이미 그리드가 그려져 있으면 
			if(grid != null) {
				grid.destroy(); // 초기화하기 위해 사용
			}
			
			// 그리드 컬럼 정보 세팅
			var columns = [{
				header: '번호',
				name: 'NUM_IDX',
				width: 70,
				align: 'center'
			}, {
				header: '제목',
				name: 'GRD_BOARD_TITLE',
				whiteSpace: 'pre' // 글자 내에 공백이 있는 경우를 공백 그대로 처리하고자 할 때 사용하는 옵션
			}, {
				header: '작성자',
				name: 'USER_NAME',
				width: 150,
				align: 'center'
			}, {
				header: '등록일시',
				name: 'INPUT_DATETIME',
				width: 150,
				align: 'center'
			}];

			// 그리드 옵션 설정 및 그리드 생성
			grid = new tui.Grid({
				el: document.getElementById('grid'), 
				columns: columns,
				columnOptions: {
			    	resizable: true // 컬럼 사이즈 조정
			    },
			    pageOptions: {
			        useClient: true,
			        perPage: 10
			    }
//			    ,
//			    rowHeaders: ['checkbox'] // 채크박스
			});
			
			grid.resetData(data); // 그리드 데이터 세팅
			tui.Grid.applyTheme('striped'); // 줄무늬 스타일 적용
			
			// 그리드 더블클릭 이벤트 
			// 1개의 행을 더블클릭할 경우 게시글 상세 영역이 노출
			grid.on("dblclick", function(selected) {
				var rowIdx = selected.rowKey; // 선택한 행의 인덱스 
				var item = grid.getRow(rowIdx);
				setBoardDetail(item);
			});
		}
		
		/**
	     * 더블클릭한 1개의 게시글의 상세 내용을 본다. 
		 */
		function setBoardDetail(item) {
			var writor = item.WRITOR_ID; // 작성자 아이디
			var boardLevel = item.BOARD_LEVEL; // 현재 보여지는 글의 레벨
			
			// 게시글 상세 내용 세팅 
			$("#descId").val(item.BOARD_ID); // text, hidden 은 val 로 값 세팅 
			$("#descLevel").val(boardLevel);
			$("#descNo").html(item.NUM_IDX); // table, span 등의 html 태그에는 html 로 값 세팅 
			$("#descTitle").html(item.BOARD_TITLE);
			$("#descWriter").html(item.USER_NAME + " (" + item.WRITOR_ID + ")");
			$("#descDate").html(item.INPUT_DATETIME);
			$("#descText").html(item.BOARD_DESC);
			
			// 게시글 작성자와 현재 로그인한 사용자가 같은 경우 
			// 수정, 삭제 버튼 활성화 + 답글 비활성화
			// 그 외에는 답글 버튼만 활성화 
			if(writor == userId) {
				$("#btnBoardReply").hide();
				$("#btnBoardModify").show();
				$("#btnBoardRemove").show();
			} else {
				$("#btnBoardModify").hide();
				$("#btnBoardRemove").hide();
				$("#btnBoardReply").show();
			}
			
			// 글 레벨이 3 이상인 경우 
			// 답글을 더 쓰지 못하도록 답글 버튼을 비활성화 
			// 0 : 원글, 1 : 댓글, 2: 대댓글, 3: 대대댓글
			if(boardLevel >= 3) {
				$("#btnBoardReply").hide();
			}
			
			$("#boardView").hide();
			$("#boardViewDetail").show(); 
		}
		
		/**
		* 팝업창 오픈
		* 1 : 등록 팝업 / 2 : 답글 팝업 / 3 : 수정 팝업
		*/
		function openModal(inType) {
			$("#ex1").modal(); // 모달(팝업) 창 오픈
				
			// 모달 창을 공용으로 같이 쓰니까 
			// 타이틀도 바꿔줌 
			var title = (inType == "1" ? "글 등록" : (inType == "2" ? "답글 등록" : "글 수정"));
			$("#popTitle").html(title);
			
			// 이미 입력된 정보가 있을 수 있으므로 
			// 각 항목을 초기화 (수정이 아닐때만)
			if(inType != "3") {
				$(".popTxt").val("");
			}
			
			// 답글 등록인 경우 
			// 원글의 아이디와 원글의 글 레벨 + 1을 가져와서 팝업에 세팅 
			if(inType == "1") { // 원글 등록
				$("#txtPopParentId").val("0");
				$("#txtPopLevel").val("0");
				
				$("#btnModifyBoard").hide();
				$("#btnSaveBoard").show();
			} else if(inType == "2") { // 답글 
				var parentId = $("#descId").val();
				var parentLevel = Number($("#descLevel").val()) + 1;
				
				$("#txtPopParentId").val(parentId);
				$("#txtPopLevel").val(parentLevel);
				
				$("#btnModifyBoard").hide();
				$("#btnSaveBoard").show();
			} else {
				// 수정 전 
				// 그전 데이터를 화면에 출력
				$("#txtPopBoardTitle").val($("#descTitle").html());
				$("#txtPopBoardDesc").val($("#descText").html());
				
				$("#btnSaveBoard").hide();
				$("#btnModifyBoard").show();
			}
		}
		
		/**
		 * 저장 시 사용될 파라미터 정보를 세팅
		 */
		function setParam() {
			var userId = $("#txtPopId").val();
			var boardTitle = $("#txtPopBoardTitle").val();
			var boardDesc = $("#txtPopBoardDesc").val();
			var parentId = $("#txtPopParentId").val();
			var boardLevel = $("#txtPopLevel").val();
			
			if(boardTitle == null || boardTitle == "") { // 게시글 제목이 입력되지 않은 경우 
				alert("제목을 입력하시기 바랍니다.");
				return false;
			}
			if(boardDesc == null || boardDesc == "") { // 게시글 내용이 입력되지 않은 경우
				alert("내용을 입력하시기 바랍니다.");
				return false;
			}
			
			var obj = {
				userId: userId,
				boardTitle: boardTitle,
				boardDesc: boardDesc,
				parentId: parentId,
				boardLevel: boardLevel
			}
			
			return obj;
		}

		function _finalize() {
		}

		return {
			init: _init,
			finalize: _finalize
		};
	};

	var board = new Board();
	board.init();

})();

//# sourceURL=board.js

 

[ BoardController.java ] 

package kr.co.art.biz.board.web;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import kr.co.art.biz.board.persistence.BoardMapper;
import kr.co.art.biz.board.service.BoardService;


@Controller
@RequestMapping("/board")
public class BoardController {
	
	@Autowired
	private BoardMapper boardMapper;
	
	@Autowired
	private BoardService boardService;
	
	@RequestMapping("")
	public String main() {
		return "board";
	}
	
	/**
	 * 등록된 게시글 조회
	 * (/board/findBoardList)
	 * @return
	 */
	@RequestMapping("/findBoardList")
	@ResponseBody
	public List<Map<String, Object>> findBoardList(@RequestBody Map<String, Object> param) {
		List<Map<String, Object>> list = boardMapper.findBoardList(param);
		
		return list;
	}
	
	/**
	 * 신규 게시글 등록
	 * (/board/saveBoardInfo)
	 * @return
	 */
	@RequestMapping("/saveBoardInfo")
	@ResponseBody
	public Map<String, Object> saveBoardInfo(@RequestBody Map<String, Object> param) {
		Map<String, Object> result = new HashMap<String, Object>();
		boardService.saveBoardInfo(param);
		
		result.put("success", true);
		return result;
	}
	
	/**
	 * 등록된 게시글 삭제
	 * (/board/saveBoardInfoRemove)
	 * @return
	 */
	@RequestMapping("/saveBoardInfoRemove")
	@ResponseBody
	public Map<String, Object> saveBoardInfoRemove(@RequestBody Map<String, Object> param) {
		Map<String, Object> result = new HashMap<String, Object>();
		boardService.saveBoardInfoRemove(param);
		
		result.put("success", true);
		return result;
	}
	
	/**
	 * 게시글 정보 수정
	 * (/board/saveModifyBoardInfo)
	 * @return
	 */
	@RequestMapping("/saveModifyBoardInfo")
	@ResponseBody
	public Map<String, Object> saveModifyBoardInfo(@RequestBody Map<String, Object> param) {
		Map<String, Object> result = new HashMap<String, Object>();
		boardService.saveModifyBoardInfo(param);
		
		result.put("success", true);
		return result;
	}
	
}

 

[ BoardService.java ]

package kr.co.art.biz.board.service;

import java.util.Map;

public interface BoardService {
	// 신규 게시글 등록
	void saveBoardInfo(Map<String, Object> map);
		
	// 등록된 게시글 정보 삭제 
	void saveBoardInfoRemove(Map<String, Object> map);
	
	// 게시글 정보 수정 
	void saveModifyBoardInfo(Map<String, Object> map);
}

 

[ BoardServiceImpl.java ]

package kr.co.art.biz.board.service;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import kr.co.art.biz.board.persistence.BoardMapper;

@Service
public class BoardServiceImpl implements BoardService {

	@Autowired
	private BoardMapper boardMapper;
	
	@Override
	public void saveBoardInfo(Map<String, Object> map) {
		
		boardMapper.saveBoardInfo(map);
	}
	
	@Override
	public void saveBoardInfoRemove(Map<String, Object> map) {
		
		boardMapper.saveBoardInfoRemove(map);
	}
	
	@Override
	public void saveModifyBoardInfo(Map<String, Object> map) {
		
		boardMapper.saveModifyBoardInfo(map);
	}
	

}

 

[ BoardMapper.java ]

package kr.co.art.biz.board.persistence;

import java.util.List;
import java.util.Map;

public interface BoardMapper {
	
	// 등록된 게시글 리스트 조회
	List<Map<String, Object>> findBoardList(Map<String, Object> params);
	
	// 게시글 정보 등록
	void saveBoardInfo(Map<String, Object> params);
	
	// 게시글 정보 삭제 
	void saveBoardInfoRemove(Map<String, Object> params);
	
	// 게시글 정보 수정 
	void saveModifyBoardInfo(Map<String, Object> params);
	
}

 

[ BoardMapper.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="kr.co.art.biz.board.persistence.BoardMapper">

	<select id="findBoardList" resultType="hashmap" parameterType="hashmap">
		WITH RECURSIVE CTE AS (
			/*부모 지정*/
			SELECT 
			    BOARD_ID, 
			    BOARD_TITLE, 
			    PARENT_ID, 
			    BOARD_LEVEL,
			    WRITOR_ID,
			    BOARD_DESC,
			    USE_YN,
			    CONVERT( LPAD(CONVERT(ROW_NUMBER() OVER(), CHAR(4)), 4, '0'), CHAR(100)) AS SEQ_CHAR,
			    1 AS DEPTH_NO,
			    INPUT_DATETIME 
			FROM ART.ART_BOARD X
			WHERE PARENT_ID = '0'
			UNION ALL
			/*A: 현재 정보이자 부모의 자식, B: 부모 정보 (A는 재귀호출됨)*/
			SELECT
			    A.BOARD_ID, 
			    A.BOARD_TITLE, 
			    A.PARENT_ID, 
			    A.BOARD_LEVEL,
			    A.WRITOR_ID,
			    A.BOARD_DESC,
			    A.USE_YN,
			    CONCAT(B.SEQ_CHAR, LPAD(CONVERT(A.BOARD_LEVEL, CHAR(4)), 4, '0')) AS SEQ_CHAR,
			    B.DEPTH_NO+1  AS DEPTH_NO,
			    A.INPUT_DATETIME
			FROM ART.ART_BOARD A
			INNER JOIN CTE B 
			ON A.PARENT_ID = B.BOARD_ID 
		)
		SELECT 
		    C.BOARD_ID,
		    C.BOARD_TITLE,
    		CONCAT(LPAD('', C.BOARD_LEVEL * 5, ' '), C.BOARD_TITLE) AS GRD_BOARD_TITLE,
		    C.PARENT_ID,
		    C.BOARD_LEVEL,
		    C.BOARD_DESC,
		    C.SEQ_CHAR,
		    C.DEPTH_NO,
		    C.WRITOR_ID,
		    D.USER_NAME,
		    DATE_FORMAT(C.INPUT_DATETIME, '%Y-%m-%d %H:%i:%s') AS INPUT_DATETIME 
		FROM CTE C
		LEFT OUTER JOIN ART.ART_USER D 
		  ON C.WRITOR_ID = D.USER_ID 
		WHERE 1=1
		<if test="!''.equals(userIdName)">
		 AND (C.WRITOR_ID LIKE CONCAT('%', #{userIdName}, '%') OR D.USER_ID LIKE CONCAT('%', #{userIdName}, '%'))
		 </if>
		 <if test="!''.equals(boardTitle)">
		 AND C.BOARD_TITLE LIKE CONCAT('%', #{boardTitle}, '%')
		 </if>
		ORDER BY SEQ_CHAR, INPUT_DATETIME DESC
	</select>
	
	<insert id="saveBoardInfo" parameterType="hashmap">
		INSERT INTO ART.ART_BOARD (
				BOARD_ID
			  , PARENT_ID
			  , BOARD_LEVEL
			  , WRITOR_ID
			  , BOARD_TITLE
			  , BOARD_DESC
			  , USE_YN
			  , INPUT_DATETIME
		)
		VALUES (
			  UUID()
			, #{parentId}
			, #{boardLevel}
			, #{userId}
			, #{boardTitle}
			, #{boardDesc}
			, 'Y' -- default 
			, NOW()
		)
	</insert>
	
	<delete id="saveBoardInfoRemove" parameterType="hashmap">
		DELETE FROM ART.ART_BOARD
		 WHERE 1=1
		   AND BOARD_ID = #{boardId}
	</delete>
	
	<update id="saveModifyBoardInfo" parameterType="hashmap">
		UPDATE ART.ART_BOARD 
		   SET	BOARD_TITLE = #{boardTitle}
			  , BOARD_DESC = #{boardDesc}
		  WHERE 1=1
		    AND BOARD_ID = #{boardId}
	</update>
	
</mapper>