SpringBoot 프로젝트

[프로젝트] 11. 관리자 화면 - 게시글 관리 구현 (Week 4)

더밸류(THEVALUE) 2022. 11. 30. 09:49

공지사항 등의 게시글을 관리하는 화면으로

탭 형태로 2개의 패널이 구현됨 

1개의 탭은 게시글 리스트를 확인, 수정 관리할 수 있도록 하며

1개의 탭은 게시글을 등록할 수 있도록 함

게시글 등록 시 에디터 사용을 위한 라이브러리 추가가 필요하며 

드래그 앤 드랍 (drag & drop  : 마우스로 끌어다가 파일을 놓는) 기능이 포함된 파일 업로드 관련 라이브러리도 함께 사용 

 

[ 에디터 ] - summernote

제공하는 js 파일, css 파일을 가져와서 사용해야 함 

한글 처리를 위한 별도의 js 파일도 존재하여 

총 3개의 파일을 가져옴 (https://summernote.org/getting-started/)

summernote-lite.js
summernote-ko-KR.min.js
summernote-lite.css

summernote-lite.js
0.34MB
summernote-ko-KR.min.js
0.00MB
summernote-lite.css
0.03MB

 

summernote 내에 사용되는 폰트를 위한 파일도 첨부

아래 파일들을 프로젝트에 복사 

경로 )
src\main\resources\static\css\font

font 폴더를 생성 후 아래에 4개 파일 복사 

summernote.eot
0.01MB
summernote.ttf
0.01MB
summernote.woff
0.01MB
summernote.woff2
0.01MB

 

참고 사이트 )

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

 

[ 드래그앤드랍 파일 업로드 ] - jquery-dm-uploader 

파일 업로드 시 사용하는 라이브러리로 마우스로 파일을 끌어다가 화면에 넣게 되면 서버(백엔드)와 통신하여 파일을 즉각 업로드할 수 있으며, 옵션에 따라 특정 버튼 이벤트 발생 시 파일업로드를 진행할 수 있음

해당 프로젝트에서는 게시글 등록시 게시글에 포함된 기본 정보를 입력하고 파일을 업로드한 후 "저장" 버튼을 클릭하면 

1. 파일 업로드 진행

2. 게시글 정보 저장 

순으로 한번에 처리되도록 함 

jquery.dm-uploader.min.js
jquery.dm-uploader.min.css

jquery.dm-uploader.min.js
0.01MB
jquery.dm-uploader.min.css
0.00MB

 

참고 사이트 )

https://github.com/danielm/uploader

 

GitHub - danielm/uploader: A lightweight and very configurable jQuery plugin for file uploading using ajax(a sync); includes sup

A lightweight and very configurable jQuery plugin for file uploading using ajax(a sync); includes support for queues, progress tracking and drag and drop. - GitHub - danielm/uploader: A lightweight...

github.com

 

[ header.html ] - summernote / jquery-dm-uploader 라이브러리 관련 내용 추가 

경로 )
src\main\resources\static\common\header.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<meta charset="UTF-8">

<!-- 스타일 시트 및 스크립트 파일을 블락으로 지정하여 사용 -->
<th:block th:fragment="header">
<!-- Site Title -->
<title>::: FAST BOARD :::</title>

<!-- CSS ============================== -->
<!-- 부트스트랩 스타일 -->
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<!-- 페이지 스타일 -->
<link href="static/css/sticky-footer-navbar.css" rel="stylesheet">

<!-- jexcel -->
<link rel="stylesheet" href="static/css/jexcel.css">
<link rel="stylesheet" href="static/css/jsuites.css">

<!-- summernote editor -->
<link rel="stylesheet" href="static/css/summernote-lite.css">

<!-- dm fileupload -->
<link rel="stylesheet" href="static/css/jquery.dm-uploader.min.css">

<!-- JS ============================== -->
<!-- jquery -->
<script type="text/javascript" src="static/js/jquery.min.js"></script>

<!-- bootstrap js -->
<script type='text/javascript' src="static/js/bootstrap.bundle.min.js"></script>

<!-- summernote editor -->
<script type='text/javascript' src="static/js/summernote-lite.js"></script>
<script type='text/javascript' src="static/js/summernote-ko-KR.min.js"></script>

<!-- dm fileupload -->
<script type='text/javascript' src="static/js/jquery.dm-uploader.min.js"></script>


<!-- util js -->
<script src="static/js/util.js"></script>

<!-- jexcel -->
<!-- https://bossanova.uk/jspreadsheet/v3/docs/quick-reference -->
<script src="static/js/jexcel.js"></script>
<script src="static/js/jquery_jexcel.js"></script>
<script src="static/js/jsuites.js"></script>

</th:block>

</html>

 

 

[ 컨트롤러 구현 ] - AdminController.java

 

게시글 관리 컨트롤러에서는 아래와 같이 기능을 구현해야 함 

1. /admin 경로로 접속한 경우 adminview 페이지로 접속하도록 함

2. 데이터베이스에 저장된 게시글 정보를 조회하도록 함 (select) 

