I T H

[포트폴리오 프로젝트 8] 포트폴리오 프로젝트 구현 – 관리자 페이지 본문

Spring MyPortfolio Project

[포트폴리오 프로젝트 8] 포트폴리오 프로젝트 구현 – 관리자 페이지

thdev 2024. 1. 24. 10:51
이번 챕터에서는 관리자 계정으로 로그인 후 출력되는 관리자 메뉴를 통해서
프로필 관리, 포트폴리오 관리를 진행하고자 한다.
먼저 이미 작성하였던 프로필 관리 페이지에도 템플릿 스타일을 동일하게 적용하기 위해서 JSP, js 파일을 각각 수정하여 준다.
아래와 같이 프로필 관리 페이지가 완성될 것이다.

 

[ admin.jsp ] – 프로필 관리 페이지 UI 수정

src\main\webapp\WEB-INF\views\admin.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="kr">
<%@include file="/resources/inc/header.jsp"%>

<style>
.row {
	justify-content: center; 
	display: flex;
	padding: 5px;
}
</style>

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

<main id="main">

	<div class="container">
		
		<div class="section featured topspace">
			<h2 class="section-title"><span>프로필 관리 페이지</span></h2>
			
			<div class="row">
				<div class="col-md-6">
                	<small>프로필 이미지</small>
                    <input id="file" name="file" type="file" class="form-control">
                </div>
			</div>
			
			<div class="row">
				<div class="col-md-6">
                	<small>프로필 이름</small>
                    <input type="text" class="form-control" id="profileName">
                </div>
			</div>
			
			<div class="row">
				<div class="col-md-6">
                	<small>프로필 직업</small>
                    <input type="text" class="form-control" id="profileJob">
                </div>
            </div>
            
            <div class="row">
                <div class="col-md-6">
                	<small>프로필 이메일</small>
                    <input type="text" class="form-control" id="profileEmail">
                </div>
            </div>
            
            <div class="row">
                <!-- 버튼 -->
                <div class="col-md-6" style="text-align: right;">
                	<a id="btnRegister" class="btn btn-warning">프로필 등록</a>
                </div>
			</div>
			
		</div> <!-- / section -->
	
	</div>	<!-- /container -->

</main>


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

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

</body>
</html>

[ admin.js ] – 프로필 관리 페이지 스크립트 작성

src\main\webapp\resources\views\admin.js

/*******************************************************************************
 * admin.js
 * @author thevalue
 * @since 2023
 * @DESC 관리자 - 프로필 관리 스크립트
 ******************************************************************************/
