I T H

[포트폴리오 프로젝트 7] Q & A 게시판 구현 본문

Spring MyPortfolio Project

[포트폴리오 프로젝트 7] Q & A 게시판 구현

thdev 2024. 1. 24. 10:46

[게시판 테이블 ]

- 게시글에 대한 테이블

-- MYPORTFOLIO.TBL_QNA_INFO definition

CREATE TABLE `TBL_QNA_INFO` (
  `CODE_ID` varchar(36) NOT NULL COMMENT '코드아이디',
  `UP_CODE_ID` varchar(36) DEFAULT NULL COMMENT '상위코드아이디',
  `TOP_CODE_ID` varchar(36) DEFAULT NULL COMMENT '원글코드아이디',
  `LVL` int(11) DEFAULT NULL COMMENT '글 레벨',
  `USER_ID` varchar(50) DEFAULT NULL COMMENT '사용자 아이디',
  `USER_EMAIL` varchar(50) DEFAULT NULL COMMENT '사용자 이메일',
  `USER_NAME` varchar(20) DEFAULT NULL COMMENT '사용자 이름',
  `QNA_TITLE` varchar(100) DEFAULT NULL COMMENT '제목',
  `QNA_DESC` text DEFAULT NULL COMMENT '내용',
  `TARGET_USER_ID` varchar(50) DEFAULT NULL COMMENT '답글대상자아이디',
  `PRIVATE_YN` char(1) DEFAULT NULL COMMENT '비밀글여부',
  `PASSWORD` char(4) DEFAULT NULL COMMENT '비밀번호',
  `CNT` int(11) DEFAULT NULL COMMENT '조회수',
  `DELETE_YN` char(1) DEFAULT NULL COMMENT '삭제여부',
  `INPUT_DATETIME` datetime DEFAULT NULL COMMENT '등록일자',
  PRIMARY KEY (`CODE_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 

- 파일에 대한 테이블

-- MYPORTFOLIO.TBL_QNA_FILE definition

CREATE TABLE `TBL_QNA_FILE` (
  `CODE_ID` varchar(36) NOT NULL COMMENT '문의글 코드아이디',
  `FILE_ID` varchar(36) NOT NULL COMMENT '파일아이디',
  `USER_ID` varchar(50) DEFAULT NULL COMMENT '사용자 아이디',
  `FILE_ORG_NAME` varchar(200) DEFAULT NULL COMMENT '파일 원본 이름',
  `FILE_NAME` varchar(200) DEFAULT NULL COMMENT '파일 이름',
  `INPUT_DATETIME` datetime DEFAULT NULL COMMENT '등록일자',
  PRIMARY KEY (`CODE_ID`,`FILE_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

[qna.jsp]  -  WEB-INF/views/qna.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<!DOCTYPE html>
<html lang="kr">
<%@include file="/resources/inc/header.jsp"%>

<style>
label {
	display: inline-block;
    margin-bottom: 5px;
    font-weight: inherit;
}
b{
	font-weight: bold;
    font-size: 17px;
}
.row{
	 justify-content: center; 
	 display: flex; /* display: flex; 균등분할하는기능*/ 
	 padding: 5px;
}
.row2 {
	justify-content: center; 
	padding: 5px;
}
.row3 {
	padding: 5px;
	display: block; /* display 기능을 block처리 */
}
.form-label{
	font-size: 14px;
	font-weight: bolder;
}
.bgCol{
    padding: 50px 40px 40px 50px;
    background: #f5f5f5;
    border-bottom: 1px solid #e6e6e6;
    color: #666;
    line-height: 2;
}
.form-control{
	height: 35px;
}

</style>
<body class="home">
<%@include file="/resources/inc/top.jsp"%>


<input id="hidUserId" type="hidden" value="${userId}">
<input id="hidUserName" type="hidden" value="${userName}">
<input id="hidUserEmail" type="hidden" value="${userEmail}">

<main id="main">
	<div class="container" id="boardList">
		<div class="section featured topspace">
			<h2 class="section-title"><span>Q & A 게시판</span></h2>
			
			<div class="row">
				<div class="col-md-3 mb-3">
                	<label class="form-label" >제목</label>
                    <input type="text" class="form-control txtCondition" id="txtBoardTitle" value="" placeholder="" required>
                </div>
                <div class="col-md-3 mb-3">
                	<label class="form-label" >작성자</label>
                    <input type="text" class="form-control txtCondition" id="txtBoardWriter" value="" placeholder="" required>
                </div>
                <!-- 버튼 -->
                <div class="col-md-8" style="text-align: right;">
                	<a id="btnSearch" class="btn btn-warning txtCondition">조회</a>
                	<a id="btnSave" class="btn btn-success txtCondition">글쓰기</a>
                </div>
			</div>
			<br/>
			<!--  그리드 사용  -->
            <div id="grid"></div>
		</div> <!-- / section -->
	</div>	<!-- /container -->
</main>

<!-- 팝업 영역 - 신규문의글등록 -->
<div id="ex1" class="modal" style="height:590px;">
	<h4 id="modalLabel" align="center">문의글 등록</h4>
	<div class="row2">
		<div class="col-md-12">
        	<small>제목</small>
            <input type="text" class="form-control popTxt" id="txtTitle" value="" placeholder="제목 입력" required>
        </div>
        <div class="col-md-12">
        	<small>이메일</small>
            <input type="text" class="form-control" id="txtUserEmail" value="" placeholder="이메일 입력" required>
        </div>
       <div class="col-md-12">
      		<small>이름</small>
          	<input type="text" class="form-control" id="txtUserName" value="" placeholder="이름 입력" required>
        </div>
        <div class="col-md-12">
			<small>내용</small>
			<textarea class="form-control popTxt" id="txtDesc" placeholder="내용 입력" style="width:100%; resize:none;"></textarea>
		</div>
        <div class="col-md-12">
           	<small>첨부파일</small>
            <input type="file" class="form-control popTxt" name="file" id="file" min="0" placeholder="첨부파일" value="">
        </div>
        <div class="col-md-12">
           	<small>비밀번호</small>
            <input type="password" class="form-control popTxt" id="txtPassword" placeholder="비밀번호 입력" value="" readonly="readonly">
        </div>
        <div class="col-md-12">
           	<small>비밀글 여부</small>
            <select class="form-control" id="cmbPrivate">
          		<option id="N">공개</option>
           		<option id="Y">비공개</option>
           </select>
        </div>
	</div>
	
	<br/>
	<div align="right">
		<a id="btnWrite" class="btn btn-warning">등록</a>
	  	<a href="#" class="btn btn-success" rel="modal:close">닫기</a>
	  	<a id="btnReturn" class="btn btn-danger" rel="modal:close">목록으로</a>
	</div>
</div>

<!-- 비밀글여부 팝업창 -->
<div id="exPwd" class="modal" style="height:320px;">
	<h5 align="center">문의내용 확인</h5>
	<small>비공개 문의글은 등록 시 입력한 비밀번호를 입력해야 확인 가능합니다.</small>
	<hr>
		<div class="row" style="display:block;">
			<div class="col-md-10 mb-3">
				<small>비밀번호</small>
				<input type="password" class="form-control popTxt" id="txtPopPwd" value="" placeholder="비밀번호 입력" required>
			</div>
		</div>
		<hr>
		<div align="right">
			<button class="btn btn-success" id="btnPasswordCheck">확인</button>
			<a href="#" class="btn btn-warning" rel="modal:close">닫기</a>
		</div>
</div>


 <!-- 게시글 상세보기 -->

<div id="boardDetail" style="display: none;">
	<div class="container" >
		<div class="section featured topspace" >
			<h2 class="section-title"><span>게시글 상세보기</span></h2>
			<div class="bgCol">
				<div class="row">
					<div class="col-sm-4 col-md-4">
						<label class="form-label" >No</label>
						<input type="text" id="descNo" class="form-control">
					</div>
					<div class="col-sm-4 col-md-4">
						<label class="form-label" >작성자</label>
						<input type="text" id="descWriter" class="form-control">
					</div>
					<div class="col-sm-4 col-md-4">
						<label class="form-label" >작성일자</label>
						<input type="text" id="descDate" class="form-control">
					</div>
				</div>
				<div class="row">
					<div class="col-sm-12 col-md-12">
						<label class="form-label" >제목</label>
						<input type="text" id="descTitle" class="form-control">
					</div>
				</div>
				<div class="row">
					<div class="col-sm-12 col-md-12">
						<label class="form-label">내용</label>
						<textarea class="form-control" id="descText" style="width:100%;" resize:none;"></textarea>
					</div>
				</div>
				<div class="row">
					<div class="col-sm-12 col-md-12">
						<label class="form-label">첨부파일</label>
						<div id="FileList"></div>
					</div>
				</div>
			</div>
			
			
			<div class="row">
				<div class="col-sm-12 col-md-12">
				<sec:authorize access="isAuthenticated()">
					<hr/>
					<label class="form-label">답글쓰기</label>
					<textarea class="form-control" id="textReply" style="width:100%; resize:none;"></textarea>
					<br/>
					<a id="btnReply" class="btn btn-warning">답글등록</a>
				</sec:authorize>
				
					<a id="btnReturn2" class="btn btn-danger">목록으로</a>
					
				<sec:authorize access="isAuthenticated()">
					<a id="btnQnaDelete" class="btn btn-success" style="display:none;" >문의글삭제</a>
				</sec:authorize>
				</div>
			</div>
			
		<br/>
	  	<br/>
		<hr>
		
		</div> <!-- / section -->
	</div>	<!-- /container -->	
	
	<div class="container" >
		<div class="section featured topspace">
			<div class="checkout_detail_area mt-50 clearfix">
				<div class="cart-title row2">
					<h4>댓글</h4>
					<div class="divider"></div>
				</div>
			</div>
			<div id="panelReply" style="min-height: 400px; overflow-x: hidden; overflow-y:auto; font-size: 0.9rem; padding: 1rem;">
				<p>답변이 없습니다.</p>
			</div>
		</div>
	</div>	
</div>

<div style="clear:both;"></div>

		

<%@include file="/resources/inc/footer.jsp"%>
<%@include file="/resources/inc/incJs.jsp"%>

<!-- page script -->
<script src="/resources/views/qna.js"></script>

</body>
</html>

[qna.js]  -  resources/views/qna.js

/**
 * qna.js
 * @author thevalue
 * @since 2023
 * @DESC Q & A 게시판
 */

 (function(){
	 
	 function Qna(){
		 
		//private variables
		 var userId = $("#hidUserId").val();
		 var userName = $("#hidUserName").val();
		 var userEmail = $("#hidUserEmail").val();
		 // alert(userId + ", " + userName + ", " + userEmail);
		 
		 //그리드 객체 담기위한 변수
		 var grid = null;
		 var SELECTED_CODE_ID = ""; //문의글 원글에 대한 코드 정보 // TOP_CODE_ID 
		 var SELECTED_REPLY_ID = ""; //1레벨 댓글에 대한 코드 정보 //UP_CODE_ID
		 var OPENED_REPLY_ID = ""; //열려있는 답글 리스트 재조회 시 다시 열도록 사용 // ▼
		 
		//초기화 메서드
		function _init(){
			//이벤트 처리 함수 호출
			bindEvent();
			
			//등록된 문의글 리스트 조회
			findBoardList();
		}
		
		function bindEvent(){
			//조회버튼 클릭 이벤트
			$(".txtCondition").keydown(function(key) {
				//엔터키 쳤을때 이벤트
				if (key.keyCode == 13) {
					findBoardList();
				}
			});
			$("#btnSearch").on("click", function(){
				
				findBoardList();
			});
			
			//글쓰기 버튼 클릭시 팝업창띄우기 이벤트
			$("#btnSave").on("click", function(){
				
				$("#modalLabel").html("문의글 등록");
				
				//세션에서 받아온 이메일과 이름 데이터를 넣어준다.
				$("#txtUserEmail").val(userEmail);
				$("#txtUserName").val(userName);
				
				$("#ex1").modal({
					clickClose: false
				});//모달(팝업) 창 오픈
				
				
				// 이미 입력된 정보가 있을 수 있으므로
				// 각 항목을 초기화
				$(".popTxt").val("");
		
			 });
			 
			//신규 문의글등록 버튼 클릭 이벤트
			$("#btnWrite").on("click", function(){
				saveBoardInfo();
			})
			
			 //비밀글 여부
			 $("#cmbPrivate").on("change", function(){
				 var id = $("#cmbPrivate option:selected")[0].id;
				 console.log("id", id);
				 if(id == "N"){ //n : 공개 //Y : 비공개
					 $("#txtPassword").val("")
					 $("#txtPassword").attr("readonly", true);
				 } else {
					 $("#txtPassword").attr("readonly", false);
				 }
			 })
			 
			 //목록으로 가기 버튼클릭 이벤트 boardList
			 $("#btnReturn, #btnReturn2").on("click", function(){
				//modal 팝업창 닫기
				 $.modal.close();
				 $("#boardDetail").hide();
				 $("#boardList").show();
				 //재조회
				findBoardList();
			 })
			 
			 //문의글 삭제 (문의글 상세보기 할때 보임)
			 $("#btnQnaDelete").on("click", function(){
				saveBoardInfoRemove();
			})
			 
		}//bindEvent
		
		//등록된 게시글 리스트 조회
		function findBoardList(){
			//alert("리스트조회");
			var obj = {
				txtBoardTitle: $("#txtBoardTitle").val(),
				txtBoardWriter: $("#txtBoardWriter").val(),
			};
			cfFind("/qna/findBoardList.do", obj, function(data){
				//글 번호 세팅
				 //조회한 리스트에 NUM_IDX 컬럼이 추가되는 것임
				$.each(data, function(idx, node){
					node["NUM_IDX"] = (idx + 1); //NUM_IDX(글번호 컬럼 추가 => grid에 뿌릴때 사용할것)
					
					if(node.PRIVATE_YN == "Y"){ //비밀글이라면
						node["QNA_TITLE"] = "🔐&nbsp;" + node.QNA_TITLE; //비밀글이면 제목에 열쇠모양 표시를 앞에 넣어줌.
					}
				});
				console.log(data);
				//그리드 만들기(표)
				setGrid(data);
			}, true, "POST");
		} 
		
		
		//신규 문의글 등록
		function saveBoardInfo(){
			var obj = setParam();
			
			if(!obj){
				return;
			}
			
			var subObj = {};
			 var codeInfo = "";
			 cfFind("/qna/findCodeInfo.do", subObj, function(subData){
				 //TBL_QNA_INFO테이블의 코드아이디(codeInfo - PK)에 넣을 데이터 조회
				 console.log("subData", subData);
				 obj.codeInfo = subData.code;
				 codeInfo = subData.code;
			 }, true, "POST");
			 
			 var file = $("#file").val();
			 console.log(file);
			
			 //파일이 포함되지 않은 경우 글등록
			 if(file == "" || file == null){
				 console.log(obj);
				 cfSave("/qna/saveBoardInfo.do", obj, function(data){
					 if(data.success){
						 alert("문의글이 등록되었습니다.");
						 $(".popTxt").val(""); //등록폼 초기화
						 $("#btnReturn").click(); //목록으로 가기
					 } else {
						 alert("글 등록 실패, 관리자에게 문의하시기 바랍니다.");
						 return;
					 }
				 }, true, "POST");
				 
			 } else { //파일이 포함된 경우 글등록
				 
				 var ext = $("#file").val().split('.').pop().toLowerCase(); 
				 //ex) abc.txt 에서 txt 확장자만 추출
				 if($.inArray(ext,['gif', 'png', 'jpg', 'jpeg', 'zip']) == -1){
					 alert("gif,png,jpg,jpeg,zip 파일만 업로드 할수 있습니다.");
					 return false;
				 }
				 
				 var dataObj = {
					 codeInfo: codeInfo,
					 userId: userId
				 };
				 
				 cfUpload("/qna/fileUpload.do", dataObj, function(rData){
					 console.log("rData", rData);
					 if(rData.success){
						 cfSave("/qna/saveBoardInfo.do", obj, function(data){
							 if(data.success){
								 alert("문의글이 등록되었습니다.");
								// setInitQnaForm();//문의글 초기화 메서드 호출
								 $(".popTxt").val(""); //등록폼 초기화
								 $("#btnReturn").click(); //목록으로 가기
							 }
						 },true, "POST");
					 } else {
						 alert("글 등록 실패, 관리자에게 문의하시기 바랍니다.");
						 return;
					 }
				 });
			 }
		}//문의글 등록 함수 끝
		
		//grid setting
		function setGrid(gridData){
			console.log(gridData);
			//이미 그리드가 그려져 있으면
			if(grid != null){
				grid.destroy();//초기화하기 위해 사용
			}
			//그리드 컬럼 정보 세팅
			var columns = [{
				header: '번호',
				name: 'NUM_IDX',
				align: "center",
				width: 170
			}, {
				header: '제목',
				name: 'QNA_TITLE',
				align: "center",
				width: 170
			}, {
				header: '관리자답변',
				name: 'REPLY_YN',
				align: "center",
				width: 170
			}, {
				header: '작성자',
				name: 'USER_NAME',
				align: "center",
				width: 170
			}, {
				header: '조회수',
				name: 'CNT',
				align: "center",
				width: 170
			}, {
				header: '등록일자',
				name: 'INPUT_DATETIME',
				align: "center",
				width: 170
			}];
			
			//그리드 옵션 설정 및 그리드 생성
			grid = new tui.Grid({
				el: document.getElementById('grid'),
				columns: columns,
				scrollX: true,
				scrollY: "auto",
				bodyHeight: 420,
				width: "100%",
				contextMenu: null,
				columnOptions: {
					resizable: true //컬럼 사이즈 조정
				},
				pageOptions: {
					perPage: 5, //한번에 보여줄 데이터 수
					useClient: true
				}
				//rowHeaders: ['checkbox'] //체크박스
			});
			
			grid.resetData(gridData);//그리드 데이터 세팅
			tui.Grid.applyTheme('striped'); //줄무늬 스타일 적용
			
			//그리드 더블클릭 이벤트
			//1개의 행을 더블클릭할 경우
			//grid.on("click"),function(selected){}
			grid.on("dblclick", function(selected){
				var rowIdx = selected.rowKey; //선택된 행의 인덱스
				var item = grid.getRow(rowIdx);
				console.log(item);
				
				 //비밀글인 경우와 비밀글이 아닌경우 글 상세보기 처리
				if(item.PRIVATE_YN == "Y" && userId != "admin"){//비밀글이면서 관리자가 아닐 경우에만 비밀번호 입력 모달창 띄우기
					 //다른 방법은 User_Auth !=2(1:일반 사용자 , 2는 관리자)
					 $("#exPwd").modal({
						 clickClose:false
					 });
					 //txtPopPwd 비공개 문의글 확인(비밀글인 경우 비밀번호 입력후 보기)
					 $("#btnPasswordCheck").off("click");
					 $("#btnPasswordCheck").on("click", function(){
						 var pwd = $("#txtPopPwd").val();
						 if(pwd == "" || pwd == null){
							 alert("비공개글 비밀번호를 입력하시기 바랍니다.");
							 return;
						 }
						 
						 if(pwd == item.PASSWORD){ //입력한 비밀번호와 비밀글의 비밀번호와 일치
							 //modal 팝업창 닫기
							 $.modal.close();
							 $("#txtPopPwd").val(""); //초기화
							 
							 //글 상세조회
							 setBoardDetail(item);
						 } else {
							 alert("비밀번호가 일치하지 않습니다.");
							 return;
						 }
					 });
				 } else { //비밀글이 아닐 경우
					 //글 상세조회
					 setBoardDetail(item);
				 }
	 		 }); 
		} //grid 끝
		
		//게시글 상세보기
		function setBoardDetail(item){
			
			//클릭한 사용자 정보 팝업창에 세팅
			console.log("item", item); //한 행에 대한 데이터
			SELECTED_CODE_ID = item.CODE_ID;
			
			 //문의글 상세내용 세팅
			 $("#descNo").val("No." + item.NUM_IDX); //글번호
			 
			// console.log(item.QNA_TITLE);
			 var title = item.QNA_TITLE;
			// console.log(title.substr(8));
			console.log(title.includes("🔐"));
			if(title.includes("🔐")){
				title = title.substr(8);
				 $("#descTitle").val(title); //제목
			} else {
				$("#descTitle").val(title);
			}
			
			$("#btnQnaDelete").hide();//삭제버튼 초기화
			 if(item.USER_ID == "" || item.USER_ID == null){
				 $("#descWriter").val(item.USER_NAME);
			 } else {
				 if(userId == item.USER_ID){ //로그인한 유저아이디와 문의글을 썻던 유저아이디와 같다면
					$("#btnQnaDelete").show();
				 }
				 $("#descWriter").val(item.USER_NAME + "(" + item.USER_ID + ")");
			 }
			 $("#descDate").val(item.INPUT_DATETIME);
			 $("#descText").val(item.QNA_DESC);
			
			
			//파일리스트 불러오기
			 var obj = {
				 codeInfo: SELECTED_CODE_ID
			 };
			 console.log(obj);
			 cfFind("/qna/findBoardFileList.do", obj, function(data){ //글 상세보기에서 파일 리스트 뿌리기
				 console.log("data", data);
				 var html = "";
				 if(data.length <= 0){
					 html +="<input class='form-control' placeholder='등록된 첨부파일이 없습니다' readonly/>"
				 }
				 $.each(data, function(idx, node){
					 
					 html += "<input id = '" + idx + "' class='lineFile form-control' style='cursor: pointer;' value='"
					 html += node.FILE_ORG_NAME;
					 html += "'/>"
				 });
				 $("#FileList").html(html);
				 
				 $(".lineFile").off("click");
				 //파일 다운로드
				 $(".lineFile").on("click", function(){
					 var id = $(this)[0].id;
					 console.log(id);
					 var obj = {
						 fileName: data[id].FILE_NAME,
						 fileOrgName: data[id].FILE_ORG_NAME,
						 fileDir: "qna"
					 };
					 cfCustomExcelDownloadDyn("/downloadFiles", obj); //파일다운로드 -> 메인컨트롤러에 있음.
				 });
				 
			 }, true, "POST");
			 
			$("#boardList").hide();
			$("#boardDetail").show();
			
			 //글 조회수 업데이트
			 var saveObj = {
				 codeInfo: SELECTED_CODE_ID
			 }
			 cfSave("/qna/saveBoardCnt.do", saveObj, function(data){
				 console.log(data.success); //true
			 }, true, "POST")
			
			
			 //답글쓰기 버튼 이벤트
			 $("#btnReply").off("click");
			 $("#btnReply").on("click", function(){
				 saveReplyInfo(item);
			 });
			 
			 //댓글 리스트 조회
			 findReplyList();
			 
		}//게시글 상세보기
		
		
		 //문의글 삭제 (TBL_QNA_INFO, TBL_QNA_FILE 데이터 삭제)
	     function saveBoardInfoRemove(){
			var obj = {
				codeInfo : SELECTED_CODE_ID
			}
			var result = confirm("문의글을 삭제하시겠습니까?");
			if(result){
				cfSave("/qna/saveBoardInfoRemove.do", obj, function(data){
					if(data.success){
						alert("문의글 삭제가 완료되었습니다.");
						$("#btnReturn2").click(); //목록으로가기
					}
				},true, "POST");
			}
	 	 }
		
		//답글쓰기
		function saveReplyInfo(item){
			 //jsp내에서 시큐리티 인증 처리 없을때는 아래와 같이 사용한다. 시큐리티를 처리한다면 아래부분 사용할 필요는 없다.
			/* if(userId == null || userId == ""){//로그인한 사용자만 댓글 달수있게 추가
				 alert("로그인 후 댓글 사용하실 수 있습니다.");
				 $("#txtReply").val("");
				 return;
			 }*/
			 
			 var reply = $("#textReply").val();
			 if(reply == "" || reply == null){
				 alert("답글 내용을 입력하시기 바랍니다.");
				 return;
			 }
			 
			 var obj = {
				 qnaTitle: "답글",
				 userEmail: userEmail,
				 userName: userName,
				 qnaDesc: reply,
				 password:"",
				 userId:userId,
				 privateYn:"N",
				 codeInfo:"",//서버사이드에서 UUID로 처리
				 upCodeId: SELECTED_CODE_ID,
				 topCodeId: SELECTED_CODE_ID,
				 targetUserId:""
			 }
			 
			 cfSave("/qna/saveReplyInfo.do", obj, function(data){
				 console.log(obj);
				 if(data.success){
					 alert("답글 입력이 완료되었습니다.");
					 $("#textReply").val("");
					 
					 //답글 리스트 재조회
					 findReplyList();
				 }
			 }, true, "POST");
			 
		 } //답글쓰기
		
		//댓글 리스트 조회
		function findReplyList(){
			var obj = {
				upCodeId: SELECTED_CODE_ID
			};
			
			//답글 영역 초기화
			$("#panelReply").html("<p>답변이 없습니다.</p>");
			var html = "";
			cfFind("/qna/findReplyList.do", obj, function(data){
				console.log("data", data);
				
				$.each(data, function(idx, node){
					 html += "<div class='row row3'>";
					 html += "		<div class='col-md-6'>";
					 html += "			<p style='margin: 0 0 15px 0;'><b>" + node.USER_NAME + "(" + node.USER_ID + ") </b>";
					 html += "&nbsp;&nbsp;|&nbsp;&nbsp;	<span>" + node.INPUT_DATETIME + "</span>";
					 if(userId !="" && node.USER_ID != userId) {//본인 댓글에는 답글 쓰지 못하도록 함.
						 html += "&nbsp;&nbsp;|&nbsp;&nbsp;<span id='" + node.CODE_ID + "' class='w-btn w-btn-green button-hover btnSubReply mainReply' style='cursor:pointer'>답글쓰기</span></p>";
					 } else {
						 html += "</p>";
					 }
					 html += "		</div>"
					 html += "		<div id= 'subBtnArea_" + node.CODE_ID + "' class='col-md-6' style='text-align: right;'>";
					  if(userId !="" && node.USER_ID == userId) {//본인 댓글은 수정, 삭제 버튼 활성화
						html += "		<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubModify'>수정</button>"
						html += "		<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubDelete'>삭제</button>"
					 } else {
						 html += "&nbsp;";
					 }
					 html += "		</div>";
					 html += "</div>";
					 
					 html += "<div id = '" + node.USER_ID + "' class='row row3'>";
					 html += "		<div id='modNot_" + node.CODE_ID + "' class='col-md-12'>"
					 html += "			<b>" + node.TARGET_USER_ID +"</b>&nbsp;&nbsp;<label id='modify'>" + node.QNA_DESC + "</label>"
					 html += "		</div>"
					 html += "		<div id='mod_" + node.CODE_ID + "' class='col-md-12' style='display: none;'>";
					 html += "			<textarea class='form-control' id='txtMod_" + node.CODE_ID + "' rows='1' style='width: 100%; resize: none;'>" + node.QNA_DESC + "</textarea><br/>"
					 html += "			<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubReplyModOk'>수정</button>";
					 html += "			<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubReplyModCancel'>취소</button>";
					 html += "		</div>"
					 html += "		<div id='re_" + node.CODE_ID + "' class='col-md-12 rePanel mb-3' style='display: none;'>";
					 html += "			<textarea class='form-control' id='txt_" + node.CODE_ID + "' rows='1' style='width: 100%; resize: none;'></textarea><br/>"
					 html += "			<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubReplyOk'>등록</button>";
					 html += "			<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubReplyCancel'>취소</button>";
					 html += "		</div>";
					 html += "</div>";
					 if(node.REPLY_CNT > 0){
						 html += "		<div class='col-md-12'>";
						 html +="			<p id='" + node.CODE_ID + "' class='btnReplyCnt off' style='cursor:pointer'><span id='span_" + node.CODE_ID + "'>▲</span> 답글 " + node.REPLY_CNT + "건</p>";
						 html +="			<div style='margin-left: 25px; padding: 5px 10px;' id='reply_" + node.CODE_ID + "'></div>";
						 html += "		</div>";
					 }
					 html += "<hr>";
				 });

				if(data.length > 0){
					//답글 리스트 세팅
					$("#panelReply").html(html);
				
					
					//버튼 클릭 이벤트 처리
					setBtnEventAfterHtml();
					
					//대댓글 카운트 클릭 이벤트
					//댓글에 대한 댓글 리스트를 조회하여 화면에 출력함
					$(".btnReplyCnt").off("click"); //off -> 버튼클릭할때마다 이벤트 처리가 누적되기 때문에 한번 초기화 시켜주는것임.
					$(".btnReplyCnt").on("click", function(){
						console.log($(this)[0].id);
						var subId = $(this)[0].id; //id에 <p id = NODE_ID> 이렇게 36자리가 매핑되있음. 그값을 subId에 넣음. id값은 매핑되있는 거에 따라 달라질수있음.
						var onOff = $(this).hasClass("off"); //off라는 클래스이름이 있는지 체크
						
						//열린 댓글 화면들 모두 닫기 위해서 처리 START
						$(this).removeClass("off"); //off라는 클래스 이름을 지움
						$(this).removeClass("on"); //on이라는 클래스 이름을 지움
						$("span[id^='span_']").html("▲"); //span태그에서 id가 span_ 로 시작하는 모든 아이디를 찾아서 적용해라.
						$("div[id^='reply_']").html(""); //div태그에서 id가 reply_로 시작하는 모든 아이디를 찾아서 적용해라.
						$("div[id^='reply_']").css("border", "none");
						$("div[id^='reply_']").css("background", "none");
						$("div[id^='re_']").hide();
						$("div[id^='mod_']").hide();
						$("div[id^='modNot_']").show();
						//열린 댓글 화면들 모두 닫기 위해서 처리 END
						
						//토글기능처리 ▲ ▼
						if(onOff){ //off라는 클래스 이름이 있으면
							$(this).addClass("on"); //토글기능 : off -> on
							$("#span_" + subId).html("▼");
							OPENED_REPLY_ID = subId; //subId; //<p id = node.CODE_ID >  36자리
						} else {
							$(this).addClass("off"); //토글기능 : on -> off
							$("#span_" + subId).html("▲");
							OPENED_REPLY_ID = "";
						}
						
						if(onOff){
							findSubReplyList();
						} else {
							$("#reply_" + subId).html("");
						}
					});
				}
			},true, "POST");
			
		}//댓글 리스트 조회
		
		//화살표▼ 답글 클릭 시 대댓글 리스트 조회
		 function findSubReplyList(){
			 var subObj = {
				 upCodeId: OPENED_REPLY_ID //(댓글에 대한 CODE아이디) -> upCodeId(부모코드)
			 };
			 
			 var subHtml = "";
			 cfFind("/qna/findReplyList.do", subObj, function(subData){
				 console.log("subData", subData);
				 
				 $.each(subData, function(idx, node){
					 subHtml += "<div id = '" + node.USER_ID + "' class='row row3'>";
					 subHtml += "		<div class='col-md-6'>";
					 subHtml += "			<p style='margin: 0 0 15px 0;'><b>" + node.USER_NAME + "(" + node.USER_ID + ") </b>";
					 subHtml += "&nbsp;&nbsp;|&nbsp;&nbsp;	<span>" + node.INPUT_DATETIME + "</span>";
					 if(userId !="" && node.USER_ID != userId) {//본인 댓글에는 답글 쓰지 못하도록 함.
						 subHtml += "&nbsp;&nbsp;|&nbsp;&nbsp;<span id='" + node.CODE_ID + "' class='w-btn w-btn-green button-hover btnSubReply' style='cursor:pointer'>답글쓰기</span></p>";
					 } else {
						 subHtml += "</p>";
					 }
					 subHtml += "		</div>";
					 
					
					 subHtml += "		<div id= 'subBtnArea_" + node.CODE_ID + "' class='col-md-6' style='text-align: right;'>";
					  if(userId !="" && node.USER_ID == userId) {//본인 댓글은 수정, 삭제 버튼 활성화
						subHtml += "		<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubModify'>수정</button>"
						subHtml += "		<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubDelete'>삭제</button>"
					 } else {
						 subHtml += "&nbsp;";
					 }
					 subHtml += "		</div>";
					 subHtml += "</div>";
					 
					 subHtml += "<div id = '" + node.USER_ID + "' class='row row3'>";
					 subHtml += "		<div id='modNot_" + node.CODE_ID + "' class='col-md-12'><p>"
					 subHtml += "			<b>" + node.TARGET_USER_ID +"</b>&nbsp;&nbsp;<label id='modify'>" + node.QNA_DESC + "</label>"
					 subHtml += "		</div>"
					 subHtml += "		<div id='mod_" + node.CODE_ID + "' class='col-md-12' style='display: none;'>";
					 subHtml += "			<textarea class='form-control' id='txtMod_" + node.CODE_ID + "' rows='1' style= 'resize: none;'>" + node.QNA_DESC + "</textarea><br/>";
					 subHtml += "			<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubReplyModOk'>수정</button>";
					 subHtml += "			<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubReplyModCancel'>취소</button>";
					 subHtml += "		</div>"
					 subHtml += "		<div id='re_" + node.CODE_ID + "' class='col-md-12 rePanel mb-3' style='display: none;'>";
					 subHtml += "			<textarea class='form-control' id='txt_" + node.CODE_ID + "' rows='1' style='width: 100%; resize: none;'></textarea><br/>";
					 subHtml += "			<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubReplyOk'>등록</button>";
					 subHtml += "			<button id='" + node.CODE_ID + "' class='btn btn-sm btn-secondary fg-white btnSubReplyCancel'>취소</button>";
					 subHtml += "		</div>"
					 subHtml += "</div>";
				 });
				 
				 if(subData.length > 0){
					 $("#reply_" + OPENED_REPLY_ID).html(subHtml);
					 $("#reply_" + OPENED_REPLY_ID).css("border", "2px solid whitesmoke");
					 $("#reply_" + OPENED_REPLY_ID).css("background", "#f5f5f5");
					 
					 // 버튼 클릭 이벤트 처리
					 // 대댓글인 경우 아이디를 보내주어 답글 등록시 사용하도록함
					 setBtnEventAfterHtml(OPENED_REPLY_ID);
				 }
				 
			 }, false, "POST");
		}
		
		
		 //html 출력후
		 //버튼클릭 이벤트 처리
		 function setBtnEventAfterHtml(){
			 
			 //답글쓰기버튼 이벤트 처리 (답글에 대한 대댓글을 쓸때 사용하는 버튼)
			 $(".btnSubReply").off("click");
			 $(".btnSubReply").on("click", function(){
				 $(".rePanel").hide();//대댓글 입력창 모두 숨김 처리 ->뒤에서  $("#re_" + id).fadeIn(); 으로 댓글쓰는부분열것임.
				 // 사용자가 해당 글에서 대댓글 쓸때 다른곳의 대댓글쓰는 창을 모두 숨김처리하기 위한 기능임.
				 
				 
				 //1레벨 댓글의 답글쓰기 버튼을 클릭한 경우에는 오픈된 대댓글 리스트를 모두 닫고 코드값을 초기화
				 var mainReply = $(this).hasClass("mainReply"); //mainReply라는 클래스 선택자가 있다면
				 if(mainReply){
					 OPENED_REPLY_ID = ""; //열려있는 답글 리스트 재조회 시 다시 열도록 하는 변수 ▼
					 
					 //열린 댓글 화면들 모두 닫기 위해서 처리 START
					 $(".btnReplyCnt").removeClass("on"); //.btnReplyCnt로 시작하는 클래스 중에서 열려있는속성(class = on)을 다 제거하고  on = ▼ 열림
					 $(".btnReplyCnt").addClass("off");//.btnReplyCnt로 시작하는 클래스 중에서 닫힌속성(class = off)를 다시 열어준다. off =  ▲ 닫힘
					 $("span[id^='span_']").html("▲"); //span태그중에서 id선택자가 span_ 로 시작하는것을 찾아서 ▲ 를 넣어줌.
					 $("div[id^='reply_']").html("");
					 $("div[id^='reply_']").css("border", "none");
					 $("div[id^='reply_']").css("background", "none");
					 $("div[id^='re_']").hide();
					 $("div[id^='mod_']").hide();
					 $("div[id^='modNot_']").show();
					 //열린 댓글 화면들 모두 닫기 위해서 처리 END
				 }
				 
				 var id = $(this)[0].id; // NODE.CODEID로 이미 매핑된 36자리 코드를 변수 id에 넣음. 
				 //<span id='" + node.CODE_ID + "' class='btnSubReply mainReply' style='cursor:pointer;'>[답글쓰기]</span>
				 
				 if(OPENED_REPLY_ID != ""){ // ▼
					 SELECTED_REPLY_ID = OPENED_REPLY_ID 
					 //var SELECTED_REPLY_ID = ""; //1레벨 댓글에 대한 코드 정보 //UP_CODE_ID(부모코드아이디)
				 } else {
					 SELECTED_REPLY_ID = id;
				 }
				 
				 //alert(id);
				 $("#re_" + id).fadeIn(); //fadeIn() : 서서히 나타나게 하는 함수
				 
			 });

			 //대댓글 취소 버튼 이벤트
			 $(".btnSubReplyCancel").off("click");
			 $(".btnSubReplyCancel").on("click", function(){
				 $(".rePanel").hide();//대댓글 입력창 모두 숨김 처리
			 });

			 //대댓글 등록 버튼 이벤트
			 $(".btnSubReplyOk").off("click");
			 $(".btnSubReplyOk").on("click", function(){
				 var id = $(this)[0].id; //id => node.CODE_ID로 이미 매핑된 36자리
				 
				 var value = $("#txt_" + id).val();
				 if(value == "" || value == null){
					 alert("답글 내용을 입력하시기 바랍니다.");
					 return;
				 }
				 
				 var writeUserId = ""; //@아이디를 쓰기 위한 변수
				 if(OPENED_REPLY_ID != ""){ // ▼ 대댓글리스트가 보여짐.
					 writeUserId = "@" + $(this).parent().parent()[0].id; //댓글 3단계 이후부터 대대댓글(3단계) 달때 누구한테 댓글 달껀지 상대방의 @아이디
				 }
				 
				 var subObj = {
					 qnaTitle: "답글",
					 userEmail: userEmail,
					 userName: userName,
					 qnaDesc: value,
					 password:"",
					 userId:userId,
					 privateYn:"N",
					 codeInfo:"",//서버사이드에서 UUID로 처리, pk이기 떄문에 값이 중복되면 안됨!
					 upCodeId: SELECTED_REPLY_ID, //부모(1레벨 댓글)
					 topCodeId: SELECTED_CODE_ID, //조상(원글=문의글)
					 targetUserId: writeUserId || ""
				 }
				 console.log(subObj);
				 cfSave("/qna/saveReplyInfo.do", subObj, function(subData){ //대댓글 입력도 답글쓰기와 같은 메서드를 사용함.
					 if(subData.success){
						 alert("답글 입력이 완료되었습니다.");
						 $("#txt_" + id).val("");
						 
						 //답글 리스트 재조회
						 findReplyList();
						 
						 //열려있는 대댓글이 있었다면.. ▼
						 //대댓글 리스트 재조회
						 if(OPENED_REPLY_ID != ""){
							 findSubReplyList();
							 $("#reply_" + OPENED_REPLY_ID).fadeIn(); //대댓글 리스트 영역 열기
							 $("p[id='" + OPENED_REPLY_ID + "']").removeClass("off");
							 $("p[id='" + OPENED_REPLY_ID + "']").addClass("on");
							 $("#span_" + OPENED_REPLY_ID).html("▼");
						  };
						 $("#mod_" + id).hide();
						 $("#modNOt_" + id).show();
							 
						}
				 }, true, "POST");
				 
			 });

			 //댓글,대댓글 수정버튼 클릭 이벤트
			 $(".btnSubModify").off("click");
			 $(".btnSubModify").on("click", function(){
				 var id = $(this)[0].id;
				 console.log("id", id);
				 
				 $("#modNot_" + id).hide();
				 $("#subBtnArea_" + id).hide();
				 $("#mod_" + id).show();
			 });

			 //수정 완료 버튼 클릭 이벤트
			 $(".btnSubReplyModOk").off("click");
			 $(".btnSubReplyModOk").on("click", function(){
				 var id = $(this)[0].id;
				 var value = $("#txtMod_" + id).val();
				 if(value == "" || value == null){
					 alert("답글 내용을 입력하시기 바랍니다.");
					 return;
				 }
				 
				 var subObj = {
					 codeInfo: id,
					 desc: value
				 };
				 
				 var result = confirm("수정하시겠습니까?");
				 if(result){
					 cfSave("/qna/saveReplyInfoUpdate.do", subObj, function(subData){
						 console.log("subObj", subObj);
						 if(subData.success){
							 alert("답글 수정이 완료되었습니다.");
							 
							 //답글 리스트 재조회
							 findReplyList();
							 
							 //열려있는 대댓글이 있었다면..
							 //대댓글 리스트 재조회
							 if(OPENED_REPLY_ID != ""){
								 findSubReplyList();
								 $("#reply_" + OPENED_REPLY_ID).fadeIn(); //대댓글 리스트 영역 열기
								 $("p[id='" + OPENED_REPLY_ID + "']").removeClass("off");
								 $("p[id='" + OPENED_REPLY_ID + "']").addClass("on");
								 $("#span_" + OPENED_REPLY_ID).html("▼");
							 }
							 $("#mod_" + id).hide();
							 $("#modNot_" + id).show();
						 }
					 }, true, "POST");
				 }
			 });

			 //수정 취소 버튼 클릭 이벤트
			 $(".btnSubReplyModCancel").off("click");
			 $(".btnSubReplyModCancel").on("click", function(){
				 var id = $(this)[0].id;
				 
				 $("#mod_" + id).hide();
				 $("#modNot_" + id).show();
				 $("#subBtnArea_" + id).show();
			 });

			 //댓글 삭제 버튼 클릭 이벤트
			 $(".btnSubDelete").off("click");
			 $(".btnSubDelete").on("click", function(){
				 var id = $(this)[0].id;
				 
				 var subObj = {
					 codeInfo: id,
				 };
				 
				 var result = confirm("삭제하시겠습니까?");
				 if(result){
					 cfSave("/qna/saveReplyInfoRemove.do", subObj, function(subData){
						 console.log("subObj", subObj);
						 if(subData.success){
							 alert("답글 삭제가 완료되었습니다.");
							 
							 //답글 리스트 재조회
							 findReplyList();
							 
							 //열려있는 대댓글이 있었다면..
							 //대댓글 리스트 재조회
							 if(OPENED_REPLY_ID != ""){
								 findSubReplyList();
								 $("#reply_" + OPENED_REPLY_ID).fadeIn(); //대댓글 리스트 영역 열기
								 $("p[id='" + OPENED_REPLY_ID + "']").removeClass("off");
								 $("p[id='" + OPENED_REPLY_ID + "']").addClass("on");
								 $("#span_" + OPENED_REPLY_ID).html("▼");
							 }
						
						 }
					 }, true, "POST");
				 }
			 });
		 } //end
		 
		
		//문의글등록시 사용될 파라미터 정보를 세팅
		function setParam(){
			
			 var txtTitle = $("#txtTitle").val();
			 //var txtUserEmail = $("#txtUserEmail").val();
			// var txtUserName = $("#txtUserName").val();
			 var code = $("#txtDesc").val();
			 var password = $("#txtPassword").val();
			 var cmbPrivate = $("#cmbPrivate option:selected")[0].id; //N or Y
			 //alert(cmbPrivate);
			 if(txtTitle == ""){
				 alert("제목을 입력하시기 바랍니다.");
				 return;
			 }
			/* if(txtUserEmail == ""){
				 alert("이메일을 입력하시기 바랍니다.");
				 return;
			 }
			 if(txtUserName == ""){
				 alert("성명을 입력하시기 바랍니다.");
				 return;
			 }*/
			 if(code == "" || code == null){
				 alert("문의내용을 입력하시기 바랍니다.");
				 return;
			 }
			 if(cmbPrivate == "Y" && password ==""){
				 alert("비밀번호를 입력하시기 바랍니다.");
				 return;
			 }
			 
			 var obj = {
				 qnaTitle: txtTitle,
				 userEmail: userEmail,
				 userName: userName,
				 qnaDesc: code,
				 password: password,
				 userId: userId,
				 privateYn: cmbPrivate,
				 codeInfo: "",
				 upCodeInfo: "",
				 topCodeInfo: "",
				 lvl: 1
			 };
			return obj;
		}
		
		
		function _finalize() {
		}

		
		return {
			init: _init,
			finalize: _finalize
		} 
	 }
	
	 
	 
	 var qna = new Qna();
	 qna.init();
	 
 })();

 

 

[MainController.java]  -  src/main/java/kr.co.values/main/web/MainController.java

- 파일다운로드 부분 추가

package kr.co.values.main.web;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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.values.main.persistence.MainMapper;

@Controller
public class MainController {
	@Autowired
	private MainMapper mainMapper;
	
	@RequestMapping("/")
	public String Main() {
		return "index";
	}
	
	//프로필 조회
	@RequestMapping("/main/searchProfile.do")
	@ResponseBody
	public List<Map<String, Object>> searchProfile(@RequestBody Map<String, Object> params){
		
		List<Map<String, Object>> list = mainMapper.searchProfile(params);
		return list;
	}
	
	/**
	 * 파일 다운로드
	 * @throws IOException 
	 */
	@RequestMapping("/downloadFiles")
	@ResponseBody
	public void downloadFiles(HttpServletRequest request, HttpServletResponse response) throws IOException {
		ServletContext context = request.getSession().getServletContext();
		String path = context.getRealPath("/resources/upload"); //파일업로드 경로
		
		String fileName = request.getParameter("fileName");
		System.out.println("fileName : " + fileName);
		
		String fileOrgName = request.getParameter("fileOrgName");
		System.out.println("fileOrgName : " + fileOrgName);
		
		String fileDir = request.getParameter("fileDir");
		System.out.println("fileDir : " + fileDir);
		
		ServletOutputStream servletOutputStream = null;
		OutputStream outs = null;
		
		try{
			String filename = fileName;
			
			response.setContentType("application/octet-stream");
			response.setHeader("Content-Disposition", "inline; filename=\"" + java.net.URLEncoder.encode(fileOrgName, "UTF-8").replaceAll("\\+", "\\ ") + "\";");
			
			servletOutputStream = response.getOutputStream();
			outs = response.getOutputStream();
			downloadFile(outs, path, filename);
			
		}
		catch(Exception e){
			response.setContentType("text/html");
			response.getOutputStream().write(0);	
		}
		finally {
			try {
				servletOutputStream.flush();
				servletOutputStream.close();
			}catch(Exception e) {
			}
		}
    }
	
	/**
	 * 파일 다운로드 메서드 호출
	 */
	private void downloadFile(OutputStream servletOutputStream, String path, String filename) throws Exception {
		
		FileInputStream fileInputStream = null; 
				
		try {
			
			System.out.println("path + \"/\" + filename : " + path + "/" + filename);
			
			File file = new File(path + "/" + filename);
			fileInputStream = new FileInputStream(file);
			
			byte[] b = new byte[2048];
			int data = 0;
			
			while ((data=(fileInputStream.read(b, 0, b.length))) != -1) {
				servletOutputStream.write(b, 0, data);
			}
		}
		catch(Exception e) {
			throw e;
		}
		finally {
			if(fileInputStream != null){
				fileInputStream.close();
			}
		}
	}
}

[QnaController.java]  -  src/main/java/kr.co.values/qna/web/QnaController.java

package kr.co.values.qna.web;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.fileupload.FileUploadException;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import kr.co.values.qna.persistence.QnaMapper;
import kr.co.values.qna.service.QnaService;

@Controller
public class QnaController {
	
	@Autowired
	private QnaService qnaService;
	
	@Autowired
	private QnaMapper qnaMapper;
	
	@RequestMapping("/qna.do")
	public String qna() {
		return "qna";
	}
	
	//신규 코드정보 조회 // TBL_QNA_INFO테이블의 코드아이디(PK)에 넣을 데이터 조회
	@RequestMapping("/qna/findCodeInfo.do")
	@ResponseBody
	public Map<String, Object> findCodeInfo(@RequestBody Map<String, Object> param ){
		String code = qnaMapper.findCodeInfo(param);
		System.out.println("code" + code);
		
		Map<String, Object> result = new HashMap<String, Object>();
		result.put("code", code);
		return result;
	}
	

	//신규 문의글 등록
	@RequestMapping("/qna/saveBoardInfo.do")
	@ResponseBody
	public Map<String, Object> saveBoardInfo(@RequestBody Map<String, Object> param){
		Map<String, Object> result = new HashMap<String, Object>();
		qnaService.saveBoardInfo(param);
		
		result.put("success", true);
		return result;
	}
	
	//파일 업로드
	@RequestMapping("/qna/fileUpload.do")
	@ResponseBody
	public Map<String, Object> fileUploadSubmit(@RequestParam("file") MultipartFile part, HttpServletRequest request) 
	throws FileUploadException {
		System.out.println("컨트롤러 유무 테스트");
		ServletContext context = request.getSession().getServletContext();
		String path = context.getRealPath("/resources/upload"); //파일업로드 경로
		System.out.println("path: " + path);
		
		String fileName = part.getOriginalFilename();
		System.out.println("fileName : " + fileName);
		
		String codeInfo = request.getParameter("codeInfo");
		String userId = request.getParameter("userId");
		System.out.println("codeInfo : " + codeInfo);
		System.out.println("userId : " + userId);
		
		SimpleDateFormat fm = new SimpleDateFormat("yyyyMMddHHmmssSS");
		//SS를 쓴경우 한번에 파일여러개 올릴경우 중복되지 않는다.
		Date time = new Date();
		String forTime = fm.format(time);
		
		int pos = fileName.lastIndexOf(".");
		String ext = fileName.substring(pos + 1); //png, jpg등 확장자
		
		try {
			File f = new File(path + "/" + forTime + "." + ext);
			part.transferTo(f);
			
		} catch (Exception e) {
			throw new FileUploadException(); //파일 업로드 오류
		}
		
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("codeInfo", codeInfo);
		map.put("userId", userId);
		map.put("fileOrgName", fileName);
		map.put("fileName", forTime + "." + ext);
		qnaMapper.saveBoardFile(map);
		
		Map<String, Object> result = new HashMap<String, Object>();
		result.put("success", true);
		return result;
	}
	
	//등록된 문의글 리스트 조회
	@RequestMapping("/qna/findBoardList.do")
	@ResponseBody
	public List<Map<String, Object>> findBoardList(@RequestBody Map<String, Object> param){
		List<Map<String, Object>> list = qnaMapper.findBoardList(param);
		
		return list;
	}
	
	//등록된 첨부파일 조회
	@RequestMapping("/qna/findBoardFileList.do")
	@ResponseBody
	public List<Map<String, Object>> findBoardFileList(@RequestBody Map<String, Object> param){
		System.out.println(param);
		List<Map<String, Object>> list = qnaMapper.findBoardFileList(param);
		System.out.println("등록된첨부파일 조회 LIST : " + list);
		return list;
	}
	
	//문의글 조회 카운트 증가 
	@RequestMapping("/qna/saveBoardCnt.do")
	@ResponseBody
	public Map<String, Object> saveBoardCnt(@RequestBody Map<String, Object> param){
		qnaService.saveBoardCnt(param);
		
		Map<String, Object> result = new HashMap<String, Object>();
		result.put("success", true);
		return result;
	}
	
	//등록된 문의글 삭제
	@RequestMapping("/qna/saveBoardInfoRemove.do")
	@ResponseBody
	public Map<String, Object> saveBoardInfoRemove(@RequestBody Map<String,Object> param, HttpServletRequest request){
		Map<String, Object> result = new HashMap<String, Object>();
		qnaService.saveBoardInfoRemove(param, request);
		
		result.put("success", true);
		return result;
	}
	
	//답글쓰기
	@RequestMapping("/qna/saveReplyInfo.do")
	@ResponseBody
	public Map<String, Object> saveReplyInfo(@RequestBody Map<String,Object> param){
		Map<String, Object> result = new HashMap<String, Object>();
		qnaService.saveReplyInfo(param);
		
		result.put("success", true);
		return result;
	}
	
	//등록된 답글 리스트 조회
	@RequestMapping("/qna/findReplyList.do")
	@ResponseBody
	public List<Map<String, Object>> findReplyList(@RequestBody Map<String, Object> param){
		List<Map<String, Object>> list = qnaMapper.findReplyList(param);
		return list;
	}
	
	//등록된 답글 수정 
	@RequestMapping("/qna/saveReplyInfoUpdate.do")
	@ResponseBody
	public Map<String, Object> saveReplyInfoUpdate(@RequestBody Map<String, Object> param){
		System.out.println("param" + param);
		Map<String, Object> result = new HashMap<String, Object>();
		qnaService.saveReplyInfoUpdate(param);
		
		result.put("success", true);
		return result;
	}
	
	//등록된 답글 삭제 
	@RequestMapping("/qna/saveReplyInfoRemove.do")
	@ResponseBody
	public Map<String, Object> saveReplyInfoRemove(@RequestBody Map<String, Object> param){
		System.out.println("param" + param);
		Map<String, Object> result = new HashMap<String, Object>();
		qnaService.saveReplyInfoRemove(param);
		
		result.put("success", true);
		return result;
	}
	
}

[QnaService.java]  -  src/main/java/kr.co.values/qna/service/QnaService.java

package kr.co.values.qna.service;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

public interface QnaService {
	//신규 문의글 등록
	void saveBoardInfo(Map<String, Object> map);
	
	//문의글 조회 카운트 증가
	void saveBoardCnt(Map<String, Object> map);
	
	//등록된 문의글 정보 삭제 ( TBL_QNA_INFO , TBL_QNA_FILE)
	void saveBoardInfoRemove(Map<String, Object> map, HttpServletRequest request);
	
	//답글 및 대댓글 쓰기
	void saveReplyInfo(Map<String, Object> map);
	
	//답글 및 대댓글 수정
	void saveReplyInfoUpdate(Map<String, Object> map);
	
	//답글 및 대댓글 삭제
	void saveReplyInfoRemove(Map<String, Object> map);

}

[QnaServiceImpl.java]  -  src/main/java/kr.co.values/qna/service/QnaServiceImpl.java

package kr.co.values.qna.service;

import java.io.File;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

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

import kr.co.values.qna.persistence.QnaMapper;

@Service
public class QnaServiceImpl implements QnaService {

	@Autowired
	private QnaMapper qnaMapper;

	@Override
	public void saveBoardInfo(Map<String, Object> map) {
		qnaMapper.saveBoardInfo(map);
	}

	@Override
	public void saveBoardCnt(Map<String, Object> map) {
		qnaMapper.saveBoardCnt(map);
	}

	@Override
	public void saveBoardInfoRemove(Map<String, Object> map, HttpServletRequest request) {
		qnaMapper.saveBoardInfoRemove(map);	
		
		//첨부한 파일이 있는지 조회 후 있으면 파일도 삭제한다.
		List<Map<String, Object>> files = qnaMapper.findBoardFileList(map);
		for(Map<String, Object> fileMap : files) {
			//실제 파일 삭제
			ServletContext context = request.getSession().getServletContext();
			String path = context.getRealPath("/resources/upload");  //업로드 경로
			System.out.println("path: " + path);
			String fileName = (String)fileMap.get("FILE_NAME");
			System.out.println("fileNAme: " + fileName);
			
			File file = new File(path + "/" + fileName);
			
			if(file.exists()) {
				if(file.delete()) {
					qnaMapper.saveBoardFileRemove(fileMap); //파일 테이블도 삭제
					System.out.println("파일삭제 성공");
				} else {
					System.out.println("파일삭제 실패");
				}
			}
		}
	}

	@Override
	public void saveReplyInfo(Map<String, Object> map) {
		qnaMapper.saveReplyInfo(map);		
	}

	@Override
	public void saveReplyInfoUpdate(Map<String, Object> map) {
		qnaMapper.saveReplyInfoUpdate(map);
	}

	@Override
	public void saveReplyInfoRemove(Map<String, Object> map) {
		qnaMapper.saveReplyInfoRemove(map);
	}
}

[QnaMapper.java]  -  src/main/java/kr.co.values/qna/persistence/QnaMapper.java

package kr.co.values.qna.persistence;

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

public interface QnaMapper {
	//신규 코드 정보 조회 // TBL_QNA_INFO테이블의 코드아이디(PK)에 넣을 데이터 조회
	String findCodeInfo(Map<String, Object> param);
	
	//문의글 정보 등록
	void saveBoardInfo(Map<String, Object> params);
	
	//문의글 첨부파일 등록
	void saveBoardFile(Map<String, Object> params);
	
	//등록된 문의글 리스트 조회
	List<Map<String, Object>> findBoardList(Map<String, Object> params);
	
	//문의글 조회 카운트 증가
	void saveBoardCnt(Map<String, Object> params);
	
	//등록된 첨부파일 리스트 조회 
	List<Map<String, Object>> findBoardFileList(Map<String, Object> params);
	
	//문의글 정보 삭제(TBL_QNA_INFO, TBL_QNA_FILE)
	void saveBoardInfoRemove(Map<String, Object> params);
	void saveBoardFileRemove(Map<String, Object> params);
	
	//답글쓰기
	void saveReplyInfo(Map<String, Object> params);
	
	//답글 리스트 조회
	List<Map<String, Object>> findReplyList(Map<String, Object> params);
	
	//답글 수정
	void saveReplyInfoUpdate(Map<String, Object> params);
	
	//답글 삭제
	void saveReplyInfoRemove(Map<String, Object> params);
}

[QnaMapper.xml]  -  src/main/java/kr.co.values/qna/persistence/QnaMapper.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.values.qna.persistence.QnaMapper"> 

	<select id="findCodeInfo" resultType="string" parameterType="hashmap">
		SELECT UUID() AS CODE_INFO
	</select>
	
	<insert id="saveBoardInfo" parameterType="hashmap">
		INSERT INTO MYPORTFOLIO.TBL_QNA_INFO (
			CODE_ID
			, UP_CODE_ID
			, TOP_CODE_ID
			, LVL
			, USER_ID
			, USER_EMAIL
			, USER_NAME
			, QNA_TITLE
			, QNA_DESC
			, TARGET_USER_ID
			, PRIVATE_YN
			, PASSWORD
			, CNT
			, DELETE_YN
			, INPUT_DATETIME
		) VALUES (
			#{codeInfo}
			,#{upCodeId}
			,#{topCodeId}
			,#{lvl}
			,#{userId}
			,#{userEmail}
			,#{userName}
			,#{qnaTitle}
			,#{qnaDesc}
			,''
			,#{privateYn}
			,#{password}
			,0
			,'N'
			, NOW()
		)
	</insert>
	
	<insert id="saveBoardFile" parameterType="hashmap">
		INSERT INTO MYPORTFOLIO.TBL_QNA_FILE (
			CODE_ID
			,FILE_ID
			,USER_ID
			,FILE_ORG_NAME
			,FILE_NAME
			,INPUT_DATETIME
		) VALUES(
			#{codeInfo}
			, UUID()
			, #{userId}
			, #{fileOrgName}
			, #{fileName}
			, NOW()
		)
	</insert>
	
	<select id="findBoardList" parameterType="hashmap" resultType="hashmap">
		SELECT DISTINCT A.CODE_ID
			,A.UP_CODE_ID
			,A.TOP_CODE_ID
			,A.LVL
			,A.USER_ID
			,A.USER_EMAIL
			,A.USER_NAME
			,A.QNA_TITLE
			,A.QNA_DESC
			,A.TARGET_USER_ID
			,A.PRIVATE_YN
			,A.PASSWORD
			,A.CNT
			,A.DELETE_YN
			, DATE_FORMAT(A.INPUT_DATETIME, '%Y-%m-%d %T') AS INPUT_DATETIME
			, CASE WHEN B.CODE_ID IS NULL THEN '답변대기중' ELSE '답변완료' END REPLY_YN
		FROM MYPORTFOLIO.TBL_QNA_INFO A 
			LEFT OUTER JOIN MYPORTFOLIO.TBL_QNA_INFO B
		ON A.CODE_ID = B.UP_CODE_ID
			AND B.LVL = 2
			AND B.USER_ID IN (SELECT USER_ID FROM TBL_USER_INFO WHERE USER_AUTH = '2')
		WHERE 1=1
			AND A.LVL = 1
			<if test="!''.equals(txtBoardTitle)">
			AND A.QNA_TITLE LIKE CONCAT('%', #{txtBoardTitle}, '%')
			</if>
			<if test="!''.equals(txtBoardWriter)">
				AND A.USER_NAME LIKE CONCAT('%', #{txtBoardWriter}, '%')
			</if>
		ORDER BY A.INPUT_DATETIME DESC
	</select>
	
	<select id="findBoardFileList" parameterType="hashmap" resultType="hashmap">
		SELECT A.CODE_ID
			,A.FILE_ID
			,A.USER_ID
			,A.FILE_ORG_NAME
			,A.FILE_NAME
			, DATE_FORMAT(A.INPUT_DATETIME, '%Y-%m-%d %T') AS INPUT_DATETIME
		FROM
		MYPORTFOLIO.TBL_QNA_FILE A
		WHERE 1=1
			AND A.CODE_ID = #{codeInfo}
		ORDER BY A.INPUT_DATETIME DESC
	</select>
	
	<update id="saveBoardCnt" parameterType="hashmap">
		UPDATE MYPORTFOLIO.TBL_QNA_INFO
		SET CNT = CNT + 1
		WHERE 1=1
			AND CODE_ID = #{codeInfo}
	</update>
	
	<delete id="saveBoardInfoRemove" parameterType="hashmap">
		DELETE FROM MYPORTFOLIO.TBL_QNA_INFO
		WHERE 1=1
		AND CODE_ID = #{codeInfo} OR TOP_CODE_ID = #{codeInfo}
	</delete>
	
	<delete id="saveBoardFileRemove" parameterType="hashmap">
		DELETE FROM MYPORTFOLIO.TBL_QNA_FILE
		WHERE 1=1
		AND CODE_ID = #{CODE_ID} AND FILE_ID = #{FILE_ID}
	</delete>
	
	<insert id="saveReplyInfo" parameterType="hashmap">
		INSERT INTO MYPORTFOLIO.TBL_QNA_INFO (
			CODE_ID
			, UP_CODE_ID
			, TOP_CODE_ID
			, LVL
			, USER_ID
			, USER_EMAIL
			, USER_NAME
			, QNA_TITLE
			, QNA_DESC
			, TARGET_USER_ID
			, PRIVATE_YN
			, PASSWORD
			, CNT
			, DELETE_YN
			, INPUT_DATETIME
		) VALUES (
			UUID()
			, #{upCodeId}
			, #{topCodeId}
			, (SELECT * FROM (SELECT LVL + 1 FROM MYPORTFOLIO.TBL_QNA_INFO WHERE CODE_ID = #{upCodeId} LIMIT 1)T)
			, #{userId}
			, #{userEmail}
			, #{userName}
			, #{qnaTitle}
			, #{qnaDesc}
			, #{targetUserId}
			, #{privateYn}
			, #{password}
			, 0
			, 'N'
			, NOW()
		)
	</insert>
	
	<select id="findReplyList" parameterType="hashmap" resultType="hashmap">
		SELECT A.CODE_ID
			,A.UP_CODE_ID
			,A.TOP_CODE_ID
			,A.LVL
			,A.USER_ID
			,A.USER_EMAIL
			,A.USER_NAME
			,A.QNA_TITLE
			,A.QNA_DESC
			,A.TARGET_USER_ID
			,A.PRIVATE_YN
			,A.PASSWORD
			,A.CNT
			,A.DELETE_YN
			, DATE_FORMAT(A.INPUT_DATETIME, '%Y-%m-%d %T') AS INPUT_DATETIME
			, (SELECT COUNT(*) FROM MYPORTFOLIO.TBL_QNA_INFO B WHERE B.UP_CODE_ID = A.CODE_ID) AS REPLY_CNT
		FROM MYPORTFOLIO.TBL_QNA_INFO A
		WHERE 1=1
			AND A.UP_CODE_ID = #{upCodeId}
		ORDER BY A.INPUT_DATETIME DESC
	</select>
	
	<update id="saveReplyInfoUpdate" parameterType="hashmap">
		UPDATE MYPORTFOLIO.TBL_QNA_INFO
		SET QNA_DESC = #{desc}
		WHERE 1=1
		AND CODE_ID = #{codeInfo}
	</update>
	
	<delete id="saveReplyInfoRemove" parameterType="hashmap">
		DELETE FROM MYPORTFOLIO.TBL_QNA_INFO
		WHERE CODE_ID = #{codeInfo} OR UP_CODE_ID = #{codeInfo}
	</delete>
	
</mapper>