3. 게시글 정보를 신규로 저장할 수 있고 기존 게시글 정보를 업데이트 할 수 있도록 함 (insert & update) 

4. 게시글 정보를 삭제할 수 있도록 함 (delete)

5. 게시글 등록 시 다수의 파일을 업로드할 수 있도록 함 (multipart upload)

경로 )
src\main\java\com\info\fastboard\admin\controller\AdminController.java
package com.info.fastboard.admin.controller;

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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
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 org.springframework.web.servlet.ModelAndView;

import com.info.fastboard.admin.persistence.AdminMapper;
import com.info.fastboard.admin.service.AdminService;
import com.info.fastboard.main.util.SessionUtil;
import com.info.fastboard.main.util.StorageService;


@Controller
public class AdminController {
	
	@Autowired
	private StorageService storageService;
	
	@Autowired
	private AdminService adminService;
	
	@Autowired
	private AdminMapper adminMapper;
	
	/**
     * 관리자 페이지 이동
     * @return
     */
	@GetMapping("/admin")
	public ModelAndView getAdminPage() {
		int lastNo = adminMapper.findBoardLastNo();
		
		ModelAndView mv = new ModelAndView();
		mv.setViewName("admin");
		mv.addObject("boardIdx", lastNo);
		return mv;
	}
	
	/**
     * 관리자 - 게시글 리스트 조회
     * @return
     */
	@ResponseBody
	@PostMapping("/admin/findBoardList")
	public List<Map<String, Object>> findBoardList(@RequestBody Map<String, Object> map) {
		List<Map<String, Object>> list = adminMapper.findBoardList(map);
		return list;
	}
	
	/**
     * 관리자 - 게시글 리스트 삭제
     * @return
     */
	@ResponseBody
	@PostMapping("/admin/saveBoardDelete")
	public Map<String, Object> saveBoardDelete(@RequestBody List<Map<String, Object>> list) {
		adminService.saveBoardDelete(list);
		Map<String, Object> result = new HashMap<String, Object>();
		int lastNo = adminMapper.findBoardLastNo();
		result.put("boardIdx", lastNo);
		result.put("success", true);
		return result;
	}
	
	/**
     * 관리자 - 게시글 등록
     * @return
     */
	@ResponseBody
	@PostMapping("/admin/saveBoardInsert")
	public Map<String, Object> saveBoardInsert(@RequestBody Map<String, Object> map) {
		map.put("userId", SessionUtil.getSessionUserId());
		Map<String, Object> result = new HashMap<String, Object>();
		adminService.saveBoard(map);
		int lastNo = adminMapper.findBoardLastNo();
		result.put("boardIdx", lastNo);
		result.put("success", true);
		return result;
	}
	
	/**
     * 관리자 - 게시글 수정
     * @return
     */
	@ResponseBody
	@PostMapping("/admin/saveBoardUpdate")
	public Map<String, Object> saveBoardUpdate(@RequestBody List<Map<String, Object>> list) {
		Map<String, Object> result = new HashMap<String, Object>();
		adminService.saveBoardUpdate(list);
		int lastNo = adminMapper.findBoardLastNo();
		result.put("boardIdx", lastNo);
		result.put("success", true);
		return result;
	}
	
	/**
	 * 파일 업로드
	 */
	@RequestMapping("/admin/fileUpload")
	@ResponseBody
	public Map<String, Object> fileUpload(@RequestParam("file") MultipartFile part) {
		Map<String, Object> result = storageService.store(part);
		result.put("success", true);
		return result;
    }
	
	/**
     * 첨부파일 리스트 저장
     * @return
     */
	@ResponseBody
	@PostMapping("/admin/saveBoardFileInsert")
	public Map<String, Object> saveBoardFileInsert(@RequestBody Map<String, Object> map) {
		map.put("userId", SessionUtil.getSessionUserId());
		Map<String, Object> result = new HashMap<String, Object>();
		adminService.saveBoardFileInsert(map);
		result.put("success", true);
		return result;
	}
	
}

 

[ 서비스 구현 ] - AdminService.java / AdminServiceImpl.java

 

insert & update / delete 를 실행 시 트랜잭션 처리를 위해 서비스 인터페이스 및 클래스를 구현 

경로 )
src\main\java\com\info\fastboard\admin\service
package com.info.fastboard.admin.service;

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

public interface AdminService {
	// 게시글 삭제 
	void saveBoardDelete(List<Map<String, Object>> list);
	
	// 게시글 추가
	void saveBoard(Map<String, Object> map);
	
	// 게시글 수정 
	void saveBoardUpdate(List<Map<String, Object>> list);
	
	// 첨부파일 리스트 저장
	void saveBoardFileInsert(Map<String, Object> map);
}

인터페이스를 구현한 클래스도 생성

package com.info.fastboard.admin.service;

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

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

import com.info.fastboard.admin.persistence.AdminMapper;
import com.info.fastboard.main.util.StorageService;

@Service
@Transactional
public class AdminServiceImpl implements AdminService {

	@Autowired
	private StorageService storageService;
	