(function() {

	function Admin() {

		/* 
		 * private variables
		 */

		/* 
		 * 초기화 메소드
		 */
		function _init() {
			// 이벤트 처리 함수 호출 
			bindEvent();

		}

		function bindEvent() {
			// 1. IMAGE UPLOAD
			$("#btnRegister").on("click", function() {
				
				var profileName = $("#profileName").val();
				var profileJob = $("#profileJob").val();
				var profileEmail = $("#profileEmail").val();
				
				if(profileName == "") {
					alert("프로필 이름을 입력하시기 바랍니다.");
					return;
				}
				
				var obj = {
						profileName: profileName,
						profileJob: profileJob,
						profileEmail: profileEmail,
						profileImg: ""
				}
				
				// 프로필 저장 시에는 아래와 같은 순서로 처리한다.
				// 1. 프로필 이미지를 포함하여 등록 버튼을 누른 경우에는 이미지를 먼저 업로드하고 (업로트 컨트롤러 호출) 저장된 이미지명을 전달받은 후 
				// 전달받은 이미지명을 파라미터에 포함한 뒤 
				// 2. 프로필 정보를 저장한다. 
				// 서버사이드에서는 프로필 정보가 이미 저장되어 있는지 체크 후 insert 혹은 update로 프로플 정보를 저장하게 된다.
				
				var fileImg = $('#file').val();
				
				if(fileImg == "" || fileImg == null) { // 이미지가 포함되지 않은 경우 
					$.ajax({
				        type: "POST",
				        url: "/admin/updateProfile.do",
				        contentType: "application/json;charset=UTF-8",
				        data: JSON.stringify(obj),
				        dataType: "json",
				        success: function(data) {
				        	alert("프로필 저장 성공!");
				        },
				        error: function() {
				        	alert("프로필 저장 실패!!");
				        }
			      	});
				} else {
					var ext = $('#file').val().split('.').pop().toLowerCase();
				    if($.inArray(ext, ['gif','png','jpg','jpeg']) == -1) {
				    	alert('gif, png, jpg, jpeg 파일만 업로드 할수 있습니다.');
						return false;
				    }
				    
					var formData = new FormData();
					formData.append("file", $("#file")[0].files[0]);	//파일 한개
					formData.append("params", JSON.stringify(obj));
					formData.append("enctype", "multipart/form-data");
					
					$.ajax({
						url: "/admin/profileUpload.do",
						type: "POST",
						processData: false,  // file전송시 필수
						contentType: false,  // file전송시 필수
						data: formData,
						dataType: "json",
						success: function(data) {
							// 이미지 등록이 완료되면 서버 사이드에서 전달해주는 src 키에 매핑된 파일명 값을 obj 파라미터에 담은 후 
							// 프로필 정보를 저장하는 컨트롤러를 호출한다.
							obj.profileImg = data.src;
							
							$.ajax({
						        type: "POST",
						        url: "/admin/updateProfile.do",
						        contentType: "application/json;charset=UTF-8",
						        data: JSON.stringify(obj),
						        dataType: "json",
						        success: function(data) {
						        	alert("프로필 저장 성공!");
						        },
						        error: function() {
						        	alert("프로필 저장 실패!!");
						        }
					      	});
							
						}, 
						error : function(request){
							console.log(request);
						}
					});
				}
				
			});
		}
		

		function _finalize() {
		}

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

	var admin = new Admin();
	admin.init();

})();

//# sourceURL=admin.js

[ 테이블 생성 ]

다음으로는 포트폴리오 관리 페이지를 구현한다.
해당 페이지에서는 아래와 같은 기능이 포함된다.
- 자바스크립트 기반의 텍스트 에디터 사용
- 포트폴리오 썸네일 이미지 업로드
- 탭(tab) 을 이용하여 페이지 구분 (리스트 조회, 신규 등록)
- 2개의 테이블을 신규로 생성하여 준다.
1) 포트폴리오 정보를 담고 있는 테이블
2) 포트폴리오별 카테고리 (java, javascript, jQuery 등의 키워드 정보를 담을 것임) 정보를 담고 있는 테이블
CREATE TABLE `TBL_PORTFOLIO_INFO` (
  `PORTFOLIO_ID` varchar(36) NOT NULL COMMENT '포트폴리오 아이디',
  `PORTFOLIO_TITLE` varchar(100) NOT NULL COMMENT '포트폴리오 제목',
  `PORTFOLIO_DESC` text NOT NULL COMMENT '포트폴리오 내용',
  `PORTFOLIO_IMG` varchar(100) DEFAULT NULL COMMENT '썸네일 이미지',
  `USE_YN` char(1) DEFAULT NULL COMMENT '사용여부',
  `INPUT_DATETIME` datetime DEFAULT NULL,
  PRIMARY KEY (`PORTFOLIO_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
CREATE TABLE MYPORTFOLIO.`TBL_PORTFOLIO_CATEGORY` (
  `PORTFOLIO_ID` varchar(36) NOT NULL COMMENT '포트폴리오 아이디',
  `PORTFOLIO_CATEGORY_ID` char(2) NOT NULL COMMENT '카테고리 아이디',
  `PORTFOLIO_CATEGORY_NAME` text NOT NULL COMMENT '카테고리명',
  PRIMARY KEY (`PORTFOLIO_ID`, `PORTFOLIO_CATEGORY_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

[ 참고 ]

[  사용하기 ]  –  부트스트랩

- https://bootstrapdocs.com/v3.0.0/docs/components/#nav

 

Components · Bootstrap 3.0.0 Documentation - BootstrapDocs

Some default panel content here. Nulla vitae elit libero, a pharetra augue. Aenean lacinia bibendum nulla sed consectetur. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Nullam id dolor id nibh ultricies vehicula ut id elit.

bootstrapdocs.com

- https://bootstrapdocs.com/v3.0.0/docs/javascript/#tabs

 

JavaScript · Bootstrap 3.0.0 Documentation - BootstrapDocs

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater

bootstrapdocs.com

 

현재 사용중인 템플릿 UI 부트스트랩 3.0.0 버전을 사용 중에 있다.

(최신은 5.x.x 버전까지 있는 것으로 알고 있음)

부트스트랩 버전에 따라서 사용하는 문법이 다르므로 해당 버전에 맞는 문법을 참고하여

탭을 그리고자 한다.

  그리기 샘플

1.  상단의 메뉴를 그려준다. 아래와 같은 그림처럼 배치가 되도록 하는 코드이다.

<!-- nav (tab) 사용 
			현재 템플릿에 적용된 부트스트랩 버전은 3.0.0 버전이므로 최신버전과는 문법이 살짝 다름 -->
			<ul class="nav nav-tabs" id="portfolitTab">
				<li class="active"><a href="#portList">포트폴리오 리스트</a></li>
			    <li><a href="#portNew">신규 등록</a></li>
			</ul>

 

2.  영역을 그려준다. 아래와 같이  메뉴바 아래에 표시되는 영역이다.

 

 

<!-- 탭 내용 영역 -->
			<div class="tab-content">
				<!-- 첫번째 탭  -->
			    <div class="tab-pane active" id="portList">
			    내용 들어갈 자리
			    </div>
				<!-- 두번째 탭  -->
			    <div class="tab-pane" id="portNew">
			    내용 들어갈 자리
			    </div>
			</div>

 

3. 탭 클릭  이벤트를 처리하여 준다.

// 탭 클릭 이벤트 
			$('#portfolitTab a').click(function (e) {
				e.preventDefault();
			    $(this).tab('show');
			});

[ 에디터 편집기 라이브러리 사용 ]

https://summernote.org/

 

Summernote - Super Simple WYSIWYG editor

Super Simple WYSIWYG Editor on Bootstrap Summernote is a JavaScript library that helps you create WYSIWYG editors online.

summernote.org

포트폴리오 등록  사용할 텍스트 에디터로 summernote 라이브러리를 사용한다.

무료 라이브러리로 기본적인 bold 처리, 글자 크기 등의 지원이 가능하여 가져와서 사용해보고자 한다.

 

 

 

incJs.jsp  header.jsp 파일에 각각 라이브러리를 import하여 준다. 상세 사용법은 전체코드 확인

먼저 썸네일 이미지 (포트폴리오 썸네일 이미지)  저장하기 위한 폴더를 아래와 같이 생성하여 준다. (images/portfolio)

 


-  포트폴리오 관리 페이지 구현하기

[ JSP 구현 ]

src\main\webapp\WEB-INF\views\portfolio.jsp

탭으로 구성되어 2개의 화면을 번갈아가면서 사용할  있도록 처리한다.

첫번째 탭에는 등록된 포트폴리오 리스트를 확인하고 삭제하는 기능이 포함되며,

두번째 탭에는 신규로 포트폴리오를 등록하는 기능이 포함된다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="kr">
<%@include file="/resources/inc/header.jsp"%>

<style>
/* 탭 내용 영역 패딩 사이즈 조절 */
.tab-pane {
	padding: 15px;
}
/* 신규 등록 폼 라벨 상단에 margin 값 주어서 간격 띄우기 */
.row > div > label {
	margin-top: 10px;
}
</style>
<body class="home">
<%@include file="/resources/inc/top.jsp"%>

<main id="main">

	<div class="container">
	
		<div class="section featured topspace">
			<h2 class="section-title"><span>포트폴리오 관리 페이지</span></h2>
			
			<!-- nav (tab) 사용 
			현재 템플릿에 적용된 부트스트랩 버전은 3.0.0 버전이므로 최신버전과는 문법이 살짝 다름 -->
			<ul class="nav nav-tabs" id="portfolitTab">
				<li class="active"><a href="#portList">포트폴리오 리스트</a></li>
			    <li><a href="#portNew">신규 등록</a></li>
			</ul>
			
			<!-- 탭 내용 영역 -->
			<div class="tab-content">
				<!-- 첫번째 탭  -->
			    <div class="tab-pane active" id="portList">
			    	<div class="row">
						<div class="col-md-2">
		                	<small>제목</small>
		                    <input type="text" class="form-control" id="txtTitleCond" value="" placeholder="" required>
		                </div>
		                <div class="col-md-2">
		                	<small>내용</small>
		                    <input type="text" class="form-control" id="txtDescCond" value="" placeholder="" required>
		                </div>
		                <div class="col-md-2">
		                	<small>사용여부</small>
		                	<select id="cmbUseYnCond" class="form-control">
			                 	<option id="">전체</option>
			                 	<option id="Y">Y</option>
			                 	<option id="N">N</option>
		                   	</select>
		                </div>
		                <!-- 버튼 -->
		                <div class="col-md-6" style="text-align: right;">
		                	<a id="btnSearch" class="btn btn-warning">조회</a>
		                	<a id="btnRemove" class="btn btn-danger">삭제</a>
		                </div>
					</div>
					
					<br/>
					
					<!--  그리드 사용  -->
					<!-- div 태그에 아이디를 작성하여 놓고 스크립트 파일 내에서 해당 아이디를 호출하면 그리드 정보를 세팅한다. -->
		            <div id="grid"></div>
			    </div>
			    
			    <!-- 두번째 탭 -->
			    <div class="tab-pane" id="portNew">
			    	<div class="row">
						<div class="col-md-12">
		                	<label>포트폴리오 제목</label>
		                    <input id="txtPortId" type="text" class="form-control">
		                </div>
						<div class="col-md-12">
		                	<label>내용</label>
		                    <div id="summernote"></div>
		                </div>
		                <div class="col-md-12">
		                	<label>포트폴리오 이미지</label>
		                    <input id="file" name="file" type="file" class="form-control">
		                </div>
		                <div class="col-md-12">
		                	<label>사용여부</label>
		                    <select id="cmbUseYn" class="form-control">
		                    	<option id="Y">Y</option>
		                    	<option id="N">N</option>
		                    </select>
		                </div>
		                <div class="col-md-12">
		                	<label>포트폴리오 카테고리 선택</label>
		                	<br>
		                    <label>
		                    	<input id="1" name="chkCategory" type="checkbox">&nbsp;JAVA
		                    </label>
		                    <label>
		                    	<input id="2" name="chkCategory" type="checkbox">&nbsp;JSP
		                    </label>
		                    <label>
		                    	<input id="3" name="chkCategory" type="checkbox">&nbsp;SPRING BOOT
		                    </label>
		                    <label>
		                    	<input id="4" name="chkCategory" type="checkbox">&nbsp;SPRING FRAMEWORK
		                    </label>
		                    <label>
		                    	<input id="5" name="chkCategory" type="checkbox">&nbsp;JQUERY
		                    </label>
		                    <label>
		                    	<input id="6" name="chkCategory" type="checkbox">&nbsp;CSS
		                    </label>
		                    <label>
		                    	<input id="7" name="chkCategory" type="checkbox">&nbsp;REACT
		                    </label>
		                    <label>
		                    	<input id="8" name="chkCategory" type="checkbox">&nbsp;MYSQL
		                    </label>
		                    <label>
		                    	<input id="9" name="chkCategory" type="checkbox">&nbsp;ORACLE
		                    </label>
		                    <label>
		                    	<input id="10" name="chkCategory" type="checkbox">&nbsp;MS-SQL
		                    </label>
		                </div>
		                
		                <br/>
		                <!-- 버튼 -->
		                <div class="col-md-12" style="text-align: center;">
		                	<a id="btnSavePortInfo" class="btn btn-warning">저장</a>
		                </div>
					</div>
			    </div>
			</div>
			
			
		</div> <!-- / section -->
	
	</div>	<!-- /container -->

</main>


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

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

</body>
</html>

[ Util.js 내용추가 ]

/**
 * 폼 초기화
 */
function initFormOrg(fileId) {
   // browser version 체크 후 file inputbox 초기화 진행
   var browserType = cfGetBrowser();
   
   if(browserType == "MSIE") {//IE version
        $("#" + fileId).replaceWith( $("#" + fileId).clone(true) );
   } else {// other browser
       $("#" + fileId).val("");
   }
}

[ 스크립트 구현 ]

src\main\webapp\resources\views\portfolio.js

스크립트 내에는  클릭 이벤트와 편집기 에디터를 사용할  있도록 기능이 추가되었다.

/**
 * portfolio.js
 * @author thevalue
 * @since 2023
 * @DESC 관리자 - 포트폴리오 관리 화면 스크립트
 */

(function() {

	function Portfolio() {

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

		//초기화 메서드
		function _init() {
			//이벤트 처리 함수 호출
			bindEvent();

			// Editor(에디터) 초기화
			setEditor();

			//등록된 포트폴리오 리스트 조회
			findPortfolioInfo();
		}

		function bindEvent() {

			//탭 클릭 이벤트
			$('#portfolitTab a').click(function(e) {
				e.preventDefault();
				$(this).tab('show');
			});

			//조회 버튼 클릭 이벤트
			$("#btnSearch").on("click", function() {
				findPortfolioInfo();
			});
			
			//삭제버튼 클릭 이벤트 - 체크박스에 체크한 항목의 리스트를 삭제할수 있도록 한다.
			$("#btnRemove").off("click");
			$("#btnRemove").on("click", function(){
				var checkedRow = grid.getCheckedRows();
				if(checkedRow.length <= 0){
					alert("삭제할 항목을 선택하시기 바랍니다.");
					return;
				}
				console.log(checkedRow);
				savePortInfoRemove(checkedRow);
			});
			
			//신규 등록 > 포트폴리오 저장 버튼 클릭 이벤트
			$("#btnSavePortInfo").on("click", function() {
				savePortInfo();
			});
		}
		
		//에디터 설정
		function setEditor() {
			$('#summernote').summernote({
				height: 500,
				lang: 'ko-KR',
				toolbar: [
					['fontname', ['fontname']],
					['fontsize', ['fontsize']],
					['style', ['bold', 'italic', 'underline', 'strikethrough', 'clear']],
					['color', ['forecolor', 'color']],
					['table', ['table']],
					['insert', ['link', 'picture']],
					['para', ['paragraph']],
					['height', ['height']],
          			['view', ['codeview']]
				],
				fontNames: ['sans-serif', 'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', '맑은 고딕', '궁서', '굴림체', '굴림', '돋움체', '바탕체'],
				fontSizes: ['8', '9', '10', '11', '12', '14', '16', '18', '20', '22', '24', '28', '30', '36', '50', '72']
			});

			//디폴트 폰트 이름 지정
			$('#summernote').summernote('fontName', '맑은 고딕');
		};

		//등록된 포트폴리오 리스트 조회
		function findPortfolioInfo(){
			var obj = {
				portTitle : $("#txtTitleCond").val(),
				porDesc : $("#txtDescCond").val(),
				useYn : $("#cmbUseYnCond option:selected")[0].id
			}
			
			console.log(obj);
			cfFind("/admin/findPortfolioInfo.do", obj, function(data){
				console.log(data);
				setGrid(data);
			}, true, "POST");
		}

		
		//gird setting
		function setGrid(gridData){
			console.log(gridData);
			//이미 그리드가 그려져 있으면
			if(grid != null){
				grid.destroy();//초기화하기 위해 사용
			}
			//그리드 컬럼 정보 세팅
			var columns = [{
				header: '포트폴리오 아이디',
				name: 'PORTFOLIO_ID',
				align: "center",
				width: 170
			}, {
				header: '제목',
				name: 'PORTFOLIO_TITLE',
				align: "center",
				width: 170
			}, {
				header: '내용',
				name: 'PORTFOLIO_DESC',
				align: "center",
				width: 170
			}, {
				header: '카테고리',
				name: 'PORTFOLIO_CATEGORY_NAME',
				align: "center",
				width: 170
			}, {
				header: '썸네일 이미지명',
				name: 'PORTFOLIO_IMG',
				align: "center",
				width: 170
			}, {
				header: '사용여부',
				name: 'USE_YN',
				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
				},
				rowHeight: "auto",
				rowHeaders: ['checkbox'] //체크박스
			});
			
			grid.resetData(gridData);//그리드 데이터 세팅
			tui.Grid.applyTheme('striped'); //줄무늬 스타일 적용
		}
		
		
		//신규 입력한 포트폴리오 정보 저장
		function savePortInfo(){
			var title = $("#txtPortId").val(); //제목
			//alert(title);
			var desc = $("#summernote").summernote('code'); //내용
			//alert(desc);
			var fileImg = $("#file").val(); //이미지
			var useYn = $("#cmbUseYn option:selected")[0].id;
			
			//모든 입력은 필수 항목으로 처리!
			if(title == null || title == ""){
				alert("포트폴리오 제목을 입력하시기 바랍니다.")
				return;
			}
			if(desc == null || desc == ""){
				alert("포트폴리오 내용을 입력하시기 바랍니다.")
				return;
			}
			if(fileImg == null || fileImg == ""){
				alert("포트폴리오 썸네일 이미지을 입력하시기 바랍니다.")
				return;
			} else {
				var ext = $("#file").val().split('.').pop().toLowerCase();
				if ($.inArray(ext, ['gif', 'png', 'jpg', 'jpeg']) == -1) {
					alert('gif, png, jpg, jpeg 파일만 업로드 할수 있습니다.');
				}
			}
			
			//체크한 카테고리 정보
			var checksId = [];
			var checksName = [];
			var names = ["JAVA", "JSP", "SPRING BOOT", "SPRING FRAMEWORK", "JQUERY", "CSS", "REACT", "MYSQL", "ORACLE", "MS-SQL"];
			
			$("input:checkbox[name='chkCategory']").each(function(){
				if(this.checked){
					console.log(this.id);
					console.log(names[Number(this.id) -1]);
					checksId.push(this.id);
					checksName.push(names[Number(this.id) -1]);
				}
			})
			
			if(checksId.length == 0){
				alert("포트폴리오 카테고리를 최소 1개 선택하시기 바랍니다.");
				return;
			}
			
			var obj = {
				title : title,
				desc: desc,
				fileImg: fileImg,
				useYn: useYn,
				checksId: checksId,
				checksName: checksName
			}
			
			var result = confirm("저장하시겠습니까?");
			if(result){
				var formData = new FormData();
				formData.append("file", $("#file")[0].files[0]); //파일한개
				formData.append("params", JSON.stringify(obj));
				formData.append("entype", "multipart/form-data");
			}
			
			$.ajax({
				url: "/admin/portfolioUpload.do",
				type: "POST",
				processData: false, //file전송시 필수
				contentType: false, //file전송시 필수
				data: formData,
				dataType: "json",
				success: function(data) {
					// 이미지 등록이 완료되면 서버 사이드에서 전달해주는 src 키에 매핑된 파일명 값을 obj 파라미터에 담은 후 
					// 프로필 정보를 저장하는 컨트롤러를 호출한다.
					obj.portfolioImg = data.src;
					$.ajax({
						url: "/admin/savePortfolio.do",
						type: "POST",
						contentType: "application/json;charset=UTF-8",
						data: JSON.stringify(obj),
						dataType: "json",
						success: function(data) {
							alert("포트폴리오 저장 성공!");
							//저장이 완료되면 신규등록 폼 초기화
							setInitForm();
						},
						error: function() {
							alert("포트폴리오 저장 실패!!")
						}
					});
				},
				error: function() {
					console.log(requet);
				}
			});
		}//포트폴리오 정보 저장 END
		
		
		//체크한 포트폴리오 정보 삭제
		function savePortInfoRemove(checkedRow) {
			console.log(checkedRow);
			var result = confirm("삭제하시겠습니까?");
			if (result) {
				cfSave("/qna/savePortInfoRemove.do", checkedRow, function(data) {
					if (data.success) {
						alert("포트폴리오 정보가 삭제되었습니다.");
						findPortfolioInfo();//포트폴리오 정보 재조회
					} else {
						alert("포트폴리오 정보 삭제 실패하였습니다. 확인 후 다시 등록하시기 바랍니다.");
					}
				}, true, "POST")
			}
		}
		
		
		//폼 초기화
		function setInitForm(){
			$("#txtPortId").val("");
			$("#summernote").summernote('code', ""); //summernote 초기화 방법
			initFormOrg("file");// util.js에 선언된 함수 호출하여 input file tag 값 초기화
			$("#cmbUseYn").val("Y");
			$("input:checkbox[name='chkCategory']").attr("checked", false);
		};



		function _finalize() {
		}


		return {
			init: _init,
			finalize: _finalize
		}
	}



	var portfolio = new Portfolio();
	portfolio.init();

})();

[ 컨트롤러 구현 ]

src\main\java\kr\co\values\portfolio\web\PortfolioController.java

이번 장에서 컨트롤러의 코드  특이 코드는 (기존에 사용해보지 않았던…) 아래와 같다.

화면 스크립트에서 배열형태로 파라미터를 전달할 경우에 컨트롤러에서는 ArrayList 객체로 전달받을  있는 것을 확인해본다.

 

 

package kr.co.values.portfolio.web;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
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.portfolio.persistence.PortfolioMapper;
import kr.co.values.portfolio.service.PortfolioService;

@Controller
public class PortfolioController {
	
	
	@Autowired
	private PortfolioService portfolioService;
	
	@Autowired
	private PortfolioMapper portfolioMapper;
	
	@RequestMapping("/admin/portfolio.do")
	public String main() {
		return "portfolio";
	}
	
	
	//등록된 포트폴리오 리스트 조회 
	@RequestMapping("/admin/findPortfolioInfo.do")
	@ResponseBody
	public List<Map<String, Object>> findPortfolioInfo(@RequestBody Map<String, Object> params){
		List<Map<String, Object>> list = portfolioMapper.findPortfolioInfo(params);
		return list;
	}
	
	
	//포트폴리오 정보 저장
	@RequestMapping("//admin/savePortfolio.do")
	@ResponseBody
	public Map<String, Object> savePortfolio(@RequestBody Map<String, Object> params){
		
		//데이터 저장 전 중복되지 않은 키를 조회하여 세팅
		String codeId = portfolioMapper.findCodeId();
		params.put("codeId", codeId);
		
		portfolioService.savePortfolio(params);
		
		ArrayList<String> checksId = (ArrayList<String>) params.get("checksId");
		ArrayList<String> checksName = (ArrayList<String>) params.get("checksName");
		
		for(int i = 0; i < checksId.size(); i++) {
			params.put("chkId", checksId.get(i));
			params.put("chkName", checksName.get(i));
			portfolioService.savePortfolioCategory(params);
		}
		Map<String, Object> result = new HashMap<String, Object>();
    	result.put("result", true);
    	
        return result;
	}

	// 업로드 (포트폴리오 이미지 파일 업로드)
	@RequestMapping("/admin/portfolioUpload.do")
	@ResponseBody
	public Map<String, Object> fileUploadSubmit(@RequestParam("file") MultipartFile part, HttpServletRequest request)
			throws FileUploadException {

		ServletContext context = request.getSession().getServletContext();
		String path = context.getRealPath("/resources/images/portfolio"); // 파일업로드 경로
		System.out.println("path: " + path);

		String fileName = part.getOriginalFilename();
		System.out.println("fileName : " + fileName);

		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> result = new HashMap<String, Object>();
		result.put("src", forTime + "." + ext);
		return result;
	}
	
	//등록된 포트폴리오 정보 삭제
	@RequestMapping("/qna/savePortInfoRemove.do")
	@ResponseBody
	public Map<String, Object> savePortInfoRemove(@RequestBody List<Map<String, Object>> params){
		portfolioService.savePortInfoRemove(params);
		Map<String, Object> result = new HashMap<String, Object>();
    	result.put("success", true);
    	
        return result;
	}

}

[ 서비스 구현 ]

src\main\java\kr\co\values\portfolio\service\PortfolioService.java

package kr.co.values.portfolio.service;

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

public interface PortfolioService {
	
	//포트폴리오 정보 저장
	void savePortfolio(Map<String, Object> map);
	
	//포트폴리오 카테고리 정보 저장
	void savePortfolioCategory(Map<String, Object> map);
	
	//등록된 포트폴리오 정보 삭제 
	void savePortInfoRemove(List<Map<String, Object>> list);

}

 

src\main\java\kr\co\values\portfolio\service\PortfolioServiceImpl.java

package kr.co.values.portfolio.service;

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

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

import kr.co.values.portfolio.persistence.PortfolioMapper;

@Service
public class PortfolioServiceImpl implements PortfolioService {
	
	@Autowired
	private PortfolioMapper portfolioMapper;

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

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

	@Override  
	public void savePortInfoRemove(List<Map<String, Object>> list) {
		for(Map<String, Object> map : list) {
			portfolioMapper.savePortInfoRemove(map); //포트폴리오 정보 삭제
			portfolioMapper.savePortCategoryRemove(map); //포트폴리오 카테고리 정보 삭제
		}
	}

}

[ 매퍼 구현 ]

src\main\java\kr\co\values\portfolio\persistence\PortfolioMapper.java

package kr.co.values.portfolio.persistence;

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

public interface PortfolioMapper {

	// 등록된 포트폴리오 리스트 조회(codeId)
	String findCodeId();

	// 포트폴리오 정보 저장
	void savePortfolio(Map<String, Object> params);

	// 포트폴리오 카테고리 정보 저장
	void savePortfolioCategory(Map<String, Object> params);

	// 등록된 포트폴리오 리스트 조회
	List<Map<String, Object>> findPortfolioInfo(Map<String, Object> params);

	// 포트폴리오 정보삭제
	void savePortInfoRemove(Map<String, Object> params);
	
	// 포트폴리오 카테고리 정보삭제
	void savePortCategoryRemove(Map<String, Object> params);
}

 

src\main\java\kr\co\values\portfolio\persistence\PortfolioMapper.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.portfolio.persistence.PortfolioMapper"> 
	<select id="findCodeId" resultType="string">
		SELECT UUID() AS CODE_ID
	</select>
	
	<insert id="savePortfolio" parameterType="hashmap">
		INSERT INTO MYPORTFOLIO.TBL_PORTFOLIO_INFO (
			PORTFOLIO_ID
			, PORTFOLIO_TITLE
			, PORTFOLIO_DESC
			, PORTFOLIO_IMG
			, USE_YN
			, INPUT_DATETIME
		) VALUES (
			#{codeId}
			, #{title}
			, #{desc}
			, #{portfolioImg}
			, #{useYn}
			, NOW()
		)
	</insert>
	
	<insert id="savePortfolioCategory" parameterType="hashmap">
		INSERT INTO MYPORTFOLIO.TBL_PORTFOLIO_CATEGORY (
			PORTFOLIO_ID
			, PORTFOLIO_CATEGORY_ID
			, PORTFOLIO_CATEGORY_NAME
		) VALUES (
			#{codeId}
			, #{chkId}
			, #{chkName}
		)
	</insert>
	
	<select id="findPortfolioInfo" resultType="hashmap" parameterType="hashmap">
		SELECT A.PORTFOLIO_ID
			, A.PORTFOLIO_TITLE
			, A.PORTFOLIO_DESC
			, A.PORTFOLIO_IMG
			, A.USE_YN
			, group_concat(B.PORTFOLIO_CATEGORY_NAME SEPARATOR ',') AS PORTFOLIO_CATEGORY_NAME
			, DATE_FORMAT(A.INPUT_DATETIME, '%Y-%m-%d %T') INPUT_DATETIME 
		FROM MYPORTFOLIO.TBL_PORTFOLIO_INFO A
			LEFT OUTER JOIN MYPORTFOLIO.TBL_PORTFOLIO_CATEGORY B
		ON A.PORTFOLIO_ID = B.PORTFOLIO_ID
		WHERE 1=1
		<if test="!''.equals(portTitle)">
			AND A.PORTFOLIO_TITLE LIKE CONCAT('%', #{portTitle}, '%')
		</if>
		<if test="!''.equals(porDesc)">
			AND A.PORTFOLIO_DESC LIKE CONCAT('%', #{porDesc}, '%')
		</if>
		<if test="!''.equals(useYn)">
			AND A.USE_YN = #{useYn}
		</if>
		GROUP BY A.PORTFOLIO_ID
		ORDER BY A.INPUT_DATETIME DESC
	</select>
	
	<delete id="savePortInfoRemove" parameterType="hashmap">
		DELETE FROM MYPORTFOLIO.TBL_PORTFOLIO_INFO
		WHERE 1=1
		AND PORTFOLIO_ID = #{PORTFOLIO_ID}
	</delete>
	
	<delete id="savePortCategoryRemove" parameterType="hashmap">
		DELETE FROM MYPORTFOLIO.TBL_PORTFOLIO_CATEGORY
		WHERE 1=1
		AND PORTFOLIO_ID = #{PORTFOLIO_ID}
	</delete>
	
</mapper>

 

- group_concat 

Group by  통해 동일한 코드 아이디를 그룹핑한 후에 특정 컬럼의 값은 ,쉼표 등의 구분자로 묶어서 한줄로 표현하기 위해서 사용한다.

-  Mysql 문법이므로 다른 DBMS에서는 사용하지 않는다.

[ 결과 테스트 ]