[프로젝트] 11. 관리자 화면 - 게시글 관리 구현 (Week 4)
공지사항 등의 게시글을 관리하는 화면으로
탭 형태로 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 내에 사용되는 폰트를 위한 파일도 첨부
아래 파일들을 프로젝트에 복사
경로 )
src\main\resources\static\css\font
font 폴더를 생성 후 아래에 4개 파일 복사
참고 사이트 )
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
참고 사이트 )
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