	@Autowired
	private AdminMapper adminMapper;
	
	@Override
	public void saveBoardDelete(List<Map<String, Object>> list) {
		for(Map<String, Object> map : list) {
			// 게시글 삭제 
			adminMapper.saveBoardDelete(map);
			
			// 첨부파일 삭제 
			List<Map<String, Object>> fileList = adminMapper.findBoardFileList(map);
			for(Map<String, Object> item : fileList) {
				storageService.deleteAsResource((String) item.get("FILE_NAME"));
			}
			
			// 첨부파일 리스트 삭제
			adminMapper.saveBoardFileDelete(map);
		}
	}
	
	@Override
	public void saveBoard(Map<String, Object> map) {
		adminMapper.saveBoardInsert(map);
	}
	
	@Override
	public void saveBoardUpdate(List<Map<String, Object>> list) {
		for(Map<String, Object> map: list) {
			adminMapper.saveBoardUpdate(map);
		}
	}
	
	@Override
	public void saveBoardFileInsert(Map<String, Object> map) {
		adminMapper.saveBoardFileInsert(map);
	}
}

 

[ 매퍼 구현 ] - AdminMapper.java

경로 )
src\main\java\com\info\fastboard\admin\persistence
package com.info.fastboard.admin.persistence;

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

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface AdminMapper {
	// 게시글 마지막 번호 조회
	int findBoardLastNo();
	
	// 게시글 리스트 조회 
	List<Map<String, Object>> findBoardList(Map<String, Object> map);
	
	// 게시글 첨부파일 리스트 조회
	List<Map<String, Object>> findBoardFileList(Map<String, Object> map);
	
	// 게시글 신규 저장
    void saveBoardInsert(Map<String, Object> map);
    
    // 게시글 수정
    void saveBoardUpdate(Map<String, Object> map);
    
    // 게시글 삭제
    void saveBoardDelete(Map<String, Object> map);
    
    // 첨부파일 리스트 삭제 
    void saveBoardFileDelete(Map<String, Object> map);
    
    // 텀부파일 리스트 저장 
    void saveBoardFileInsert(Map<String, Object> map);
}

 

[ AdminMapper.xml ]

경로 )
src\main\resources\mapper\AdminMapper.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="com.info.fastboard.admin.persistence.AdminMapper">

	<select id="findBoardLastNo" resultType="int">
		SELECT NVL(MAX(BOARD_IDX) + 1, 1) AS BOARD_IDX
   		  FROM INFO.CRT_BOARD_INFO 
	</select>
	
	<select id="findBoardList" parameterType="hashmap" resultType="hashmap">
		SELECT A.BOARD_IDX
		     , A.BOARD_TYPE
		     , A.BOARD_TITLE
		     , A.BOARD_DESC
		     , A.USE_YN
		     , TO_CHAR(A.DUE_DATE, 'yyyy-mm-dd hh24:mi:ss') AS DUE_DATE
		     , A.FIXED_YN
		     , A.INPUT_USER_ID
		     , TO_CHAR(A.INPUT_DATETIME, 'yyyy-mm-dd hh24:mi:ss') AS INPUT_DATETIME
		     , A.MODIFY_USER_ID
		     , TO_CHAR(A.MODIFY_DATETIME, 'yyyy-mm-dd hh24:mi:ss') AS MODIFY_DATETIME
		     , CASE WHEN B.CNT IS NULL THEN 'N' ELSE 'Y' END AS FILE_YN
		  FROM INFO.CRT_BOARD_INFO A
		  LEFT OUTER JOIN (
		  	SELECT BOARD_IDX, COUNT(FILE_IDX) AS CNT 
		  	  FROM INFO.CRT_BOARD_FILE
		  	 GROUP BY BOARD_IDX
		  ) B
		    ON A.BOARD_IDX = B.BOARD_IDX
		 WHERE 1=1
		 	<if test="!''.equals(searchType)">
		 	AND A.BOARD_TYPE = #{searchType}
		 	</if>
            <if test="!''.equals(searchText)">
            AND A.BOARD_TITLE LIKE '%' || #{searchText} || '%'
            </if>
         ORDER BY A.INPUT_DATETIME DESC
	</select>
	
	<select id="findBoardFileList" parameterType="hashmap" resultType="hashmap">
		SELECT FILE_NAME
		  FROM INFO.CRT_BOARD_FILE 
		 WHERE BOARD_IDX = #{boardIdx}
	</select>
	
    <insert id="saveBoardInsert" parameterType="hashmap">
    	INSERT INTO INFO.CRT_BOARD_INFO (BOARD_IDX, BOARD_TYPE, BOARD_TITLE, BOARD_DESC, USE_YN, DUE_DATE, FIXED_YN, INPUT_USER_ID, INPUT_DATETIME)
    	 VALUES (
    	 	  #{boardIdx}
    	 	, #{boardType}
    	 	, #{boardTitle}
    	 	, #{boardDesc}
    	 	, #{useYn}
    	 	, #{dueDate}
    	 	, #{fixedYn}
    	 	, #{userId}
    	 	, SYSDATE
    	 )
    </insert>
    
    <update id="saveBoardUpdate" parameterType="hashmap">
    	UPDATE INFO.CRT_BOARD_INFO
    	   SET USE_YN = #{useYn}
    	     , FIXED_YN = #{fixedYn}
    	 WHERE BOARD_IDX = #{boardIdx}
    </update>
    
    <delete id="saveBoardDelete" parameterType="hashmap">
    	DELETE 
    	  FROM INFO.CRT_BOARD_INFO
    	 WHERE BOARD_IDX = #{boardIdx}
    </delete>
    
    <delete id="saveBoardFileDelete" parameterType="hashmap">
    	DELETE 
    	  FROM INFO.CRT_BOARD_FILE 
    	 WHERE BOARD_IDX = #{boardIdx}
    </delete>
    
    <insert id="saveBoardFileInsert" parameterType="hashmap">
    	INSERT INTO INFO.CRT_BOARD_FILE (BOARD_IDX, FILE_IDX, FILE_NAME, ORG_FILE_NAME, USE_YN, INPUT_USER_ID, INPUT_DATETIME)
    	 VALUES (
    	 	  #{boardIdx}
    	 	, (SELECT NVL(MAX(FILE_IDX) + 1, 1) FROM INFO.CRT_BOARD_FILE)
    	 	, #{fileName}
    	 	, #{orgFileName}
    	 	, 'Y'
    	 	, #{userId}
    	 	, SYSDATE
    	 )
    </insert>

</mapper>

 


백엔드 구성이 완료된 이후 

프론트 화면 및 화면용 스크립트를 작성하여 데이터에 대한 CRUD 기능을 적용 

에디터 및 파일 업로드 관련 코드 포함

 

[ 화면 레이아웃 구현 ] - admin.html 

경로 )
src\main\resources\templates\admin.html
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko" class="h-100">
<head>
<meta charset="utf-8">
<!-- 반응형 -->
<meta name="viewport" content="width=device-width, initial-scale=1">

<th:bloc th:replace="../static/common/header::header"></th:bloc>
</head>
<body class="d-flex flex-column h-100">
 
	<!-- 상단 메뉴바 -->
	<th:bloc th:replace="../static/common/menu::menu"></th:bloc>

	<!-- Begin page content -->
	<main class="flex-shrink-0">
		<div class="container">
			<h1 class="mt-5">게시판 관리</h1>
			<p class="lead">
				관리자를 통한 게시글 등록 및 수정 관리를 위한 화면입니다.
			</p>
			
			<br/>
			
			<nav>
				<div class="nav nav-tabs nav-pills nav-fill" id="nav-tab" role="tablist">
				    <button class="nav-link active" id="nav-page1-tab" data-bs-toggle="tab" data-bs-target="#nav-page1" type="button" role="tab" aria-controls="nav-page1" aria-selected="true">게시글 리스트</button>
				    <button class="nav-link" id="nav-page2-tab" data-bs-toggle="tab" data-bs-target="#nav-page2" type="button" role="tab" aria-controls="nav-page2" aria-selected="false">게시글 등록</button>
				</div>
			</nav>
			
			<br/>
			
			<div class="tab-content" id="nav-tabContent">
				<div class="tab-pane fade show active" id="nav-page1" role="tabpanel" aria-labelledby="nav-page1-tab" tabindex="0">
					<!-- 조회조건 영역 -->
					<div class="row p-5 bg-light border rounded-3 mb-5">
						<div class="col-sm-4 row">
							<label for="txtTitle1" class="col-sm-4 col-form-label">제목</label>
							<div class="col-sm-8">
								<input type="text" class="form-control" id="txtTitle1" placeholder="제목으로 검색">
							</div>
						</div>
						<div class="col-sm-4 row">
							<label for="cmbBoardType1" class="col-sm-4 col-form-label">카테고리</label>
							<div class="col-sm-8">
								<select id="cmbBoardType1" class="form-control"></select>
							</div>
						</div>
						
						<div class="col-sm-4 text-end">
							<button type="button" class="btn btn-primary" id="btnSearch">조회</button>
							<button type="button" class="btn btn-danger" id="btnDelete">삭제</button>
							<button type="button" class="btn btn-secondary" id="btnModify">저장</button>
						</div>
					</div>
					
					<!-- 데이터 영역 -->
					<div class="row p-5 bg-light border rounded-3 mb-5">
						<div class="com-sm-12">
							<div id="gridDataMain" class="jexcel_container"></div>
						</div>
					</div>
				</div>
				
				<!-- 2번째 탭 -->
  				<div class="tab-pane fade" id="nav-page2" role="tabpanel" aria-labelledby="nav-page2-tab" tabindex="0">
					<!-- 조회조건 영역 -->
					<div class="row p-5 bg-light border rounded-3 mb-5">
						<div class="col-sm-4 row">
							<label for="cmbCodeList" class="col-sm-4 col-form-label">상위 코드</label>
							<div class="col-sm-8">
								<select id="cmbCodeList" class="form-control">
								</select>
							</div>
						</div>
						
						<div class="col-sm-4 row">
							<label for="SearchCriteria" class="col-sm-4 col-form-label">검색조건 입력</label>
							<div class="col-sm-8">
								<input type="text" class="form-control" id="txtSearchSub" placeholder="아이디/코드명으로 검색">
							</div>
						</div>
						
						<div class="col-sm-4 text-end">
							<button type="button" class="btn btn-primary" id="btnSearchSub">조회</button>
							<button type="button" class="btn btn-success" id="btnAddSub">추가</button>
							<button type="button" class="btn btn-danger" id="btnDeleteSub">삭제</button>
							<button type="button" class="btn btn-secondary" id="btnSaveSub">저장</button>
						</div>
					</div>
					
					<!-- 데이터 영역 -->
					<div class="row p-5 bg-light border rounded-3 mb-5">
						<div class="com-sm-12">
							<div id="gridDataSub" class="jexcel_container"></div>
						</div>
					</div>
  				</div>
			</div>
			
		</div>
	</main>

	<!-- 하단 푸터 영역 -->
	<th:bloc th:replace="../static/common/footer::footer"></th:bloc>

	<!-- Page Script -->
	<script src="static/views/admin.js"></script>

</body>
</html>

 

[ 화면 스크립트 구현 ] - admin.js

경로 )
src\main\resources\static\views\admin.js
/*******************************************************************************
 * 
 * admin.js
 * 
 * @author THKIM
 * @since 2022
 * @DESC 관리자페이지 스크립트
 * 
 ******************************************************************************/
(function() {
	
	function Admin() {
		
		/* 
		 * private variables
		 */
		var UPLOAD_FILES = 0; // 첨부파일 업로드 카운트
		var CMB_BOARD_TYPE = [];  // 게시글 카테고리
		var USE_YN = ["Y", "N"];
		 
		var grid = null; // 메인 그리드
		
		var ORG_DATA = null;
		var selectedItems = [];
		
		var selectItem = function(item) {
			if($.inArray(item, selectedItems) < 0) {
				selectedItems.push(item);
			}
	    };
	    
	    var selectAllItem = function() {
	    	selectedItems = [];
	    	
	    	var count = grid.getData();
	    	for(var i = 0; i < count.length; i++) {
	    		var inItem = grid.getRowData(i);
	    		inItem[0] = true;
	    	}
	    	$("#gridDataMain > div.jexcel_content > table > tbody > tr > td > input[type=checkbox]").prop("checked", true);
	    	$.each(ORG_DATA, function(idx, node) {
	    		selectedItems.push(node);
	    	});
	    };
	 
	    var unselectItem = function(item) {
	    	selectedItems = [];
	    	
	    	var count = grid.getData();
	    	for(var i = 0; i < count.length; i++) {
	    		var inItem = grid.getRowData(i);
	    		
	    		if(inItem[0]) {
	    			selectedItems.push(inItem);
	    		}
	    	}
	    };
	    
	    var unselectAllItem = function() {
	    	var count = grid.getData();
	    	for(var i = 0; i < count.length; i++) {
	    		var inItem = grid.getRowData(i);
	    		inItem[0] = false;
	    	}
	    	
	    	$("#gridDataMain > div.jexcel_content > table > tbody > tr > td > input[type=checkbox]").prop("checked", false);
	    	selectedItems = [];
	    };
	    
		/* 
		 * 초기화 메소드
		 */
		function _init() {
			// 달력 한글 
			setKorDate();
			
			// 이벤트 
			bindEvent();
			
			// 게시글 카테고리 콤보박스 데이터 조회
			findCodeList();
			
			// 그리드 설정
			setGrid();
			
			// 에디터 설정
			setEditor();
			
			// 파일업로드 설정 
			setFileUpload();
		}
		
		// 달력 한글 설정 
		function setKorDate() {
			$.datepicker.setDefaults({
			    dateFormat: 'yymmdd',
			    prevText: '이전 달',
			    nextText: '다음 달',
			    monthNames: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
			    monthNamesShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
			    dayNames: ['일', '월', '화', '수', '목', '금', '토'],
			    dayNamesShort: ['일', '월', '화', '수', '목', '금', '토'],
			    dayNamesMin: ['일', '월', '화', '수', '목', '금', '토'],
			    showMonthAfterYear: true,
			    yearSuffix: '년'
			});
		}
		
		function bindEvent() {
			// 게시글 리스트 탭 클릭
			$("#linka").on("click", function() {
				$("#linkb").removeClass('lnb_on');
				$(this).addClass('lnb_on');
				$(".contentsPanel2").hide();
				$(".contentsPanel1").show();
			});
			
			// 게시글 등록 탭 클릭
			$("#linkb").on("click", function() {
				$("#linka").removeClass('lnb_on');
				$(this).addClass('lnb_on');
				$(".contentsPanel1").hide();
				$(".contentsPanel2").show();
			});
			
			// 조회 버튼 클릭 이벤트
			$("#btnSearch").on("click", function() {
				toastr.success('데이터 조회중입니다.', '게시글 리스트');
				setGrid();
			});
			
			// 삭제 버튼 클릭 이벤트
			$("#btnDelete").on("click", function() {
				setRemove();
			});
			
			// 저장 버튼 클릭 이벤트
			$("#btnModify").on("click", function() {
				setSave();
			});
			
			// 만료일 달력 
			$('#calDueDate').datepicker({
				autoClose: true,
				language: 'ko',
				dateFormat: 'yy-mm-dd'
			});
			
			// 게시글 저장 버튼 클릭 이벤트
			$("#btnSave").on("click", function() {
				saveBoardBefore();
			});
		}
		
		/*
		 * 에디터 설정
		 */
		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']],
				    ['para', ['paragraph']],
				    ['height', ['height']],
				    ['insert',['picture','link']],
				    ['view', ['help']]
				  ],
				fontNames: ['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']
		    });
		}
		
		/*
		 * 코드 정보 리스트 조회 
		 */
		function findCodeList() {
			var obj = {
				codeId: 'BOARD_TYPE'
			}
			
			var items = [];
			var html = "";
			cfFind("/findCodeList", obj, function(data) {
				$.each(data, function(idx, node) {
					html += "<option id='" + node.DETAIL_ID + "'>" + node.CODE_NAME + "</option>";
					
					items.push({
						id: node.DETAIL_ID,
						name: node.CODE_NAME
					});
				});
				
				
				$("#cmbBoardType1").html("<option id=''>전체</option>" + html);
				$("#cmbBoardType2").html(html);
				
				CMB_BOARD_TYPE = items;
			}, true, "POST");
		}
		
		/*
		 * 그리드 설정
		 */
		function setGrid() {
			// 그리드 초기화
			$("#gridDataMain").html("");
			selectedItems = [];
			
			// 컬럼 정보 조회
			var colData = new Array();
			
			var chk = "<div class='confirm-checkbox' style='margin: auto;'>";
			chk += "<input type='checkbox' id='confirm-checkbox' class='checkno'>";
			chk += "<label for='confirm-checkbox'></label>";
			chk += "</div>";
		
			colData.push(chk);
			colData.push("아이디");
			colData.push("카테고리");
			colData.push("제목");
			colData.push("사용여부");
			colData.push("만기일");
			colData.push("고정여부");
			colData.push("입력자");
			colData.push("입력일시");
			colData.push("수정자");
			colData.push("수정일시");
			colData.push("첨부파일");
			
			var colWidths = new Array();
			var colOptions = new Array();
			var allData = new Array();
			
			colWidths = [50, 100, 100, 150, 100, 100, 100, 100, 100, 100, 100, 100];
			
			for(var j = 0; j < colData.length; j++) {
				if(j == 0) {
					colOptions.push({ type: 'checkbox' });
				} else if(colData[j] == "아이디") { 
					colOptions.push({ type: 'hidden' });
				} else if(colData[j] == "카테고리") { 
					colOptions.push({ type: 'dropdown', source: CMB_BOARD_TYPE, readOnly: true});
				} else if(colData[j] == "사용여부" || colData[j] == "고정여부") { 
					colOptions.push({ type: 'dropdown', source: USE_YN });
				} else { 
					colOptions.push({ type: 'text', readOnly: true });
				}
			}
			
			var obj = {
				searchText: $("#txtTitle1").val(),
				searchType: $("#cmbBoardType1 option:selected")[0].id
			}
			
			cfFind("/admin/findBoardList", obj, function(data) {
				if(data.length > 0) {
					var rowData = [];
					$.each(data, function(idx, node) {
						rowData = [];
						rowData.push("");
						rowData.push(node.BOARD_IDX);
						rowData.push(node.BOARD_TYPE);
						rowData.push(node.BOARD_TITLE);
						rowData.push(node.USE_YN);
						rowData.push(node.DUE_DATE);
						rowData.push(node.FIXED_YN);
						rowData.push(node.INPUT_USER_ID);
						rowData.push(node.INPUT_DATETIME);
						rowData.push(node.MODIFY_USER_ID);
						rowData.push(node.MODIFY_DATETIME);
						rowData.push(node.FILE_YN);
						
						allData.push(rowData);
					});
				} else {
					allData = [];
				}
				
				ORG_DATA = allData;
			}, true, "POST");
			
			grid = jexcel(document.getElementById('gridDataMain'), {
			    data : allData,
			    colHeaders :  colData,
			    colWidths : colWidths,
			    columns: colOptions,
			    selectionCopy: true,
			    editable: true,
			    allowInsertRow : false,
			    allowInsertColumn : false,
			    tableOverflow:true,
			    lazyLoading:false,
			    loadingSpin:true,
			    onchange: changed,
			    contextMenu:function() { return false; } // 우측 마우스 클릭 시 메뉴 비활성화
			});
			
			// 그리드 체크박스 이벤트
			$("#confirm-checkbox").on("click", function() {
				if($(this).hasClass("checkno")) {
	          		$(this).removeClass("checkno");
	          		$(this).addClass("checkok");
	          		
	          		selectAllItem();
	          	} else {
	          		$(this).removeClass("checkok");
	          		$(this).addClass("checkno");
	          		
	          		unselectAllItem();
	          	}
			});
		}
		
		// 그리드 셀 편집 시 표시하기 위함
		var changed = function(instance, cell, x, y, value) {
			// y : 0부터 시작함 그리드 첫번째 행이 0
			var item = grid.getRowData(y);
			
			if(x != 0) {
				var checkbox = $("input[type=checkbox]");
				$.each(checkbox, function(idx, node) {
					if(idx == (Number(y) + 1)) {
						checkbox[idx].checked = true;
						item[0] = true;
						selectItem(item);
					}
				});
				
				cell.style.backgroundColor = "#f1e3bf";
			} else {
				// 그리드 셀 체크박스 이벤트
				if(value == true || value) {
					selectItem(item);
				} else {
					unselectItem(item);
				}
			}
		}
		
		/*
		 * 리스트 삭제
		 */
		function setRemove() {
			if(selectedItems.length == 0) {
				alert("선택된 항목이 없습니다.");
				return false;
			}
			
			var list = setParam(selectedItems);
			
			var result = confirm('선택한 항목을 삭제하시겠습니까?');
			if(result) {
				
				cfSave("/admin/saveBoardDelete", list, function(data) {
					if (data.success == true) {
						toastr.success('데이터 삭제가 완료되었습니다.', '게시글 관리');
						setGrid();
						selectedItems = [];
						$("#txtBoardId").val(data.boardIdx); // 신규 게시글 번호 초기화
					} else {
						toastr.error('데이터 삭제 오류', '게시글 관리');
					}
				});
			}
		}
		
		/*
		 * 저장
		 */
		function setSave() {
			if(selectedItems.length == 0) {
				alert("선택된 항목이 없습니다.");
				return false;
			}
			
			var list = setParam(selectedItems);
			var result = confirm('선택한 항목을 저장하시겠습니까?');
			if(result) {
				
				cfSave("/admin/saveBoardUpdate", list, function(data) {
					if (data.success == true) {
						toastr.success('데이터 저장이 완료되었습니다.', '게시글 관리');
						setGrid();
						selectedItems = [];
						$("#txtBoardId").val(data.boardIdx); // 신규 게시글 번호 초기화
					} else {
						toastr.error('데이터 저장 오류', '게시글 관리');
					}
				});
				
			}
		}
		
		// 수정, 삭제 시 파라미터 만들기
		function setParam(obj) {
			
			var list = new Array();
			$.each(obj, function(idx, node) {
				var map = {};
				map.boardIdx = node[1];
				map.useYn = node[4];
				map.fixedYn = node[6];
				list.push(map);
			});
			
			return list;
		}
		
		/*
		 * 게시글 저장 전 체크 
		 */
		function saveBoardBefore() {
			var boardTitle = $("#txtTitle2").val(); // 게시글 제목 
			if(boardTitle == "") { 
				alert("게시글 제목을 입력하시기 바랍니다."); 
				$("#txtTitle2").focus();
				return false; 
			}
			
			var message = "";
			if(UPLOAD_FILES == 0) {
				message = "첨부파일 업로드 없이 게시글을 저장하시겠습니까?";
			} else {
				message = "게시글을 저장하시겠습니까?";
			}
			
			var result = confirm(message);
			if(result) {
				if(UPLOAD_FILES > 0) {
					$('#drag-and-drop-zone').dmUploader('start');
				} else {
					saveBoard();
				}
			}
		}
		
		/*
		 * 게시글 저장 파라미터 
		 */
		function saveBoardParam() {
			var boardIdx = $("#txtBoardId").val(); // 게시글 아이디 
			var code = $('#summernote').summernote('code'); // 게시글 내용 
			var boardType = $("#cmbBoardType2 option:selected")[0].id; // 게시글 타입 
			var boardTitle = $("#txtTitle2").val(); // 게시글 제목 
			var useYn = $("#cmbUseYn").val(); // 사용 여부
			var dueDate = $("#calDueDate").val(); // 만료 날짜
			var fixedYn = $("#cmbFixedYn").val(); // 고정 여부 
			
			var obj = {
				boardIdx: boardIdx,
				boardDesc: code,
				boardType: boardType,
				boardTitle: boardTitle,
				useYn: useYn,
				dueDate: dueDate,
				fixedYn: fixedYn
			}
			
			return obj;
		}
		
		/*
		 * 게시글 저장
		 */
		function saveBoard() {
			var obj = saveBoardParam();
			cfSave("/admin/saveBoardInsert", obj, function(data) {
				if (data.success) {
					toastr.success('데이터 저장이 완료되었습니다.', '게시글 관리');
					
					// form 초기화
					$("#txtBoardId").val(data.boardIdx);
					$('#summernote').summernote('code', "");
					$("#txtTitle2").val(""); // 제목 초기화 
					$("#calDueDate").val(""); // 만료일자 초기화 
					$("#cmbBoardType2 option:eq(0)").prop("selected", "selected"); // 카테고리 - 첫번째 항목으로 선택 
					$("#cmbUseYn option:eq(0)").prop("selected", "selected"); // 사용여부 - Y로 선택
					$("#cmbFixedYn option:eq(1)").prop("selected", "selected"); // 고정여부 - N으로 선택 
					
					// 게시글 리스트 재조회
					setGrid();
					
					// 첨부파일 업로드 초기화 
					setFileUpload();
					
				} else {
					toastr.error('데이터 저장 오류', '게시글 관리');
				}
			});
		}
		
		/*
		 * 게시글 첨부파일 리스트 저장
		 */
		function saveFileInsert(obj) {
			cfSave("/admin/saveBoardFileInsert", obj, function(data) {
			});
		}
		
		/*
		 * 파일 업로드 설정
		 */
		function setFileUpload() {
			UPLOAD_FILES = 0;
			
			$('#drag-and-drop-zone').dmUploader({ 
			    url: '/admin/fileUpload',
			    auto: false,
 				queue: true,
			    maxFileSize: 10000000, // 10M 
			    onDragEnter: function(){
			    	this.addClass('active');
			    },
			    onDragLeave: function(){
			    	this.removeClass('active');
			    },
			    onInit: function(){
			    },
			    onComplete: function(){
					// 업로드 완료 시 
					toastr.success('업로드가 완료되었습니다.', '파일 업로드');
					// 게시글 저장
			    	saveBoard();
			    },
			    onNewFile: function(id, file){
			    	// 새로 드래그하거나 추가한 파일이 있는 경우
			    	ui_multi_add_file(id, file, "");
			    	UPLOAD_FILES++;
			    },
			    onBeforeUpload: function(id){
					ui_multi_update_file_status(id, 'uploading', 'Uploading...', "");
					ui_multi_update_file_progress(id, 0, '', true, "");
			    },
			    onUploadCanceled: function(id) {
			    	// Happens when a file is directly canceled by the user.
			    	ui_multi_update_file_status(id, 'warning', 'Canceled by User', "");
			    	ui_multi_update_file_progress(id, 0, 'warning', false, "");
			    },
			    onUploadProgress: function(id, percent){
			    	// Updating file progress
			    	ui_multi_update_file_progress(id, percent, null, null, "");
			    },
			    onUploadSuccess: function(id, data){
			    	var obj = {
			    			fileName: data.fileName,
			    			orgFileName: data.orgFileName,
			    			boardIdx: $("#txtBoardId").val()
			    	}
			    	
			    	// 개별 파일 업로드가 완료된 경우 게시글에 대한 첨부파일 리스트 저장
			    	saveFileInsert(obj);
			    	
			    	ui_multi_update_file_status(id, 'success', 'Upload Complete', "");
			    	ui_multi_update_file_progress(id, 100, 'success', false, "");
			    },
			    onUploadError: function(id, xhr, status, message){
			    	ui_multi_update_file_status(id, 'danger', 'Upload Failed', "");
			    	ui_multi_update_file_progress(id, 100, 'danger', false, "");  
			    },
			    onFallbackMode: function(){
			      // When the browser doesn't support this plugin :(
			    },
			    onFileSizeError: function(file){
			    	alert('파일명 : \'' + file.name + '\' 업로드 사이즈(10MB 이내)를 초과하였습니다. 확인 후 다시 첨부하시기 바랍니다.', 'danger');
			    }
			});
		}
		
		// Creates a new file and add it to our list
		function ui_multi_add_file(id, file, numIdx) {
			var template = $('#files-template').text();
			template = template.replace('%%filename%%', file.name);

			template = $(template);
			template.prop('id', 'uploaderFile' + numIdx + id);
			template.data('file-id', id);

			$('#files' + numIdx).find('li.empty').fadeOut(); // remove the 'no files yet'
			$('#files' + numIdx).prepend(template);
		}

		// Changes the status messages on our list
		function ui_multi_update_file_status(id, status, message, numIdx) {
			$('#uploaderFile' + numIdx + id).find('span').html(message).prop('class',
					'status text-' + status);
		}

		// Updates a file progress, depending on the parameters it may animate it or
		// change the color.
		function ui_multi_update_file_progress(id, percent, color, active, numIdx) {
			color = (typeof color === 'undefined' ? false : color);
			active = (typeof active === 'undefined' ? true : active);

			var bar = $('#uploaderFile' + numIdx + id).find('div.progress-bar');

			bar.width(percent + '%').attr('aria-valuenow', percent);
			bar.toggleClass('progress-bar-striped progress-bar-animated', active);

			if (percent === 0) {
				bar.html('');
			} else {
				bar.html(percent + '%');
			}

			if (color !== false) {
				bar.removeClass('bg-success bg-info bg-warning bg-danger');
				bar.addClass('bg-' + color);
			}
		}
		
		
		function _finalize() {
		}
		
		return {
            init : _init,
            finalize : _finalize
        };
    };
    
    var admin = new Admin();
    admin.init();
    
})();

//# sourceURL=admin.js