Sfoglia il codice sorgente

이태영 - 20210125 장바구니 제거

xodud1202 5 anni fa
parent
commit
22012db9b5

+ 35 - 0
src/main/java/com/style24/persistence/domain/Cart.java

@@ -0,0 +1,35 @@
+package com.style24.persistence.domain;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.style24.persistence.TscBaseDomain;
+import com.style24.persistence.TsfPageRequest;
+import lombok.Data;
+
+/**
+ * 장바구니
+ *
+ * @author xodud1202
+ * @since 2021.01.22
+ */
+@SuppressWarnings("serial")
+@Data
+public class Cart extends TscBaseDomain {
+	// 장바구니 정보
+	private int cartSq;			// 장바구니 번호
+	private int custNo;			// 고객번호
+	private int planDtlSq;		// 기획전상세번호
+	private String cartGb;		// 장바구니 구분 (공통코드G026)
+	private String goodsCd;		// 상품번호
+	private String productNo;	// ProductNo(WMS)
+	private String productCode;	// ProductCode(WMS)
+	private String jsessionId;	// JSESSIONID
+	private String afLinkCd;	// 제휴링크코드
+	private String ithrCd;		// 유입경로(공통코드 G027)
+	private String contentsLoc;	// 컨텐츠위치(공통코드G028)
+
+	// 상품 정보
+
+	// 다다익선 정보
+
+	// 즉시할인쿠폰 정보
+}

+ 42 - 0
src/main/webapp/WEB-INF/views/web/cart/cartListForm.html

@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="ko" xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+    <!-- Include JS library -->
+    <script type="text/javascript" src="/ux/plugins/jquery/jquery-1.12.4.min.js"></script>
+    <script type="text/javascript" src="/ux/plugins/jquery/jquery-ui.min.js"></script>
+    <script type="text/javascript" src="/ux/plugins/jquery/jquery.serializeObject.min.js"></script>
+
+    <!-- Include gaga library -->
+    <link rel="stylesheet" href="/ux/plugins/gaga/gaga.agGrid.css"/>
+    <script type="text/javascript" src="/ux/plugins/gaga/gaga.common.js"></script>
+    <script type="text/javascript" src="/ux/plugins/gaga/gaga.validation.js"></script>
+    <script type="text/javascript" src="/ux/plugins/gaga/gaga.agGrid.js"></script>
+    <script type="text/javascript" src="/ux/plugins/gaga/gaga.alert.js"></script>
+</head>
+<body>
+    <form id="cartInfo" name="searchForm" action="/cart/createCart">
+        <div with="500" height="500" style="text-align:center;">
+            <div style="vertical-align: middle;">
+                <input type="text" name="goodsCd" />
+                <button type="button" id="save">cart 저장</button>
+            </div>
+        </div>
+    </form>
+
+    <script type="text/javascript">
+        // 저장 후 콜백 함수
+        var saveFinish = function(result) {
+            alert(result.result);
+        }
+        $("#save").click(function() {
+            let data = {
+                goodsCd : $("#cartInfo input[name=goodsCd]").val()
+            };
+            let jsonData = JSON.stringify(data);
+            gagajf.ajaxJsonSubmit('/cart/createCartInfo', jsonData, saveFinish);
+        });
+    </script>
+</body>
+</html>

+ 10 - 0
src/main/webapp/WEB-INF/views/web/error/500Web.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+</head>
+<body>
+
+</body>
+</html>

+ 36 - 0
src/main/webapp/ux/plugins/gaga/gaga.agGrid.css

@@ -0,0 +1,36 @@
+/*
+ * agGrid css written by gagamel.
+ *
+ * Copyright (c) 2019 gagamel
+ *
+ * $Date: 2019-04-04 $
+ *
+ */
+
+.text-left { text-align: left; }
+.text-center { text-align: center; }
+.text-right { text-align: right; }
+
+/* Tooltip */
+.custom-tooltip {
+    position: absolute;
+    width: 150px;
+    height: 70px;
+    border: 1px solid cornflowerblue;
+    overflow: hidden;
+    pointer-events: none;
+    transition: opacity 1s;
+}
+
+.custom-tooltip.ag-tooltip-hiding {
+    opacity: 0;
+}
+
+.custom-tooltip p {
+    margin: 5px;
+    white-space: nowrap;
+}
+
+.custom-tooltip p:first-of-type {
+    font-weight: bold;
+}

+ 1874 - 0
src/main/webapp/ux/plugins/gaga/gaga.agGrid.js

@@ -0,0 +1,1874 @@
+/*
+ * ag-Grid(https://www.ag-grid.com) Common Java Script written by gagamel.
+ *
+ * Copyright (c) 2019 gagamel
+ * Dual licensed under GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2019-03-31 $
+ */
+
+var isNavigationKey = function(event) {
+	var KEY_LEFT = 37;
+	var KEY_UP = 38;
+	var KEY_RIGHT = 39;
+	var KEY_DOWN = 40;
+	var KEY_PAGE_UP = 33;
+	var KEY_PAGE_DOWN = 34;
+	var KEY_PAGE_HOME = 36;
+	var KEY_PAGE_END = 35;
+
+	var keyCode = event.keyCode;
+
+	return keyCode === KEY_LEFT || keyCode === KEY_RIGHT || keyCode === KEY_UP || keyCode === KEY_DOWN
+		|| keyCode === KEY_PAGE_DOWN || keyCode === KEY_PAGE_UP || keyCode === KEY_PAGE_HOME || keyCode === KEY_PAGE_END;
+}
+
+/**
+ * Customer loading
+ * @author gagamel
+ * @since 2019. 4. 10
+ */
+var getCustomLoadingOverlay = function() {
+	function CustomLoadingOverlay() {};
+
+	CustomLoadingOverlay.prototype.init = function(params) {
+		this.eGui = document.createElement('div');
+		this.eGui.innerHTML = ''
+			+ '<div style="'
+			+ 'background: url(/ux/plugins/gaga/loader.gif); border-style: none; background-repeat: no-repeat; '
+			+ 'position: absolute; top: 45%; left: 50%; width: auto; '
+			+ 'z-index: 101; padding: 16px; margin: 5px;'
+			+ '"></div>';
+	};
+
+	CustomLoadingOverlay.prototype.getGui = function() {
+		// Button disabled & progressBar creation
+		//$('.btn').each(function(idx) { $(this).attr('disabled', true); });
+		return this.eGui;
+	};
+
+	return CustomLoadingOverlay;
+}
+
+/**
+ * Datepicker component
+ * <pre>
+ *     cellEditor : 'datePicker'
+ * </pre>
+ * @author gagamel
+ * @since 2019. 4. 8
+ */
+var getDatePicker = function(pattern) {
+	if (typeof(pattern) == 'undefined') {
+		pattern = "yyyy/mm/dd";
+	}
+
+	// function to act as a class
+	function Datepicker() {};
+
+	// gets called once before the renderer is used
+	Datepicker.prototype.init = function(params) {
+		this.eGui = document.createElement('div');
+		this.eGui.innerHTML = '<input'
+			+ ' name="' + params.colDef.field + '"'
+			+ (!gagajf.isNull(params.value) ? ' value="' + params.value + '"' : '')
+			+ (!gagajf.isNull(params.maxlength) ? ' maxlength="' + params.maxlength + '"' : '')
+			+ (!gagajf.isNull(params.validType) ? ' data-valid-type="' + params.validType + '"' : '')
+			+ ' style="width: 100%; z-index: 999999"/>';
+
+		this.eInput = this.eGui.querySelector('input');
+
+		if (params.required) {
+			this.eInput.required = 'required';
+//			this.eInput.classList.add('required');
+		}
+
+		$(this.eInput).datepicker({
+			startView: 0,
+			format: pattern,
+			todayBtn: "linked",
+			keyboardNavigation: false,
+			forceParse: false,
+			autoclose: true,
+			todayHighlight: true,
+			onSelect: function(dateText, inst) {
+				params.stopEditing();
+			}
+		});
+	};
+
+	// gets called once when grid ready to insert the element
+	Datepicker.prototype.getGui = function() {
+		return this.eGui;
+	};
+
+	// focus and select can be done after the gui is attached
+	Datepicker.prototype.afterGuiAttached = function() {
+		this.eInput.focus();
+		this.eInput.select();
+	};
+
+	// returns the new value after editing
+	Datepicker.prototype.getValue = function() {
+		return this.eInput.value;
+	};
+
+	// any cleanup we need to be done here
+	Datepicker.prototype.destroy = function() {
+		// but this example is simple, no cleanup, we could
+		// even leave this method out as it's optional
+	};
+
+	// if true, then this editor will appear in a popup
+	Datepicker.prototype.isPopup = function() {
+		// and we could leave this method out also, false is the default
+		return true;
+	};
+
+	return Datepicker;
+}
+
+/**
+ * Datepicker component
+ * <pre>
+ *     cellEditor : 'dateTimer'
+ * </pre>
+ * @author gagamel
+ * @since 2020. 1. 09
+ */
+var getDateTimer = function(pattern) {
+	// function to act as a class
+	function DateTimer() {};
+
+	// gets called once before the renderer is used
+	DateTimer.prototype.init = function(params) {
+		this.eGui = document.createElement('div');
+
+		// 시간 초기화
+		var TimeH = '';
+		var TimeM = '';
+		var SecGb =  params.colDef.field;
+		if( SecGb.indexOf('ed') > -1 || SecGb.indexOf('end') > -1) {
+			TimeH = '23';
+			TimeM = '59';
+			this.sec = '59';
+		}else{
+			TimeH = '00';
+			TimeM = '00';
+			this.sec = '00';
+		}
+
+		if(typeof(params.value) != 'undefined') {
+			var time = params.value.split(":");
+			TimeH = time[0];
+			TimeM = time[1];
+		}
+
+		// 시간 셀렉트 박스
+		var time = 0;
+		var Hhtml = '';
+		Hhtml += '<select name="' + params.colDef.field + 'H">\n';
+		for (var i = 0; i < 24; i++) {
+			time = i;
+			if ( time < 10)
+				time = '0' + time;
+			if (TimeH != i) {
+				Hhtml += '<option value="' + time + '">' + time + '시</option>\n';
+			} else {
+				Hhtml += '<option selected="selected" value="' + time + '">' + time + '시</option>\n';
+			}
+		}
+		Hhtml += '</select>\n';
+
+		this.eGui.innerHTML = Hhtml;
+		this.eSelectH = this.eGui.querySelector('select');
+		this.eSelectH.innerHTML = Hhtml;
+
+		// 분 셀렉트 박스
+		time = 0;
+		var Mhtml ='';
+		Mhtml = '<select name="' + params.colDef.field + 'M">\n';
+		for (var i = 0; i < 60; i++) {
+			time = i;
+			if ( time < 10)
+				time = '0' + time;
+			if (TimeM != i) {
+				Mhtml += '<option value="' + time + '">' + time + '분</option>\n';
+			} else {
+				Mhtml += '<option selected="selected" value="' + time + '">' + time + '분</option>\n';
+			}
+		}
+		Mhtml += '</select>';
+
+		this.eGui.innerHTML = Mhtml;
+		this.eSelectM = this.eGui.querySelector('select');
+		this.eSelectM.innerHTML = Mhtml;
+		this.eGui.innerHTML = Hhtml + Mhtml; // html 전체가 있어야 하기 떄문에 마지막에 전체 입력
+
+		if (params.required) {
+			this.eSelectH.required = 'required';
+			this.eSelectM.required = 'required';
+//			this.eInput.classList.add('required');
+		}
+	};
+
+	// gets called once when grid ready to insert the element
+	DateTimer.prototype.getGui = function() {
+		return this.eGui;
+	};
+
+	// focus and select can be done after the gui is attached
+	DateTimer.prototype.afterGuiAttached = function() {
+		this.eSelectH.focus();
+	};
+
+	// returns the new value after editing
+	DateTimer.prototype.getValue = function() {
+		return $('[name=' + this.eSelectH.name + '] option:selected').val() + ':' + $('[name=' + this.eSelectM.name + '] option:selected').val() + ':' + this.sec ;
+	};
+
+	// any cleanup we need to be done here
+	DateTimer.prototype.destroy = function() {
+		// but this example is simple, no cleanup, we could
+		// even leave this method out as it's optional
+	};
+
+	// if true, then this editor will appear in a popup
+	DateTimer.prototype.isPopup = function() {
+		// and we could leave this method out also, false is the default
+		return false;
+	};
+
+	return DateTimer;
+}
+
+/**
+ * Numeric editor component
+ * <pre>
+ *     cellEditor: 'numericCellEditor'
+ * </pre>
+ * @author gagamel
+ * @since 2019. 4. 25
+ */
+var getNumericCellEditor = function() {
+	function isCharNumeric(charStr) {
+		return !!/\d/.test(charStr);
+	}
+
+	function getCharCodeFromEvent(event) {
+		event = event || window.event;
+		return (typeof event.which == "undefined") ? event.keyCode : event.which;
+	}
+
+	function isKeyPressedNumeric(event) {
+		var charCode = getCharCodeFromEvent(event);
+		var charStr = String.fromCharCode(charCode);
+		return isCharNumeric(charStr);
+	}
+
+	// function to act as a class
+	function NumericCellEditor() {};
+
+	// gets called once before the renderer is used
+	NumericCellEditor.prototype.init = function(params) {
+		// create the cell
+		this.eInput = document.createElement('input');
+
+		if (isCharNumeric(params.charPress)) {
+			this.eInput.value = params.charPress;
+		} else {
+			if (!gagajf.isNull(params.value)) {
+				this.eInput.value = params.value;
+			}
+		}
+
+		var that = this;
+		this.eInput.addEventListener('keypress', function(event) {
+			if (!isKeyPressedNumeric(event)) {
+				that.eInput.focus();
+				if (event.preventDefault) event.preventDefault();
+			//} else if (that.isKeyPressedNavigation(event)) {
+			} else if (isNavigationKey(event)) {
+				event.stopPropagation();
+			}
+		});
+
+		// only start edit if key pressed is a number, not a letter
+		var charPressIsNotANumber = params.charPress && ('1234567890'.indexOf(params.charPress) < 0);
+		this.cancelBeforeStart = charPressIsNotANumber;
+	};
+
+//	NumericCellEditor.prototype.isKeyPressedNavigation = function(event) {
+//		return event.keyCode === 39 || event.keyCode === 37;
+//	};
+
+	// gets called once when grid ready to insert the element
+	NumericCellEditor.prototype.getGui = function() {
+		return this.eInput;
+	};
+
+	// focus and select can be done after the gui is attached
+	NumericCellEditor.prototype.afterGuiAttached = function() {
+		this.eInput.focus();
+	};
+
+	// returns the new value after editing
+	NumericCellEditor.prototype.isCancelBeforeStart = function() {
+		return this.cancelBeforeStart;
+	};
+
+	// example - will reject the number if it contains the value 007
+	// - not very practical, but demonstrates the method.
+	NumericCellEditor.prototype.isCancelAfterEnd = function() {
+		var value = this.getValue();
+		return value.indexOf('007') > -1;
+	};
+
+	// returns the new value after editing
+	NumericCellEditor.prototype.getValue = function() {
+		return this.eInput.value;
+	};
+
+	// any cleanup we need to be done here
+	NumericCellEditor.prototype.destroy = function() {
+		// but this example is simple, no cleanup, we could  even leave this method out as it's optional
+	};
+
+	// if true, then this editor will appear in a popup
+	NumericCellEditor.prototype.isPopup = function() {
+		// and we could leave this method out also, false is the default
+		return false;
+	};
+
+	return NumericCellEditor;
+}
+
+/**
+ * Cell renderer component for checkbox with value (Y/N)
+ * <pre>
+ *     cellRenderer: 'booleanCellRenderer'
+ * </pre>
+ * @author gagamel
+ * @since 2019. 4. 8
+ */
+var getBooleanCellRenderer = function() {
+
+	// function to act as a class
+	function BooleanCellRenderer() {};
+
+	// gets called once before the renderer is used
+	BooleanCellRenderer.prototype.init = function(params) {
+		this.eGui = document.createElement('span');
+
+		if (!gagajf.isNull(params.value)) {
+			var checked = (params.value == "Y" || params.value == "1") ? "checked" : "";
+			var input = document.createElement('input');
+			input.type = "checkbox";
+			input.checked = checked;
+			input.value = params.value;
+			input.addEventListener('click', function (event) {
+				if (params.value == "Y") params.value = "N";
+				else if (params.value == "N") params.value = "Y";
+				else if (params.value == "1") params.value = "0";
+				else if (params.value == "0") params.value = "1";
+
+				// checked input value has changed, perform your update here
+				params.data[params.colDef.field] = params.value;
+				params.data.crud = "U";
+				params.api.updateRowData({update: [params.data]});
+			});
+			this.eGui.innerHTML = '';
+			this.eGui.appendChild(input);
+		}
+	};
+
+	// gets called once when grid ready to insert the element
+	BooleanCellRenderer.prototype.getGui = function() {
+		return this.eGui;
+	};
+
+	// focus and select can be done after the gui is attached
+	BooleanCellRenderer.prototype.afterGuiAttached = function() {
+
+	};
+
+	// returns the new value after editing
+	BooleanCellRenderer.prototype.getValue = function() {
+		return this.eGui.value;
+	};
+
+	// any cleanup we need to be done here
+	BooleanCellRenderer.prototype.destroy = function() {
+		// but this example is simple, no cleanup, we could
+		// even leave this method out as it's optional
+	};
+
+	// if true, then this editor will appear in a popup
+	BooleanCellRenderer.prototype.isPopup = function() {
+		// and we could leave this method out also, false is the default
+		return false;
+	};
+
+	return BooleanCellRenderer;
+}
+
+/**
+ * Checkbox editor component with value (Y/N)
+ * <pre>
+ *     cellEditor: 'booleanCellEditor'
+ * </pre>
+ * @author gagamel
+ * @since 2019. 4. 8
+ */
+var getBooleanCellEditor = function() {
+
+	// function to act as a class
+	function BooleanCellEditor() {};
+
+	// gets called once before the renderer is used
+	BooleanCellEditor.prototype.init = function(params) {
+		this.container = document.createElement('div');
+		this.value = params.value;
+		params.stopEditing();
+	};
+
+	// gets called once when grid ready to insert the element
+	BooleanCellEditor.prototype.getGui = function() {
+		return this.container;
+	};
+
+	// focus and select can be done after the gui is attached
+	BooleanCellEditor.prototype.afterGuiAttached = function() {
+
+	};
+
+	BooleanCellEditor.prototype.getValue = function() {
+		return this.value;
+	};
+
+	// any cleanup we need to be done here
+	BooleanCellEditor.prototype.destroy = function() {
+
+	};
+
+	// if true, then this editor will appear in a popup
+	BooleanCellEditor.prototype.isPopup = function() {
+		return true;
+	};
+
+	return BooleanCellEditor;
+}
+
+///**
+// * Cell renderer component for selectbox
+// * <pre>
+// *     cellRenderer: 'selectCellRenderer'
+// * </pre>
+// * @param comboList - Combo List
+// * @author gagamel
+// * @since 2019. 4. 29
+// */
+//var getSelectCellRenderer = function(comboList) {
+//
+//	// function to act as a class
+//	function SelectCellRenderer() {};
+//
+//	// gets called once before the renderer is used
+//	SelectCellRenderer.prototype.init = function(params) {
+//		this.eGui = document.createElement('span');
+//		var select = document.createElement('select');
+//
+//		console.log('========getSelectCellRenderer========');
+//		console.log(comboList);
+//
+//		var options;
+//		var comboLen = comboList.length;
+//		for (var i = 0; i < comboLen; i++) {
+//			options += '<option value="' + comboList[i].cd + '">' + comboList[i].cdNm + '</option>';
+//		}
+//
+//		console.log(options);
+//		select.appendChild(options);
+//
+//		//input.value = params.value;
+//		this.eGui.innerHTML = '';
+//		this.eGui.appendChild(select);
+//	};
+//
+//	// gets called once when grid ready to insert the element
+//	SelectCellRenderer.prototype.getGui = function() {
+//		return this.eGui;
+//	};
+//
+//	// focus and select can be done after the gui is attached
+//	SelectCellRenderer.prototype.afterGuiAttached = function() {
+//
+//	};
+//
+//	// returns the new value after editing
+//	SelectCellRenderer.prototype.getValue = function() {
+//		return this.eGui.value;
+//	};
+//
+//	// any cleanup we need to be done here
+//	SelectCellRenderer.prototype.destroy = function() {
+//		// but this example is simple, no cleanup, we could
+//		// even leave this method out as it's optional
+//	};
+//
+//	// if true, then this editor will appear in a popup
+//	SelectCellRenderer.prototype.isPopup = function() {
+//		// and we could leave this method out also, false is the default
+//		return false;
+//	};
+//
+//	return SelectCellRenderer;
+//}
+//
+///**
+// * Selectbox editor component
+// * <pre>
+// *     cellEditor: 'selectCellEditor'
+// * </pre>
+// * @author gagamel
+// * @since 2019. 4. 29
+// */
+//var getSelectCellEditor = function() {
+//	// function to act as a class
+//	function SelectCellEditor() {};
+//
+//	// gets called once before the renderer is used
+//	SelectCellEditor.prototype.init = function(params) {
+//		// create the cell
+//		this.eInput = document.createElement('select');
+//		this.eInput.value = params.value;
+//
+//		var that = this;
+//		this.eInput.addEventListener('change', function(event) {
+//			console.log('SelectCellEditor change');
+////			that.eInput.focus();
+////			if (event.preventDefault) event.preventDefault();
+//			event.stopPropagation();
+//		});
+//	};
+//
+//	// gets called once when grid ready to insert the element
+//	SelectCellEditor.prototype.getGui = function() {
+//		console.log('SelectCellEditor getGui');
+//		return this.eInput;
+//	};
+//
+//	// focus and select can be done after the gui is attached
+//	SelectCellEditor.prototype.afterGuiAttached = function() {
+//		console.log('SelectCellEditor afterGuiAttached');
+//		this.eInput.focus();
+//	};
+//
+//	// returns the new value after editing
+//	SelectCellEditor.prototype.isCancelBeforeStart = function() {
+//		console.log('SelectCellEditor isCancelBeforeStart');
+//		//return this.cancelBeforeStart;
+//	};
+//
+//	// not very practical, but demonstrates the method.
+//	SelectCellEditor.prototype.isCancelAfterEnd = function() {
+//		console.log('SelectCellEditor isCancelAfterEnd');
+//	};
+//
+//	// returns the new value after editing
+//	SelectCellEditor.prototype.getValue = function() {
+//		console.log('SelectCellEditor getValue');
+//		return this.eInput.value;
+//	};
+//
+//	// any cleanup we need to be done here
+//	SelectCellEditor.prototype.destroy = function() {
+//		console.log('SelectCellEditor destroy');
+//		// but this example is simple, no cleanup, we could  even leave this method out as it's optional
+//	};
+//
+//	// if true, then this editor will appear in a popup
+//	SelectCellEditor.prototype.isPopup = function() {
+//		console.log('SelectCellEditor isPopup');
+//		// and we could leave this method out also, false is the default
+//		return false;
+//	};
+//
+//	return SelectCellEditor;
+//}
+
+/**
+ * Text Tooltip component
+ * <pre>
+ *     textTooltip: TextTooltip
+ * </pre>
+ * @author gagamel
+ * @since 2019. 4. 29
+ */
+var getTextTooltip = function() {
+
+	// function to act as a class
+	function TextTooltip() {};
+
+	TextTooltip.prototype.init = function(params) {
+		var eGui = this.eGui = document.createElement('div');
+		var color = params.color || 'white';
+
+		eGui.classList.add('custom-tooltip');
+		eGui.style['background-color'] = color;
+		/*var data = params.api.getRowNode(params.rowIndex).data;
+		eGui.innerHTML =
+			'<p><span class"name">' + data.athlete + '</span></p>' +
+			'<p><span>Country: </span>' + data.country + '</p>' +
+			'<p><span>Total: </span>' + data.total + '</p>';*/
+
+		eGui.innerHTML = '<p><span>' + params.value + '</span>';
+	};
+
+	TextTooltip.prototype.getGui = function() {
+		return this.eGui;
+	};
+
+	return TextTooltip;
+}
+
+
+/**
+ * Text cell editor component
+ * <pre>
+ *     cellEditor: 'textCellEditor'
+ * </pre>
+ * @author gagamel
+ * @since 2019. 4. 30
+ */
+var getTextCellEditor = function() {
+
+	// function to act as a class
+	function TextCellEditor() {};
+
+	TextCellEditor.prototype.init = function(params) {
+		this.eGui = document.createElement('div');
+		this.eGui.innerHTML = '<input'
+			+ ' name="' + params.colDef.field + '"'
+			+ (!gagajf.isNull(params.value) ? ' value="' + params.value + '"' : '')
+			+ (!gagajf.isNull(params.maxlength) ? ' maxlength="' + params.maxlength + '"' : '')
+			+ (!gagajf.isNull(params.validType) ? ' data-valid-type="' + params.validType + '"' : '')
+			+ (!gagajf.isNull(params.onblur) ? ' onblur="' + params.onblur + '"' : '')
+			+ ' style="width: 100%"/>';
+
+		this.eInput = this.eGui.querySelector('input');
+
+		if (params.required) {
+			this.eInput.required = 'required';
+//			this.eInput.classList.add('required');
+		}
+
+//		this.eInput.addEventListener('input', this.inputChanged.bind(this));
+
+//		var that = this;
+//		this.eInput.addEventListener('keypress', function(event) {
+//			if (!isKeyPressedNumeric(event)) {
+//				that.eInput.focus();
+//				if (event.preventDefault) event.preventDefault();
+//			//} else if (that.isKeyPressedNavigation(event)) {
+//			} else if (isNavigationKey(event)) {
+//				event.stopPropagation();
+//			}
+//		});
+
+//		this.eInput.addEventListener('blur', function(event) {
+//			if (params.required) {
+//				if (gagajf.isNull(event.target.value)) {
+//					alert(params.colDef.headerName + ' 을(를) 입력해 주세요.');
+//					that.eInput.focus();
+//					if (event.preventDefault) event.preventDefault();
+//					event.stopPropagation();
+//					return false;
+//				}
+//			}
+//		});
+	}
+
+//	TextCellEditor.prototype.inputChanged = function(event) {
+//		console.log('TextCellEditor.prototype.inputChanged');
+//		console.log(event);
+//
+//		const val = event.target.value;
+//
+////		if (!this.isValid(val)) {
+////			this.eInput.classList.add('invalid-cell');
+////		} else {
+////			this.eInput.classList.remove('invalid-cell');
+////		}
+//
+//		if (this.eInput.required === 'required') {
+//			if (gagajf.isNull(val)) {
+//				alert('을(를) 입력해 주세요.');
+//				return false;
+//			}
+//		}
+//
+//		return true;
+//	}
+
+//	TextCellEditor.prototype.isValid = function(value) {
+//		return value.length <= this.maxLength;
+//	}
+
+	TextCellEditor.prototype.getValue = function() {
+		return this.eInput.value;
+	}
+
+	TextCellEditor.prototype.isCancelAfterEnd = function() {
+//		return !this.isValid(this.eInput.value);
+	}
+
+	TextCellEditor.prototype.getGui = function() {
+		return this.eGui;
+	}
+
+	TextCellEditor.prototype.destroy = function() {
+		this.eInput.removeEventListener('input', this.inputChanged);
+	}
+
+	return TextCellEditor;
+}
+
+
+/**
+ * 공통 그리드 util
+ */
+var gagaAgGrid = {
+	defaultBlank : " - ",
+	eGridDiv : "",
+
+	/**
+	 * 기본 Grid Options을 얻는다.
+	 * @param colDefs - Column Definition
+	 */
+	getGridOptions : function(colDefs) {
+		return {
+			// a default column definition with properties that get applied to every column
+			defaultColDef: {
+				sortable: true, // make every column sortable
+				resizable: true, // make every column resizable
+				filter: 'agTextColumnFilter', // make every column use 'text' filter by default
+				tooltipComponent: 'textTooltip',
+				suppressSizeToFit: true // this columns width to be fixed during 'size to fit' operation.
+			},
+
+//			floatingFilter: true, // display filter region
+
+			// if we had column groups, we could provide default group items here
+			defaultColGroupDef: {
+				//marryChildren: true
+			},
+
+			// define a column type (you can define as many as you like)
+			columnTypes: {
+				boolean: {
+					cellClass: 'text-center',
+					/*cellRenderer: function(params) {
+						return '<input type="checkbox" ' + (params.value ? "checked" : "") + '/>';
+					},*/
+					cellRenderer: 'booleanCellRenderer',
+					cellEditor: 'booleanCellEditor'
+				},
+				numeric: {
+					filter: 'agNumberColumnFilter'
+				},
+				date: {
+					filter: 'agDateColumnFilter',
+					filterParams: {
+						comparator: function(filterLocalDateAtMidnight, cellValue) {
+							// Dates are stored as yyyy/MM/dd
+							// We create a Date object for comparison against the filter date
+							var dateParts = cellValue.split('/');
+							var year = Number(dateParts[0]);
+							var month = Number(dateParts[1]) - 1;
+							var day = Number(dateParts[2]);
+							var cellDate = new Date(day, month, year);
+
+							// Now that both parameters are Date objects, we can compare
+							if (cellDate < filterLocalDateAtMidnight) {
+								return -1;
+							} else if (cellDate > filterLocalDateAtMidnight) {
+								return 1;
+							} else {
+								return 0;
+							}
+						}
+					},
+					suppressMenu: true
+				},
+				measure: {
+					// test 김유중
+//					chartDataType: 'series',
+//					cellClass: 'number',
+					valueFormatter: 'numberCellFormatter',
+					cellRenderer: 'agAnimateShowChangeCellRenderer'
+
+				},
+				numberValue: {
+					enableValue: true,
+					aggFunc: 'sum',
+					editable: true,
+//					valueParser: numberParser
+				},
+				nonEditable: {
+					editable: false
+				}
+			},
+
+			columnDefs: colDefs,
+
+			// Columns
+			suppressAutoSize: false, // suppresses auto-sizing columns
+
+			// Selection
+			//rowSelection: 'single', // 'single' or 'multiple'
+			//suppressRowClickSelection: false,
+			enableRangeSelection: true, // enable Range Selection
+
+			// Rendering & Styling
+			animateRows: true, // enable Row Animation
+
+			// Localization
+			localeText: {
+				noRowsToShow: '조회 결과가 없습니다.'
+			},
+
+			// Overlays
+			suppressLoadingOverlay: true, // disables the 'loading' overlay
+			loadingOverlayComponent: 'customLoadingOverlay',
+			loadingOverlayComponentParams: {
+				loadingMessage: 'Loading... one moment please...'
+			},
+
+			// Scrolling
+			//suppressHorizontalScroll: false,
+
+			statusBar: {
+				statusPanels: [
+					{ statusPanel: 'agTotalRowCountComponent', align: 'left' },
+					{ statusPanel: 'agFilteredRowCountComponent' },
+					{ statusPanel: 'agSelectedRowCountComponent' },
+					{ statusPanel: 'agAggregationComponent' }
+				]
+			},
+
+			// 헤더의 열에 aggFunc명(예, sum, avg 등) 표시 제거
+			suppressAggFuncInHeader: true,
+
+			// Grouping Custom Function
+			aggFuncs: {
+				// this overrides the grids built in sum function
+				'sum': this.sum,
+				'avg': this.avg,
+				'ratio': this.ratio
+			},
+
+			onGridReady: function(params) {
+				params.api.sizeColumnsToFit(); // 자동 맞춤
+			},
+
+			// 창 크기 변경 되었을 때 이벤트
+			onGridSizeChanged: function(params) {
+				params.api.sizeColumnsToFit(); // 자동 맞춤
+			},
+
+//			debug: true,
+
+			onCellValueChanged: function(event) {
+				// if cell is editable and changed value
+				if (event.colDef.editable && event.oldValue != event.newValue) {
+					/*var cellClass = event.colDef.cellClass;
+					event.colDef.cellClass = cellClass ? cellClass + ' modified' : 'modified';
+					var row = this.api.getDisplayedRowAtIndex(event.rowIndex);
+					this.api.redrawRows({ rowNodes: [row] });*/
+				}
+			},
+
+			onCellEditingStopped: function(event) {
+				if (event.colDef.editable && event.data.crud != "C") {
+					event.data.crud = "U";
+					//gridOptions.api.updateRowData({update: [event.data]});
+				}
+			},
+
+			// 총합계, 소계 등을 표시하기 위한 row의 스타일
+			getRowStyle: function(params) {
+				if (params.node.rowPinned) {
+					return {'font-weight': 'bold', 'color': '#721c24', 'background': '#f8d7da'};
+				}
+			},
+
+			components:{
+				customLoadingOverlay: getCustomLoadingOverlay(),
+				booleanCellRenderer: getBooleanCellRenderer(),
+				booleanCellEditor: getBooleanCellEditor(),
+				numericCellEditor: getNumericCellEditor(),
+				datePicker: getDatePicker(),
+				dateTimer: getDateTimer(),
+				textTooltip: getTextTooltip(),
+				textCellEditor: getTextCellEditor(),
+				ComboboxCellRenderer : getComboboxCellRenderer(),
+				ComboboxHeaderComponent : getComboboxHeaderComponent()
+			}
+		};
+	},
+
+	/**
+	 * Create a ag-Grid
+	 * @param gridId - ag-Grid ID
+	 * @param gridOptions - ag-Grid options
+	 * @author gagamel
+	 * @since 2019. 4. 8
+	 */
+	createGrid : function (gridId, gridOptions) {
+		eGridDiv = document.querySelector('#' + gridId);
+		if (typeof gridOptions.rowHeight == "undefined") gridOptions.rowHeight = 32;
+		new agGrid.Grid(eGridDiv, gridOptions);
+	},
+
+	/**
+	 * Progress bar
+	 */
+	showProgressbar : function(isLoading) {
+		if (isLoading) {
+			// Button disabled & progressBar creation
+			$('.btn').each(function(idx) { $(this).attr('disabled', true); });
+			var load_AjaxSubmit = '<div id="load_AjaxSubmit" style="'
+				+ 'background: url(/ux/plugins/gaga/loader.gif); border-style: none; background-repeat: no-repeat; '
+				+ 'position: absolute; top: 45%; left: 50%; width: auto; '
+				+ 'z-index: 101; padding: 16px; margin: 5px;'
+				+ '"></div>';
+			$(eGridDiv).append(load_AjaxSubmit);
+		} else {
+			// Button activated & progressBar remove
+			$('.btn').each(function(idx) { $(this).attr('disabled', false); });
+			$('#load_AjaxSubmit').remove();
+		}
+	},
+	
+	/**
+	 * Hide the status bar of bottom
+	 * gagaAgGrid.createGrid() 함수 사용 후 호출한다.
+	 * <pre>
+	 *     gagaAgGrid.createGrid('gridList', gridOptions);
+	 *     gagaAgGrid.hideStatusBar('gridList');
+	 * </pre>
+	 * @param gridId - ag-Grid ID
+	 * @author gagamel
+	 * @since 2021. 1. 14
+	 */
+	hideStatusBar : function(gridId) {
+		$('#' + gridId + ' .ag-status-bar').hide();
+	},
+
+	/**
+	 * Fetch data using json format.
+	 * <pre>
+	 *     var actionUrl = $('#searchForm').prop('action') + '?' + $('#searchForm').serialize();
+	 *     gagaAgGrid.fetch(actionUrl, gridOptions);
+	 * </pre>
+	 * @param actionUrl - request URL. 필수
+	 * @param gridOptions - ag-Grid options. 필수
+	 * @param formId - form ID. option. 옵션
+	 * @param callbackFn - Callback function. 옵션
+	 * @author gagamel
+	 * @since 2019. 4. 8
+	 */
+	fetch : function(actionUrl, gridOptions, formId, callbackFn) {
+//		gridOptions.api.showLoadingOverlay();
+
+		var _this = this;
+
+		if (typeof(formId) == 'undefined' || gagajf.isNull(formId)) { // formId 값이 없으면
+//			fetch(actionUrl).then(function(response) {
+//				return response.json(); // promise 반환
+//			}).then(function(data) {
+//				gridOptions.api.setRowData(data);
+//				_this.showProgressbar(false);
+//			});
+
+			$.ajax({
+				type : 'GET',
+				url : actionUrl,
+				data : null,
+				dataType : 'json',
+				beforeSend : function(xhr, settings) {
+					// Button disabled & progressBar creation
+					_this.showProgressbar(true);
+				},
+				complete : function() {
+					_this.showProgressbar(false);
+				},
+				success : function(data) {
+					try {
+						gridOptions.api.setRowData(data);
+					} catch (e) {
+						console.log(e);
+						mcxDialog.alert('오류로 인해 처리되지 않았습니다.');
+					}
+
+					if (typeof(callbackFn) == "function") {
+						callbackFn.call(this, data);
+					}
+				},
+				error : function(data) {
+					console.log(data);
+					_this.showProgressbar(false);
+					mcxDialog.alert('오류로 인해 처리되지 않았습니다.');
+				}
+			});
+		} else { // formId 값이 있으면
+			// comma(,) 제거
+			gagajf.removeCommaAtNumberFormattedInput(formId);
+			var jsonData = JSON.stringify($(formId).serializeObject());
+
+			$.ajax({
+				type : 'POST',
+				url : actionUrl,
+				data : jsonData,
+				dataType : 'json',
+				beforeSend : function(xhr, settings) {
+					// dataType: "json"일 때
+					xhr.setRequestHeader('Accept', 'application/json');
+					xhr.setRequestHeader('Content-Type', 'application/json');
+
+					// Button disabled & progressBar creation
+					_this.showProgressbar(true);
+				},
+				complete : function() {
+					_this.showProgressbar(false);
+				},
+				success : function(data) {
+					try {
+						gridOptions.api.setRowData(data);
+					} catch (e) {
+						console.log(e);
+						mcxDialog.alert('오류로 인해 처리되지 않았습니다.');
+					}
+
+					if (typeof(callbackFn) == "function") {
+						callbackFn.call(this, data);
+					}
+				},
+				error : function(data) {
+					console.log(data);
+					_this.showProgressbar(false);
+					mcxDialog.alert('오류로 인해 처리되지 않았습니다.');
+				}
+			});
+
+//			var options = {
+//				method: "POST",
+//				headers: { "Content-Type": "application/json; charset=utf-8" },
+//				body: jsonData
+//			};
+//
+//			fetch(actionUrl, options).then(function(response) {
+//				return response.json(); // promise 반환
+//			}).then(function(data) {
+//				gridOptions.api.setRowData(data);
+//				_this.showProgressbar(false);
+//			});
+		}
+
+		// Button activated & progressBar remove
+
+//		gridOptions.api.hideOverlay();
+	},
+
+	/**
+	 * 그리드의 총건수 가져오기
+	 * <pre>
+	 *     gagaAgGrid.getTotalCount(gridOptions);
+	 * </pre>
+	 * @param gridOptions - ag-Grid options
+	 * @author gagamel
+	 * @since 2019. 5. 8
+	 */
+	getTotalCount : function(gridOptions) {
+		return gridOptions.api.getDisplayedRowCount();
+	},
+
+	/**
+	 * 그리드 컬럼을 보이기/감추기
+	 * <pre>
+	 *     gagaAgGrid.showOrHideColumn(gridOptions, "useYn", false);
+	 * </pre>
+	 * @param gridOptions - ag-Grid options
+	 * @param colKey - column ID
+	 * @param isShow - show or hide (true/false)
+	 * @author gagamel
+	 * @since 2019. 4. 8
+	 */
+	showOrHideColumn : function(gridOptions, colKey, isShow) {
+		gridOptions.columnApi.setColumnVisible(colKey, isShow);
+	},
+
+	/**
+	 * 그리드 컬럼의 Header명을 설정
+	 * <pre>
+	 *     gagaAgGrid.setColumnHeaderName(gridOptions, 'M1', '1월');
+	 * </pre>
+	 * @param gridOptions - ag-Grid options
+	 * @param colKey - column ID or column group ID
+	 * @param headerName - header name
+	 * @author gagamel
+	 * @since 2019. 5. 7
+	 */
+	setColumnHeaderName : function(gridOptions, colKey, headerName) {
+		gridOptions.columnApi.getColumn(colKey).colDef.headerName = headerName;
+//		gridOptions.columnApi.resetColumnState();
+	},
+
+	/**
+	 * 그리드 컬럼 그룹의 Header명을 설정
+	 * <pre>
+	 *     gagaAgGrid.setColumnGroupHeaderName(gridOptions, 'M1', '1월');
+	 * </pre>
+	 * @param gridOptions - ag-Grid options
+	 * @param colKey - column ID
+	 * @param headerName - header name
+	 * @author gagamel
+	 * @since 2019. 5. 7
+	 */
+	setColumnGroupHeaderName : function(gridOptions, colKey, headerName) {
+		gridOptions.columnApi.getColumn(colKey).parent.originalColumnGroup.colGroupDef.headerName = headerName;
+//		gridOptions.columnApi.resetColumnState();
+	},
+
+	/**
+	 * 그리드 내에 변경된 데이터를 배열로 반환한다.
+	 * <pre>
+	 *     var changedData = gagaAgGrid.getChangedData(gridOptions);
+	 * </pre>
+	 * @param gridOptions - ag-Grid options
+	 * @author gagamel
+	 * @since 2019. 4. 8
+	 */
+	getChangedData : function(gridOptions) {
+		// Stop editing
+		gridOptions.api.stopEditing();
+
+		var changedData = [];
+
+		gridOptions.api.forEachNode(function(rowNode, index) {
+			if (rowNode.data.crud == "C" || rowNode.data.crud == "U" || rowNode.data.crud == "D") {
+				rowNode.data.index = index;
+				changedData.push(rowNode.data);
+			}
+		});
+
+		return changedData;
+	},
+
+	/**
+	 * 그리드 첫번째 row에 data를 갖는 행을 추가한다.
+	 * <pre>
+	 *     var data = { cdGb: "", cd: "", cdNm: "", cdChar: "40", cdNum: 1, cdDt: today, dispRk: 1, useYn: "Y" };
+	 *     gagaAgGrid.addRowData(gridOptions, data, "cdGb");
+	 * </pre>
+	 * @param gridOptions - ag-Grid options
+	 * @param data - 추가할 데이터
+	 * @param focusedColKey - Column ID to focus
+	 * @author gagamel
+	 * @since 2019. 4. 9
+	 */
+	addRowData : function(gridOptions, data, focusedColKey) {
+		data.crud = "C";
+
+		gridOptions.api.updateRowData({add: [data], addIndex: 0});
+
+		// Focused cell
+		gridOptions.api.setFocusedCell(0, focusedColKey, null);
+
+		// start editing cell
+		//gridOptions.api.startEditing({rowIndex: 0, colKey: focusedColKey});
+	},
+
+	/**
+	 * 그리드 내에 선택된 데이터를 가져온다
+	 * <pre>
+	 *     gagaAgGrid.selectedRowData(gridOptions);
+	 * </pre>
+	 * @param gridOptions - ag-Grid options
+	 * @author gagamel
+	 * @since 2019. 7. 9
+	 */
+	selectedRowData : function(gridOptions) {
+		return gridOptions.api.getSelectedRows();
+	},
+
+	/**
+	 * 그리드 내에 전체 데이터를 가져온다
+	 * <pre>
+	 *     gagaAgGrid.getAllRowData(gridOptions);
+	 * </pre>
+	 * @param gridOptions - ag-Grid options
+	 * @author qkwlstktma
+	 * @since 2019. 7. 25
+	 */
+	getAllRowData : function(gridOptions){
+		var data = [];
+
+		gridOptions.api.forEachNode(function(rowNode, index) {
+			data.push(rowNode.data);
+		});
+
+		return data;
+	},
+
+	/**
+	 * 그리드 내에 선택된 데이터를 삭제하고 배열에 담아 반환한다.
+	 * 실제 삭제되지 않고 해당 행이 보이지 않게 된다.
+	 * <pre>
+	 *     gagaAgGrid.removeRowData(gridOptions);
+	 * </pre>
+	 * @param gridOptions - ag-Grid options
+	 * @param isShow - Row 보이기(true/false. 디폴트는 보이는 것으로). 옵션
+	 * @author gagamel
+	 * @since 2019. 4. 9
+	 */
+	removeRowData : function(gridOptions, isShow) {
+		var selectedData = gridOptions.api.getSelectedRows();
+
+		var removedData = [];
+
+		selectedData.forEach(function(item, index) {
+			if (item.crud == 'C') {
+				gridOptions.api.updateRowData({remove: [item]});
+			} else {
+				removedData.push(item);
+				if (typeof(isShow) != 'undefined' && !isShow) {
+					gridOptions.api.updateRowData({remove: [item]});
+				}
+			}
+		});
+
+		return removedData;
+	},
+
+	/**
+	 * 그리드 내에 고정된(pinned) top 또는 bottom에 데이터 표시
+	 * <pre>
+	 *     var data = {
+	 *         sellDt: '총합계',
+	 *         orderQty: 100, orderAmt: 10000,
+	 *     };
+	 *     gagaAgGrid.setPinnedRowData(gridOptions, data, 'top');
+	 * </pre>
+	 * @param gridOptions - ag-Grid options. 필수
+	 * @param data - 고정된 row에 보일 데이터. 필수
+	 * @param topBottom - top/bottom. 옵션
+	 * @author gagamel
+	 * @since 2019. 9. 7
+	 */
+	setPinnedRowData : function(gridOptions, data, topBottom) {
+		if (typeof(topBottom) == 'undefined' || topBottom == 'top') {
+			gridOptions.api.setPinnedTopRowData([data]);
+		} else {
+			gridOptions.api.setPinnedBottomRowData([data]);
+		}
+	},
+
+	checkRequired : function(gridOptions, validItem) {
+		var isInvalid = true;
+
+		for (var i = 0; i < gridOptions.columnDefs.length; i++) {
+			var column = gridOptions.columnDefs[i];
+
+			if (typeof(column.cellEditorParams) == 'undefined')
+				continue;
+
+			if (typeof(column.cellEditorParams.required) == 'undefined')
+				continue;
+
+			if (!column.cellEditorParams.required)
+				continue;
+
+			if (!gagajf.isNull(validItem[column.field]))
+				continue;
+
+			mcxDialog.alert(column.headerName + '은[는] 필수 입력 항목입니다.');
+
+			// Focused cell
+			gridOptions.api.setFocusedCell(validItem.index, column.field, null);
+
+			isInvalid = false;
+			break;
+		}
+
+		return isInvalid;
+	},
+
+	validation : function(gridOptions, validData) {
+		var isInvalid = true;
+
+		validData.every(function(item, index) {
+			if (gagaAgGrid.checkRequired(gridOptions, item))
+				return true;
+
+			isInvalid = false;
+			return false;
+		});
+
+		return isInvalid;
+	},
+
+	/*// 그리드 값 넣기
+	setRowData : function (gridOptions, rowData) {
+		//$('#'+gridDiv).children().remove();
+		gridOptions.api.setRowData(rowData);
+	},*/
+
+	/**
+	 * mappings(코드목록배열) 중에 명칭만을 배열로 구성해 반환한다.
+	 * <pre>
+	 *     cellEditorParams: { values: gagaAgGrid.extractValues(mappings) }
+	 * </pre>
+	 * @param mappings - 코드목록배열. 필수
+	 *     예) { "10":"항공(AIR)", "20":"해운(OCEAN)", ... }
+	 * @author gagamel
+	 * @since 2019. 4. 7
+	 */
+	extractValues : function(mappings) {
+		return Object.keys(mappings);
+	},
+
+	/**
+	 * mappings(코드목록배열) 중에 키(key)에 메핑되는 값을 반환한다.
+	 * <pre>
+	 *     valueFormatter: function (params) { return gagaAgGrid.lookupValue(mappings, params.value); }
+	 * </pre>
+	 * @param mappings - 코드목록배열. 필수
+	 *     예) { "10":"항공(AIR)", "20":"해운(OCEAN)", ... }
+	 * @param key - 키. 필수
+	 * @author gagamel
+	 * @since 2019. 4. 7
+	 */
+	lookupValue : function(mappings, key) {
+		return mappings[key];
+	},
+
+	/**
+	 * mappings(코드목록배열) 중에 name(콤보박스 option text)에 메핑되는 Key를 반환한다.
+	 * <pre>
+	 *     valueParser: function (params) { return gagaAgGrid.lookupKey(mappings, params.newValue); }
+	 * </pre>
+	 * @param mappings - 코드목록배열. 필수
+	 *     예) { "10":"항공(AIR)", "20":"해운(OCEAN)", ... }
+	 * @param name - 콤보박스 option text. 필수
+	 * @author gagamel
+	 * @since 2019. 4. 7
+	 */
+	lookupKey : function(mappings, name) {
+		for (var key in mappings) {
+			if (mappings.hasOwnProperty(key)) {
+				if (name === mappings[key]) {
+					return key;
+				}
+			}
+		}
+	},
+
+	/**
+	 * value를 yyyy-MM-dd 형식으로 반환
+	 * <pre>
+	 *     cellRenderer: function (params) { return gagaAgGrid.toDateFormat(params.value); }
+	 * </pre>
+	 * @param value - 일자 (yyyyMMdd 형식)
+	 * @author gagamel
+	 * @since 2019. 4. 8
+	 */
+	toDateFormat : function(value) {
+		if (gagajf.isNull(value))
+			return "";
+
+		return value.replaceAll("/", "").replaceAll("-", "").toDate("YYYYMMDD").format("YYYY-MM-DD");
+	},
+
+	/**
+	 * value를 yyyy-MM-dd HH:mm:ss 형식으로 반환
+	 * <pre>
+	 *     cellRenderer: function (params) { return gagaAgGrid.toDateTimeFormat(params.value); }
+	 * </pre>
+	 * @param value - 일자 (yyyyMMddHHmmss 형식)
+	 * @param format - 일자표현식
+	 * @author gagamel
+	 * @since 2019. 6. 7
+	 */
+	toDateTimeFormat : function(value, format) {
+		if (gagajf.isNull(value))
+			return "";
+
+		var rst = "";
+
+		if (format == "YYYYMMDD") {
+			rst = value.replace(/[^0-9]/g, "").toDate("YYYYMMDDHHmmss").format("YYYYMMDD");
+		} else if (format == "hh:mm:ss") {
+			rst = value.replace(/[^0-9]/g, "").toDate("YYYYMMDDHHmmss").format("hh:mm:ss");
+		} else if (format == "yyyy-MM-dd") {
+			rst = value.replace(/[^0-9]/g, "").toDate("YYYYMMDDHHmmss").format("YYYY-MM-DD");
+		} else {
+			rst = value.replace(/[^0-9]/g, "").toDate("YYYYMMDDHHmmss").format("YYYY-MM-DD HH:mm:ss");
+		}
+
+		return rst;
+	},
+
+	/**
+	 * value가 숫자 타입일 때 comma(,)를 붙여 반환
+	 * <pre>
+	 *     cellRenderer: function (params) { return gagaAgGrid.toAddComma(params.value); }
+	 * </pre>
+	 * @param value - 숫자
+	 * @author gagamel
+	 * @since 2019. 9. 8
+	 */
+	toAddComma : function(value) {
+		if (gagajf.isNull(value) || typeof value != 'number')
+			return '';
+
+		return value.addComma();
+	},
+
+	/**
+	 * value가 실수 타입일 때 comma(,)를 붙여 반환 소수점 n까지 반환
+	 * <pre>
+	 *     cellRenderer: function (params) { return gagaAgGrid.toFixed(params.value, n); }
+	 * </pre>
+	 * @param value - 숫자
+	 * @author ldh
+	 * @since 2019. 12. 10
+	 */
+	toFixed : function(value, n) {
+		if (gagajf.isNull(value) || typeof value != 'number')
+			return '';
+
+		return value.toFixed(n);
+	},
+
+	/*
+	convertTime : function (param){
+		if (!param.value) {
+			return this.defaultBlank;
+		} else {
+			return moment(param.value, "YYYYMMDDHHmmss").format("HH:mm");
+		}
+	},
+
+	convertTimestamp : function(param) { // 8자리 문자열을 날짜로 변환
+		if (!param.value) {
+			return this.defaultBlank;
+		} else {
+			return moment(param.value, "YYYYMMDDHHmmss").format("YYYY-MM-DD HH:mm:ss");
+		}
+	},
+
+	formatDate : function(param) { // long 값을 날짜로 변환
+		if (!param.value) {
+			return this.defaultBlank;
+		} else {
+			return moment(parseInt(param.value)).format("YYYY-MM-DD");
+		}
+	},
+
+	formatTimestamp : function(param) { // long 값을 날짜로 변환
+		if (!param.value) {
+			return this.defaultBlank;
+		} else {
+			return moment(parseInt(param.value)).format("YYYY-MM-DD HH:mm:ss");
+		}
+	},
+
+	numberOnly : function(param){
+		if (!param.value || param.value =='')
+			return '';
+
+		if (typeof param.value === 'string') {
+			return param.value.replace(/[^0-9\.]+/g, '');
+		}
+
+		return CommonGrid.formatCurrency(param);
+	},
+
+	formatCurrency : function (param){
+		if (!param.value && param.value != "0") {
+			return this.defaultBlank;
+		}
+
+		return parseInt(param.value).format();
+	},
+
+	formatNumber : function(param){
+		if (!param.value && param.value != "0") {
+			return this.defaultBlank;
+		}
+
+		return numberFormatPoint2Digits(param.value);
+	},
+
+	convertCommon : function(codes, param){
+		if (!codes) return " - ";
+		for (var i = 0; i < codes.length;i++) {
+			if (param.value == codes[i].name) {
+				return codes[i].value;
+			}
+		}
+		return this.defaultBlank;
+	},
+
+	getCode : function(codes, value) {
+		for (var i = 0; i < codes.length; i++) {
+			if (value == codes[i].value) {
+				return codes[i].name;
+			}
+		}
+
+		return "";
+	},
+
+	checkMobile : function(gridOpts){
+		// 모바일 브라우저 가로크기 체크
+		if (document.body.clientWidth < 800) {
+			if (gridOpts){
+				gridOpts.columnDefs.forEach(function(el) {
+					el.pinned = null;
+					if(el.children){
+						el.children.forEach(function(subEl) {
+							subEl.pinned = null;
+						});
+					}
+				});
+			}
+		}
+	},
+
+	makeTopGrid : function(gridMainOpts, rowData, params) {
+		var colsSum = [];
+
+		gridMainOpts.columnDefs.forEach(function(coldefs, index) {
+			if (coldefs.children) {
+				coldefs.children.forEach(function(child, index) {
+					if (child.isSum && child.isSum == true){
+						var temp = {};
+						temp[child.field] = 0;
+						temp["field"] = child.field;
+						colsSum.push(temp);
+					}
+				});
+			} else {
+				if (coldefs.isSum && coldefs.isSum == true) {
+					var temp = {};
+					temp[coldefs.field] = 0;
+					temp["field"] = coldefs.field;
+					colsSum.push(temp);
+				}
+			}
+		});
+
+		rowData.forEach(function(rowvalue, index) {
+			colsSum.forEach(function(value, index) {
+				if (rowvalue[value.field] && !rowvalue[value.field].isNaN)
+					value[value.field] += rowvalue[value.field];
+			});
+		});
+
+		var resultSum = {}
+		colsSum.forEach(function(cValue, index) {
+			resultSum[cValue.field] = cValue[cValue.field];
+		});
+
+		if (params && params.length > 0) {
+			params.forEach(function(value, index) {
+				resultSum[value.field] = value.value;
+			});
+		}
+
+		resultSum['top'] = true;
+
+		var resultArray = [];
+		resultArray[0] = resultSum;
+		gridMainOpts.api.setPinnedTopRowData(resultArray);
+	}*/
+
+	// sum function has no advantage over the built in sum function.
+
+	// it's shown here as it's the simplest form of aggregation and
+	// showing it can be good as a starting point for understanding
+	// how the aggregation functions work.
+	sum : function(values) {
+		var result = 0;
+		values.forEach(function(value) {
+			if (typeof value === 'number') {
+				result += value;
+			}
+		});
+		return result;
+	},
+
+	// Average function
+	avg : function(values) {
+		var result = 0;
+		var cnt = 0;
+		values.forEach(function(value) {
+			if (typeof value === 'number') {
+				result += value;
+				cnt++;
+			}
+		});
+		return gagaAgGrid.toAddComma(Number(gagaAgGrid.toFixed(result / cnt)));
+	},
+
+	// Ratio function
+	ratio : function(values) {
+		var value1 = 0;
+		var value2 = 0;
+
+		values.forEach(function(value) {
+			if (value && value.value1) {
+				value1 += value.value1;
+			}
+			if (value && value.value2) {
+				value2 += value.value2;
+			}
+		});
+
+		return gagaAgGrid.setRatio(value1, value2);
+	},
+
+	/**
+	 * 2개의 값으로 비율값을 계산해 설정한다.
+	 * <pre>
+	 *     valueGetter: function (params) { if (!params.node.group) return gagaAgGrid.setRatio(params.data.payAmt, params.data.sellTagAmt); }
+	 * </pre>
+	 * @param value1 - 분자
+	 * @param value2 - 분모
+	 * @author gagamel
+	 * @since 2020. 5. 11
+	 */
+	setRatio : function(value1, value2) {
+		return {
+			value1: value1,
+			value2: value2,
+			toString: function() {
+				return value1 && value2 ? gagaAgGrid.toFixed(value1 / value2 * 100, 2) : 0;
+			}
+		};
+	},
+
+	// csv 파일 export
+	exportToCsv : function(title, gridOptions) {
+		var params = {
+			skipHeader: false,
+			skipFooters: false,
+			columnGroups: true,
+			skipGroups: false,
+			skipPinnedTop: false,
+			skipPinnedBottom: false,
+			allColumns: true,
+			onlySelected: false,
+			fileName: title + moment().format("YYYYMMDDHHmmss") + '.csv'
+		};
+
+		gridOptions.api.exportDataAsCsv(params);
+	},
+
+	/**
+	 * 그리드 데이터를 엑셀로 내보내기
+	 * <pre>
+	 * 		gagaAgGrid.exportToExcel('주문목록', gridOptionsOrderList, columnKeys);
+	 * 		or
+	 * 		gagaAgGrid.exportToExcel('주문목록', gridOptionsOrderList, columnKeys, true);
+	 * </pre>
+	 * @param title - 엑셀의 타이틀
+	 * @param gridOptions - Grid options
+	 * @param columnKeys - Column keys (출력하고자 하는 칼럼 정의)
+	 * @param isSelected - true/false
+	 * @author gagamel
+	 * @since 2019. 6. 7
+	 */
+	exportToExcel  : function(title, gridOptions, isSelected) {
+		var sColumnKeys = (typeof(columnKeys) == 'undefined') ? [] : columnKeys;
+		var bOnlySelected = (typeof(isSelected) == 'undefined') ? false : isSelected;
+
+		var params = {
+			columnKeys: sColumnKeys,
+			onlySelected: bOnlySelected, // true: Only export selected rows
+			fileName : title + "_" + new Date().format("YYYYMMDDHHmmss"),
+			sheetName: "DATA"/* ,
+			customHeader: title,
+			customFooter: "Copyright(c) 2019 TSIT, All rights reserved." */
+		};
+
+		gridOptions.excelStyles = [
+			{
+				id: 'dateFormat',
+				dataType: 'dateTime',
+				numberFormat: {
+					format: 'YYYY-MM-DD;@'
+				}
+			},
+			{
+				id: 'textFormat',
+				dataType: 'string'
+			}
+		]
+
+		gridOptions.api.exportDataAsExcel(params);
+	},
+
+}
+
+/*var gDefaultTxt = '';
+var gIsDisplayCode = true;
+var ComboboxCellRenderer = function(defaultTxt, isDisplayCode) {
+	if (defaultTxt) gDefaultTxt = defaultTxt;
+	if (isDisplayCode) gIsDisplayCode = isDisplayCode;
+}
+
+ComboboxCellRenderer.prototype.init = function(params) {
+	this.eGui = document.createElement('span');
+	if (!gagajf.isNull(params.value)) {
+		var checked = (params.value == "Y" || params.value == "1") ? "checked" : "";
+		var input = document.createElement('input');
+		input.type = "checkbox";
+		input.checked = checked;
+		input.value = params.value;
+		input.addEventListener('click', function (event) {
+			if (params.value == "Y") params.value = "N";
+			else if (params.value == "N") params.value = "Y";
+			else if (params.value == "1") params.value = "0";
+			else if (params.value == "0") params.value = "1";
+			//checked input value has changed, perform your update here
+			console.log("addEventListener params.value: "+ params.value);
+		});
+		this.eGui.innerHTML = '';
+		this.eGui.appendChild(input);
+	}
+};
+
+ComboboxCellRenderer.prototype.getGui = function() {
+	return this.eGui;
+};*/
+
+// cellRenderer: 'ComboboxCellRenderer'
+var getComboboxCellRenderer = function() {
+	function ComboboxCellRenderer() {};
+
+	ComboboxCellRenderer.prototype.init = function(params) {
+		this.eGui = document.createElement('span');
+		var span1 = document.createElement('span');
+		var span2 = document.createElement('span');
+		var span3 = document.createElement('span');
+		this.eGui.classList = 'cellChecked'
+		span1.classList = 'ag-selection-checkbox';
+		var check = 'params.data.' + params.colDef.field;
+		$(".allChecked").closest('div').each(function(){
+			if($(this).attr('col-id')==params.colDef.field){
+				if($(this).children('.allChecked').children('.ag-selection-checkbox').children('.ag-icon-checkbox-unchecked').hasClass('ag-hidden')){
+					eval(check+' = "Y"');
+				}else if($(this).children('.allChecked').hasClass('uncheckedAll')){
+					eval(check+' = "N"');
+				}
+			}
+		});
+		if(eval(check)=='Y'){
+			span2.classList = 'ag-icon ag-icon-checkbox-checked';
+			span3.classList = 'ag-icon ag-icon-checkbox-unchecked ag-hidden';
+		}else{
+			eval(check+' = "N"');
+			span2.classList = 'ag-icon ag-icon-checkbox-checked ag-hidden';
+			span3.classList = 'ag-icon ag-icon-checkbox-unchecked';
+		}
+		this.eGui.addEventListener('click', function (event) {
+			if(span2.classList.contains('ag-hidden')){
+				eval(check+' = "Y"');
+				span2.classList.remove('ag-hidden');
+				span3.classList.add('ag-hidden');
+				$(".allChecked").closest('div').each(function(){
+					if($(this).attr('col-id')==params.colDef.field){
+						$(this).children('.allChecked').removeClass('uncheckedAll');
+					}
+				});
+			}else{
+				eval(check+' = "N"');
+				span2.classList.add('ag-hidden');
+				span3.classList.remove('ag-hidden');
+				$(".allChecked").closest('div').each(function(){
+					if($(this).attr('col-id')==params.colDef.field){
+						$(this).children('.allChecked').children('.ag-selection-checkbox').children('.ag-icon-checkbox-checked').addClass('ag-hidden');
+						$(this).children('.allChecked').children('.ag-selection-checkbox').children('.ag-icon-checkbox-unchecked').removeClass('ag-hidden');
+					}
+				});
+			}
+		});
+		span1.appendChild(span2);
+		span1.appendChild(span3);
+		this.eGui.innerHTML = '';
+		this.eGui.appendChild(span1);
+	};
+
+	ComboboxCellRenderer.prototype.getGui = function() {
+		return this.eGui;
+	};
+
+	return ComboboxCellRenderer;
+}
+
+// headerComponent : 'ComboboxHeaderComponent'
+var getComboboxHeaderComponent = function() {
+	function ComboboxHeaderComponent() {};
+
+	ComboboxHeaderComponent.prototype.init = function(params) {
+		this.eGui = document.createElement('span');
+		var span1 = document.createElement('span');
+		var span2 = document.createElement('span');
+		var span3 = document.createElement('span');
+		this.eGui.classList = 'allChecked'
+		span1.classList = 'ag-selection-checkbox';
+		span2.classList = 'ag-icon ag-icon-checkbox-checked ag-hidden';
+		span3.classList = 'ag-icon ag-icon-checkbox-unchecked';
+		this.eGui.addEventListener('click', function (event) {
+			if(span2.classList.contains('ag-hidden')){
+				this.classList.remove('uncheckedAll');
+				span2.classList.remove('ag-hidden');
+				span3.classList.add('ag-hidden');
+				$(".ag-icon-checkbox-checked").closest('div').each(function(){
+					if($(this).attr('col-id')==params.column.colId){
+						$(this).children('.cellChecked').children('.ag-selection-checkbox').children('.ag-icon-checkbox-checked').addClass('ag-hidden');
+						$(this).children('.cellChecked').children('.ag-selection-checkbox').children('.ag-icon-checkbox-unchecked').removeClass('ag-hidden');
+						$(this).children('.cellChecked').click();
+					}
+				});
+			}else{
+				this.classList.add('uncheckedAll');
+				span2.classList.add('ag-hidden');
+				span3.classList.remove('ag-hidden');
+				$(".ag-icon-checkbox-checked").closest('div').each(function(){
+					if($(this).attr('col-id')==params.column.colId){
+						$(this).children('.cellChecked').children('.ag-selection-checkbox').children('.ag-icon-checkbox-checked').removeClass('ag-hidden');
+						$(this).children('.cellChecked').children('.ag-selection-checkbox').children('.ag-icon-checkbox-unchecked').addClass('ag-hidden');
+						$(this).children('.cellChecked').click();
+					}
+				});
+			}
+		});
+
+		span1.appendChild(span2);
+		span1.appendChild(span3);
+		span1.append(" "+params.displayName);
+		this.eGui.innerHTML = '';
+		this.eGui.appendChild(span1);
+	};
+
+	ComboboxHeaderComponent.prototype.getGui = function() {
+		return this.eGui;
+	};
+
+	return ComboboxHeaderComponent;
+}

+ 68 - 0
src/main/webapp/ux/plugins/gaga/gaga.alert.js

@@ -0,0 +1,68 @@
+/*
+ * Alert, Confirm Java Script written by gagamel.
+ *
+ * Copyright (c) 2010 gagamel
+ * Dual licensed under GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2019-07-01 $
+ */
+
+var gagaAlert = {
+	obj : {
+		message : '',
+		callback : '',
+		cancelCallback : ''
+	},
+	
+	show : function(type, message) {
+		var liTag = '<ul class="popup modal" data-width="350" style="min-width: 350px;">\n';
+		liTag += '		<li class="mdPopContent">' + message + '</li>\n';
+		liTag += '		<li class="mdPopBtnB aR">\n';
+		liTag += '			<button id="okBtn" type="button" class="btn btn-primary btn-lg" onclick="gagaAlert.ok();">확인</button>\n';
+		
+		if (type == 'confirm') {
+			liTag += '			<button type="button" class="btn btn-dark btn-lg" onclick="gagaAlert.cancel();">취소</button>\n';
+		}
+		
+		liTag += '		</li>\n';
+		liTag += '	</ul>\n';
+
+		if ($('#customAlert').length == 0) {
+			var tag = '<div class="popupWrap bgTrans" id="customAlert" style="z-index:900;">\n';
+			tag += liTag;
+			tag += '</div>';
+			$('body').append(tag);
+		} else {
+			$('#customAlert').append(liTag);
+		}
+		uifnMpopup('customAlert');
+		$("#okBtn").attr("tabindex", -1).focus();
+	},
+	
+	alert : function(message,callback) {
+		this.obj.callback = null;
+		gagaAlert.show('alert', message);
+		this.obj.callback = callback;
+	},
+	
+	confirm : function(message, callback, cancelCallback) {
+		gagaAlert.show('confirm', message);
+		this.obj.callback = callback;
+		this.obj.cancelCallback = cancelCallback;
+	},
+	
+	ok : function() {
+		uifnPopClose('customAlert');
+		if (typeof(this.obj.callback) != undefined && typeof(this.obj.callback) == 'function' && this.obj.callback != null ) {
+			this.obj.callback('abc');
+		}
+	},
+	
+	cancel : function() {
+		uifnPopClose('customAlert');
+		if (typeof(this.obj.cancelCallback) != undefined && typeof(this.obj.cancelCallback) == 'function' && this.obj.cancelCallback != null ) {
+			this.obj.cancelCallback('abc');
+		}
+	}
+	
+}

+ 379 - 0
src/main/webapp/ux/plugins/gaga/gaga.common.js

@@ -0,0 +1,379 @@
+/*
+ * Common Java Script written by gagamel.
+ *
+ * Copyright (c) 2010 gagamel
+ * Dual licensed under GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2010-05-19 $
+ */
+
+/**
+ * @type   : prototype_function
+ * @access : public
+ * @desc   : 자바스크립트의 내장 객체인 String 객체에 trim 메소드를 추가한다. trim 메소드는 스트링의 앞과 뒤에
+ *           있는 white space 를 제거한다.
+ * <pre>
+ *     var str = " abcde "
+ *     str = str.trim();
+ * </pre>
+ * 위의 예에서 str는 "abede"가 된다.
+ * @return : trimed String.
+ * @author : gagamel
+ */
+String.prototype.trim = function() {
+	return this.replace(/(^\s*)|(\s*$)/g, "");
+};
+
+/**
+ * @type   : prototype_function
+ * @access : public
+ * @desc   : 자바스크립트의 내장 객체인 String 객체에 replaceAll 메소드를 추가한다. replaceAll 메소드는
+ *           스트링 내에 있는 특정 스트링을 다른 스트링으로 모두 변환한다. String 객체의 replace 메소드를 확장한 것이다.
+ * <pre>
+ *     var str = "abcde"
+ *     str = str.replaceAll("cd", "xx");
+ * </pre>
+ * 위의 예에서 str는 "abxxe"가 된다.
+ * @sig    : oldStr, newStr
+ * @param  : oldStr required 바뀌어야 될 기존의 스트링
+ * @param  : newStr required 바뀌어질 새로운 스트링
+ * @return : replaced String.
+ * @author : gagamel
+ */
+String.prototype.replaceAll = function(oldStr, newStr) {
+	var rStr = oldStr;
+
+	rStr = rStr.replace(/\\/g, "\\\\");
+	rStr = rStr.replace(/\^/g, "\\^");
+	rStr = rStr.replace(/\$/g, "\\$");
+	rStr = rStr.replace(/\*/g, "\\*");
+	rStr = rStr.replace(/\+/g, "\\+");
+	rStr = rStr.replace(/\?/g, "\\?");
+	rStr = rStr.replace(/\./g, "\\.");
+	rStr = rStr.replace(/\(/g, "\\(");
+	rStr = rStr.replace(/\)/g, "\\)");
+	rStr = rStr.replace(/\|/g, "\\|");
+	rStr = rStr.replace(/\,/g, "\\,");
+	rStr = rStr.replace(/\{/g, "\\{");
+	rStr = rStr.replace(/\}/g, "\\}");
+	rStr = rStr.replace(/\[/g, "\\[");
+	rStr = rStr.replace(/\]/g, "\\]");
+	rStr = rStr.replace(/\-/g, "\\-");
+
+	var re = new RegExp(rStr, "g");
+
+	return this.replace(re, newStr);
+};
+
+/**
+ * @type   : prototype_function
+ * @access : public
+ * @desc   : 자바스크립트의 내장 객체인 String 객체에 toDate 메소드를 추가한다. toDate 메소드는 날짜를 표현하는
+ *           스트링 값을 자바스크립트의 내장 객체인 Date 객체로 변환한다.
+ * <pre>
+ *     var date = "20020305".toDate();
+ *     or
+ *     var date = "20020305".toDate("YYYYMMDD");
+ *     or
+ *     var date = "2002.03.05".toDate("YYYY.MM.DD");
+ * </pre>
+ * 위의 예에서 date 변수는 실제로 2002년 3월 5일을 표현하는 Date 오브젝트를 가르킨다.
+ * @sig    : [pattern]
+ * @param  : pattern optional Date를 표현하고 있는 현재의 String을 pattern으로 표현한다. (default : YYYYMMDD)
+ * <pre>
+ *     # syntax
+ *
+ *       YYYY : year(4자리)
+ *       YY   : year(2자리)
+ *       MM   : month in year(number)
+ *       DD   : day in month
+ *       HH   : hour in day (0~23)
+ *       mm   : minute in hour
+ *       ss   : second in minute
+ *       SS   : millisecond in second
+ *
+ *     <font color=red>주의)</font> YYYY(YY)는 반드시 있어야 한다. YYYY(YY) 만 사용할 경우는 1월 1일을 기준으로
+ *     하고 YYYY와 MM 만사용할 경우는 1일을 기준으로 한다.
+ * </pre>
+ * @return : 변환된 Date Object.
+ * @author : gagamel
+ */
+String.prototype.toDate = function(pattern) {
+	var index = -1;
+	var year;
+	var month;
+	var day;
+	var hour = 0;
+	var min  = 0;
+	var sec  = 0;
+	var ms   = 0;
+	var newDate;
+
+	if (pattern == null) {
+		pattern = "YYYYMMDD";
+	}
+
+	if ((index = pattern.indexOf("YYYY")) == -1 ) {
+		index = pattern.indexOf("YY");
+		year = "20" + this.substr(index, 2);
+	} else {
+		year = this.substr(index, 4);
+	}
+
+	if ((index = pattern.indexOf("MM")) != -1 ) {
+		month = this.substr(index, 2);
+	} else {
+		month = 1;
+	}
+
+	if ((index = pattern.indexOf("DD")) != -1 ) {
+		day = this.substr(index, 2);
+	} else {
+		day = 1;
+	}
+
+	if ((index = pattern.indexOf("HH")) != -1 ) {
+		hour = this.substr(index, 2);
+	}
+
+	if ((index = pattern.indexOf("mm")) != -1 ) {
+		min = this.substr(index, 2);
+	}
+
+	if ((index = pattern.indexOf("ss")) != -1 ) {
+		sec = this.substr(index, 2);
+	}
+
+	if ((index = pattern.indexOf("SS")) != -1 ) {
+		ms = this.substr(index, 2);
+	}
+
+	newDate = new Date(year, month - 1, day, hour, min, sec, ms);
+
+	if (month > 12) {
+		newDate.setFullYear(year + 1);
+	} else {
+		newDate.setFullYear(year);
+	}
+
+	return newDate;
+};
+
+/**
+ * @type   : prototype_function
+ * @object : Date
+ * @access : public
+ * @desc   : 자바스크립트의 내장 객체인 Date 객체에 format 메소드를 추가한다. format 메소드는 Date 객체가 가진 날짜를
+ *           지정된 포멧의 스트링으로 변환한다.
+ * <pre>
+ *     var dateStr = new Date().format("YYYYMMDD");
+ *
+ *     참고 : Date 오브젝트 생성자들 - dateObj = new Date()
+ *                                   - dateObj = new Date(dateVal)
+ *                                   - dateObj = new Date(year, month, date[, hours[, minutes[, seconds[,ms]]]])
+ * </pre>
+ * 위의 예에서 오늘날짜가 2002년 3월 5일이라면 dateStr의 값은 "20020305"가 된다.
+ * default pattern은 "YYYYMMDD"이다.
+ * @sig    : [pattern]
+ * @param  : pattern optional 변환하고자 하는 패턴 스트링. (default : YYYYMMDD)
+ * <pre>
+ *     # syntax
+ *
+ *       YYYY : hour in am/pm (1~12)
+ *       MM   : month in year(number)
+ *       MON  : month in year(text)  예) "January"
+ *       mon  : short month in year(text)  예) "Jan"
+ *       DD   : day in month
+ *       DAY  : day in week  예) "Sunday"
+ *       day  : short day in week  예) "Sun"
+ *       hh   : hour in am/pm (1~12)
+ *       HH   : hour in day (0~23)
+ *       mm   : minute in hour
+ *       ss   : second in minute
+ *       SS   : millisecond in second
+ *       a    : am/pm  예) "AM"
+ * </pre>
+ * @return : Date를 표현하는 변환된 String.
+ * @author : gagamel
+ */
+Date.prototype.format = function(pattern) {
+	var GLB_MONTH_IN_YEAR = new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December");
+	var GLB_DAY_IN_WEEK   = new Array("Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday");
+
+	var year      = this.getFullYear();
+	var month     = this.getMonth() + 1;
+	var day       = this.getDate();
+	var dayInWeek = this.getDay();
+	var hour24    = this.getHours();
+	var hour12    = (hour24 > 12) ? (hour24 - 12) : hour24;
+	var min       = this.getMinutes();
+	var sec       = this.getSeconds();
+	var YYYY = "" + year;
+	var YY   = YYYY.substr(2);
+	var MM   = (("" + month).length == 1) ? "0" + month : "" + month;
+	var MON  = GLB_MONTH_IN_YEAR[month-1];
+	var DD   = (("" + day).length == 1) ? "0" + day : "" + day;
+	var DAY  = GLB_DAY_IN_WEEK[dayInWeek];
+	var HH   = (("" + hour24).length == 1) ? "0" + hour24 : "" + hour24;
+	var hh   = (("" + hour12).length == 1) ? "0" + hour12 : "" + hour12;
+	var mm   = (("" + min).length == 1) ? "0" + min : "" + min;
+	var ss   = (("" + sec).length == 1) ? "0" + sec : "" + sec;
+	var a    = (a == 0) ? "AM" : "PM";
+
+	var dateStr;
+
+	if (typeof(pattern) == "undefined") {
+		dateStr = "YYYYMMDD";
+	} else {
+		dateStr = pattern;
+	}
+
+	dateStr = dateStr.replace(/a/g,    a);
+	dateStr = dateStr.replace(/YYYY/g, YYYY);
+	dateStr = dateStr.replace(/YY/g,   YY);
+	dateStr = dateStr.replace(/MM/g,   MM);
+	dateStr = dateStr.replace(/MON/g,  MON);
+	dateStr = dateStr.replace(/DD/g,   DD);
+	dateStr = dateStr.replace(/DAY/g,  DAY);
+	dateStr = dateStr.replace(/hh/g,   hh);
+	dateStr = dateStr.replace(/HH/g,   HH);
+	dateStr = dateStr.replace(/mm/g,   mm);
+	dateStr = dateStr.replace(/ss/g,   ss);
+
+	return dateStr;
+};
+
+/**
+ * @type   : prototype_function
+ * @object : Date
+ * @access : public
+ * @desc   : 현재 Date 객체의 날짜보다 이후날짜를 가진 Date 객체를 리턴한다.
+ *           예를 들어 내일 날짜를 얻으려면 다음과 같이 하면 된다.
+ * <pre>
+ *     var oneDayAfter = new Date.after(0, 0, 1);
+ * </pre>
+ * @sig    : [years[, months[, days[, hours[, minutes[, seconds[, mss]]]]]]]
+ * @param  : years   optional 이후 년수
+ * @param  : months  optional 이후 월수
+ * @param  : days   optional 이후 일수
+ * @param  : hours   optional 이후 시간수
+ * @param  : minutes optional 이후 분수
+ * @param  : seconds optional 이후 초수
+ * @param  : mss     optional 이후 밀리초수
+ * @return : 이후날짜를 표현하는 Date 객체
+ * @author : gagamel
+ */
+Date.prototype.after = function(years, months, days, hours, miniutes, seconds, mss) {
+	if (years == null) years = 0;
+	if (months == null) months = 0;
+	if (days == null) days = 0;
+	if (hours == null) hours = 0;
+	if (miniutes == null) miniutes = 0;
+	if (seconds == null) seconds = 0;
+	if (mss == null) mss = 0;
+	return new Date(this.getFullYear() + years,
+			this.getMonth() + months,
+			this.getDate() + days,
+			this.getHours() + hours,
+			this.getMinutes() + miniutes,
+			this.getSeconds() + seconds,
+			this.getMilliseconds() + mss
+	);
+};
+
+/**
+ * @type   : prototype_function
+ * @object : Date
+ * @access : public
+ * @desc   : 현재 Date 객체의 날짜보다 이전날짜를 가진 Date 객체를 리턴한다.
+ *           예를 들어 어제 날짜를 얻으려면 다음과 같이 하면 된다.
+ * <pre>
+ *     var oneDayBefore = new Date.before(0, 0, 1);
+ * </pre>
+ * @sig    : [years[, months[, days[, hours[, minutes[, seconds[, mss]]]]]]]
+ * @param  : years   optional 이전으로 돌아갈 년수
+ * @param  : months  optional 이전으로 돌아갈 월수
+ * @param  : days   optional 이전으로 돌아갈 일수
+ * @param  : hours   optional 이전으로 돌아갈 시간수
+ * @param  : minutes optional 이전으로 돌아갈 분수
+ * @param  : seconds optional 이전으로 돌아갈 초수
+ * @param  : mss     optional 이전으로 돌아갈 밀리초수
+ * @return : 이전날짜를 표현하는 Date 객체
+ * @author : gagamel
+ */
+Date.prototype.before = function(years, months, days, hours, miniutes, seconds, mss) {
+	if (years == null) years = 0;
+	if (months == null) months = 0;
+	if (days == null) days = 0;
+	if (hours == null) hours = 0;
+	if (miniutes == null) miniutes = 0;
+	if (seconds == null) seconds  = 0;
+	if (mss == null) mss = 0;
+	return new Date(this.getFullYear() - years,
+			this.getMonth() - months,
+			this.getDate() - days,
+			this.getHours() - hours,
+			this.getMinutes() - miniutes,
+			this.getSeconds() - seconds,
+			this.getMilliseconds() - mss
+	);
+};
+
+/**
+ * @type   : function
+ * @access : public
+ * @desc   : 자바스크립트의 내장 객체인 Number 객체에 addComma 메소드를 추가한다.
+ *           값에서 콤마(,)를 추가한다.
+ * <pre>
+ *     -12345678.123.addComma();
+ * </pre>
+ * @return : 콤마(,)가 추가된 값
+ * @since  : 2014/10/15
+ * @author : gagamel
+ */
+Number.prototype.addComma = function(decimalPosition) {
+	var num = this;
+
+	if (typeof(decimalPosition) == 'undefined') {
+		return num.toLocaleString();
+	} else {
+		return num.toLocaleString("ko-KR", {maximumFractionDigits:decimalPosition});
+	}
+}
+
+/**
+ * @type   : function
+ * @access : public
+ * @desc   : 자바스크립트의 내장 객체인 String 객체에 addComma 메소드를 추가한다.
+ *           스트링에서 콤마(,)를 추가한다.
+ * <pre>
+ *     "-12345678.123".addComma();
+ * </pre>
+ * @return : 콤마(,)가 추가된 값
+ * @since  : 2014/10/15
+ * @author : gagamel
+ */
+String.prototype.addComma = function(decimalPosition) {
+	var num = Number(this);
+
+	if (typeof(decimalPosition) == 'undefined') {
+		return num.addComma();
+	} else {
+		return num.addComma(decimalPosition);
+	}
+}
+
+/**
+ * @type   : prototype_function
+ * @access : public
+ * @desc   : 자바스크립트의 내장 객체인 String 객체에 removeComma 메소드를 추가한다.
+ *           스트링에서 콤마(,)를 제거한다.
+ * <pre>
+ *     "-123,456,789.123".removeComma();
+ * </pre>
+ * @return : 콤마(,)가 제거된 스트링
+ * @author : gagamel
+ */
+String.prototype.removeComma = function() {
+	return this.replace(/,/gi,"");
+}

+ 377 - 0
src/main/webapp/ux/plugins/gaga/gaga.dx5.js

@@ -0,0 +1,377 @@
+/*
+ * dextupload X5(https://www.dextsolution.com) Common Java Script written by gagamel.
+ *
+ * Copyright (c) 2019 gagamel
+ * Dual licensed under GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2019-06-26 $
+ * 
+ * 사용 예)
+ * 		// HTML 태그는 다음과 같은 구조로 되어 있어야 하며
+ * 		// id 명칭은 dextIndex, dext5-container 가 필요하다.
+ * 		<ul class="dexterTable">
+ * 			<li class="dexterNo" id="dx5Index">
+ * 				<div>1</div>
+ * 				<div>2</div>
+ * 			</li>
+ * 			<li>
+ * 				<div id="dext5-container" style="width: 100%; height: 450px;"></div>
+ * 			</li>
+ * 		</ul>
+ * 
+ * 		<button type="button" class="btn btn-base btn-sm" id="btnAddFiles">파일 추가</button>
+ * 		<button type="button" class="btn btn-default btn-sm" onclick="gagaDx5.deleteChooseFile();">선택 삭제</button>
+ * 		<button type="button" class="btn btn-default btn-sm" onclick="gagaDx5.deleteAllFiles();">전체 삭제</button>
+ * 		<button type="button" class="btn btn-info btn-sm" onclick="gagaDx5.moveFile(true);">위로 이동</button>
+ * 		<button type="button" class="btn btn-info btn-sm" onclick="gagaDx5.moveFile(false);">아래로 이동</button>
+ * 		<button type="button" class="btn btn-base btn-sm" onclick="gagaDx5.previewImage();">이미지 보기</button>
+ * 		<button type="button" class="btn btn-base btn-sm" onclick="gagaDx5.uploadFiles();">업로드/수정</button>
+ *
+ * 		// Import할 자바스크립트 파일
+ * 		<script type="text/javascript" src="/dx5/dextuploadx5-configuration.js"></script>
+ *		<script type="text/javascript" src="/dx5/dextuploadx5.js"></script>
+ * 		<script type="text/javascript" src="/ux/plugins/gaga/gaga.dx5.js"></script>
+ * 
+ * 		<script type="text/javascript">
+ * 			// Dextupload X5 생성. 파일추가 버튼 ID는 22라인에 정의된 btnAddFiles과 동일해야 함
+ * 			$(function() { gagaDx5.createDX5("dext5", "btnAddFiles"); });
+ * 
+ * 			// Dextupload X5 생성 후 호출되는 이벤트
+ * 
+ * 			var onDX5Created = function(id) {
+ * 				var actionUrl = '/dextupload/goods/thumbnail/image/save'
+ * 						+ '/' + $('#goodsImgForm input[name=goodsCode]').val()
+ * 						+ '/' + $('#goodsImgForm input[name=goodsColor]').val();
+ * 				gagaDx5.onDX5Created(actionUrl, goodsImgList);
+ * 			}
+ * 
+ * 			// Dextupload X5 에러 시 호출되는 이벤트
+ * 			var onDX5Error = function(id, code, msg) {
+ * 				alert(id + " => " +  code + "\n" + msg);
+ * 			}
+ * 
+ * 			// Dextupload X5 업로드 성공 시 호출되는 이벤트
+ * 			var onDX5UploadCompleted = function(id) {
+ * 				
+ * 			}
+ * 
+ * 			// Dextupload X5 이미지 등록이 완료된 후 호출되는 이벤트
+ * 			var onDX5ItemsAdded = function(id, count) {
+ * 				gagaDx5.resortDX5FileList();
+ * 			}
+ *		</script>
+ */
+
+var gagaDx5 = {
+	dxId : "",
+	previewUrl : "",
+	dxProdGbn : "",
+	obj : {
+		dx5Index : $('#dx5Index'),
+		dx5CardArea: $('#dx5CardArea')
+	},
+	
+	/**
+	 * Create a dx5
+	 * @param dx5Id - dx5 ID
+	 * @param fileAddBtnId - 파일추가버튼 ID
+	 * @author gagamel
+	 * @since 2019. 6. 26
+	 */
+	createDX5 : function(dx5Id, fileAddBtnId, previewDomain, prodGbn) {
+		dxId = dx5Id;
+		previewUrl = previewDomain;
+		dxProdGbn = prodGbn;
+		dx5.create({
+			mode: "multi", id: dx5Id, parentId: dx5Id + "-container",
+			btnFile: fileAddBtnId/*, btnFolder: "btnAddFolder"*/
+		});
+	},
+	
+	/**
+	 * Dextupload X5 생성 후 호출되는 이벤트
+	 * @param dx5Id - dx5 ID
+	 * @param actionUrl - Upload URL
+	 * @author gagamel
+	 * @since 2019. 6. 26
+	 */
+	onDX5Created : function(actionUrl, imgList) {
+		var dx = dx5.get(dxId);
+		
+		dx.setUploadURL(dx5.canonicalize(actionUrl));
+		
+		// 기존 업로드된 파일을 dx5에 그리기
+		$.each(imgList, function(idx, item) {
+			//if (item.extmallImgYn == 'Y'){	//외부몰이미지구분용
+			//	dx.addVirtualFile({ vindex : item.sysImgNm, name : item.sysImgNm, size : 20000});
+			//}else{
+				dx.addVirtualFile({ vindex : item.sysImgNm, name : item.sysImgNm, size : 10000, url : item.sysImgUrl});
+			//}
+		});
+		
+		// 이미지 항목 높이 조절
+		dx.setUIStyle({ itemHeight: parseInt(24, 10) });
+		
+		// 헤더 높이 조절
+		dx.setUIStyle({ headerHeight: parseInt(28, 10) });
+		
+		// 이미지 미리보기 세팅
+		var totCnt = dx.getTotalVirtualFileCount();
+		
+		// DX5 파일 리스트 순번 재정렬
+		gagaDx5.resortDX5FileList();
+		
+		// DX5 이미지 미리보기 영역 리스트 재정렬
+		gagaDx5.drawDX5ImagePreview();
+		
+		// 이미지 미리보기 적용
+		dx.setPreviewEnable(true);
+		
+		// 이미지 미리보기 이벤트로 적용
+		dx.setPreviewMethod(2);
+	},
+	
+	// 파일 선택 삭제
+	deleteChooseFile : function() {
+		var dx = dx5.get(dxId);
+		
+		dx.removeSelected();
+		
+		// DX5 파일 리스트 순번 재정렬
+		gagaDx5.resortDX5FileList();
+		
+		// DX5 이미지 미리보기 영역 리스트 재정렬
+		gagaDx5.drawDX5ImagePreview(dxId);
+	},
+	
+	// 파일 전체 삭제
+	deleteAllFiles : function() {
+		var dx = dx5.get(dxId);
+		
+		dx.removeAll();
+		
+		// DX5 파일 리스트 순번 재정렬
+		gagaDx5.resortDX5FileList();
+		
+		// DX5 이미지 미리보기 영역 리스트 재정렬
+		gagaDx5.drawDX5ImagePreview();
+	},
+	
+	// 파일 위/아래로 이동
+	moveFile : function(isUp) {
+		var dx = dx5.get(dxId);
+		
+		var totCnt = dx.getTotalItemCount();
+		var selCnt = 0;
+		var selIdx = 0;
+		
+		// 2개 이상 선택했는지 확인
+		for (var i = 0; i < totCnt; i++) {
+			if (dx.isSelectedByIndex(i)) {
+				selIdx = i;
+				selCnt++;
+			}
+		}
+		
+		if (selCnt > 1) {
+			alert("파일이 2개 이상 선택되었습니다.");
+			return;
+		}
+		
+		var ti = parseInt(selIdx, 10);
+		
+		if (isNaN(ti)) {
+			alert("파일의 인덱스(순서)가 필요합니다.");
+			return;
+		}
+		
+		if (isUp)
+			dx.moveItemUp(ti);
+		else
+			dx.moveItemDown(ti);
+		
+		// DX5 이미지 미리보기 영역 리스트 재정렬
+		gagaDx5.drawDX5ImagePreview(dxId);
+	},
+	
+	// 이미지 미리보기
+	previewImage : function() {
+		var dx = dx5.get(dxId);
+		
+		var indices = dx.getSelectedIndices();
+		
+		if (indices.length > 1 || indices.length == 0) {
+			alert("이미지 파일을 한 개만 선택해 주세요.");
+			return;
+		}
+		
+		var idx = indices[0];
+		ti = parseInt(idx, 10);
+		
+		if (isNaN(ti)) {
+			alert("파일의 인덱스(순서)가 필요합니다.");
+			return;
+		}
+
+		dx.preview(ti);
+	},
+	
+	// DX5 파일 리스트 순번 재정렬
+	resortDX5FileList : function() {
+		var dx = dx5.get(dxId);
+		
+		this.obj.dx5Index.html('');
+		
+		var totCnt = dx.getTotalItemCount();
+		var tag = "";
+		
+		for (var i = 0; i < totCnt; i++) {
+			if (i < 16) {
+				tag += '<div>' + (i + 1) + '</div>';
+			} else {
+				break;
+			}
+		}
+		
+		this.obj.dx5Index.html(tag);
+	},
+	
+	// DX5 이미지 미리보기 영역 리스트 재정렬
+	drawDX5ImagePreview : function() {
+		var dx = dx5.get(dxId);
+		
+		this.obj.dx5CardArea.html('');
+		
+		var totCnt = dx.getTotalItemCount();
+		
+		var tag = '';
+		
+		for (var i = 0 ; i < totCnt; i++) {
+			var item = dx.getItemByIndex(i);
+			//console.log("dx.item=>" + item);
+			//console.log("dx.item.name=>" + item.name);
+			//console.log("dx.item.extmallImgYn=>" + item.extmallImgYn);
+			if (item.type == "FILE") {
+				continue;
+			}
+			if (dxProdGbn =="goods"){
+				tag += '<div class="imgCard">\n';
+				//if (item.size > 10000){
+				//	tag += '<div class="imgCard"  style="border:2px solid #8597eb">\n';	
+				//}else{
+				//	tag += '<div class="imgCard">\n';
+				//}
+				tag += '	<button type="button" class="cardClose" onclick="gagaDx5.deletePreviewImage(\'' + i + '\');">닫기</button>\n';
+				tag += '	<ul>\n';
+				tag += '		<li>\n';
+				tag += '			<img src="' + previewUrl + '/' + item.url + '/' + item.name + '" width="70" height="70"/>\n';
+				//if (item.size > 10000){
+				//	tag += '			<img src="' + previewUrl + '/upload/goods/type5/' + item.name + '" width="70" height="70"/>\n';
+				//}else{
+				//	tag += '			<img src="' + previewUrl + '/upload/goods/type4/' + item.name + '" width="70" height="70"/>\n';
+				//}
+				tag += '		</li>\n';
+				tag += '		<li>이미지' + (i + 1) + '</li>\n';
+				tag += '	</ul>\n';
+				tag += '	<p>' + item.name + '</p>\n';
+				tag += '</div>\n';
+			//}else if (dxProdGbn =="magazine"){
+			//	tag += '<div class="imgCard">\n';
+			//	tag += '	<button type="button" class="cardClose" onclick="gagaDx5.deletePreviewImage(\'' + i + '\');">닫기</button>\n';
+			//	tag += '	<ul>\n';
+			//	tag += '		<li>\n';
+			//	tag += '			<img src="' + previewUrl + '/upload/michaa/magazine/' + item.name + '" width="70" height="70"/>\n';
+			//	tag += '		</li>\n';
+			//	tag += '		<li>이미지' + (i + 1) + '</li>\n';
+			//	tag += '	</ul>\n';
+			//	tag += '	<p>' + item.name + '</p>\n';
+			//	tag += '</div>\n';
+			}else if (dxProdGbn =="lookbook"){
+				tag += '<div class="imgCard">\n';
+				tag += '	<button type="button" class="cardClose" onclick="gagaDx5.deletePreviewImage(\'' + i + '\');">닫기</button>\n';
+				tag += '	<ul>\n';
+				tag += '		<li>\n';
+				tag += '			<img src="' + previewUrl + '/lookbook/' + item.name + '" width="70" height="70"/>\n';
+				tag += '		</li>\n';
+				tag += '		<li>이미지' + (i + 1) + '</li>\n';
+				tag += '	</ul>\n';
+				tag += '	<p>' + item.name + '</p>\n';
+				tag += '</div>\n';	
+			//}else if (dxProdGbn =="collection"){
+			//	tag += '<div class="imgCard">\n';
+			//	tag += '	<button type="button" class="cardClose" onclick="gagaDx5.deletePreviewImage(\'' + i + '\');">닫기</button>\n';
+			//	tag += '	<ul>\n';
+			//	tag += '		<li>\n';
+			//	tag += '			<img src="' + previewUrl + '/upload/michaa/collection/COLLECTION_AD/' + item.name + '" width="70" height="70"/>\n';
+			//	tag += '		</li>\n';
+			//	tag += '		<li>이미지' + (i + 1) + '</li>\n';
+			//	tag += '	</ul>\n';
+			//	tag += '	<p>' + item.name + '</p>\n';
+			//	tag += '</div>\n';
+			//}else if (dxProdGbn =="mdlookbook"){
+			//	tag += '<div class="imgCard">\n';
+			//	tag += '	<button type="button" class="cardClose" onclick="gagaDx5.deletePreviewImage(\'' + i + '\');">닫기</button>\n';
+			//	tag += '	<ul>\n';
+			//	tag += '		<li>\n';
+			//	tag += '			<img src="' + previewUrl + '/michaa/campaign/md/' + item.name + '" width="70" height="70"/>\n';
+			//	tag += '		</li>\n';
+			//	tag += '		<li>이미지' + (i + 1) + '</li>\n';
+			//	tag += '	</ul>\n';
+			//	tag += '	<p>' + item.name + '</p>\n';
+			//	tag += '</div>\n';		
+			}
+		}
+		
+		this.obj.dx5CardArea.html(tag);
+	},
+	
+	// 파일 업로드
+	uploadFiles : function() {
+		var dx = dx5.get(dxId);
+		
+		var totCnt = dx.getTotalItemCount(); // 모든 항목 개수
+		
+		// 업로드 할 때 필요한  데이터를 보내지 못해 MetaData로 세팅 후 보냄
+		for (var i = 0; i < totCnt; i++) {
+			var item = dx.getItemByIndex(i);
+			if (item.type == "FILE") { // MetaData는 FILE type만 등록 가능
+				dx.setMetaDataByIndex(i, "name", item.name);
+			}
+		}
+		
+		if (dx.hasUploadableItems()) { // 업로드할 파일이 있으면
+			dx.upload("AUTO");
+		} else {
+			gagaDx5.uploadAfterProcess(dxId);
+		}
+	},
+	
+	// 업로드 후처리
+	uploadAfterProcess : function() {
+		var dx = dx5.get(dxId);
+		
+		// Biz단에서 반드시 구현해야 함
+		fnUploadAfterProcess(dxId, dx.getResponses()[0]);
+	},
+	
+	// 미리보기 영역에서 삭제 시
+	deletePreviewImage : function(idx) {
+		var dx = dx5.get(dxId);
+		
+		var ti = parseInt(idx, 10);
+		
+		if (isNaN(ti)) {
+			alert("파일의 인덱스(순서)가 필요합니다.");
+			return;
+		}
+		
+		dx.removeByIndex(ti);
+		
+		// DX5 파일 리스트 순번 재정렬
+		gagaDx5.resortDX5FileList();
+		
+		// DX5 이미지 미리보기 영역 리스트 재정렬
+		gagaDx5.drawDX5ImagePreview();
+	}
+	
+}

+ 227 - 0
src/main/webapp/ux/plugins/gaga/gaga.paging.js

@@ -0,0 +1,227 @@
+/*
+ * Pagination Common Java Script written by gagamel.
+ *
+ * Copyright (c) 2019 gagamel
+ * Dual licensed under GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2019-07-21 $
+ * 
+ * 사용 예)
+ * 		// HTML 태그는 다음과 같은 구조로 되어 있어야 한다.
+ * 		<div class="tablePaging" id="pagination">
+ * 			<a href="#pageNo=1">1</a>
+ * 			<a href="#pageNo=2">2</a>
+ * 			...
+ * 		</div>
+ * 
+ * 		// Import할 자바스크립트 파일
+ * 		<script type="text/javascript" src="/ux/plugins/gaga/gaga.paging.js"></script>
+ * 
+ * 		<script type="text/javascript">
+ * 			// Initialize a pagination
+ * 			gagaPaging.init('searchForm', fnSearchGoodsQnaCallback, 'pagination');
+ * 			
+ *			// Load data
+ * 			gagaPaging.load($('#searchForm input[name=pageNo]').val(), $('#searchForm select[name=pageSize]').val());
+ * 
+ * 			var fnSearchGoodsQnaCallback = function(result) {
+ * 				// Handle Data
+ * 				...
+ * 				
+ * 				// Create pagination
+ * 				gagaPaging.createPagination(result.goodsAsk.pageable);
+ * 			}
+ *		</script>
+ */
+
+var gagaPaging = {
+
+	oFormId : '',
+	oCallbackFn : '',
+	oPaginationId : '',
+	nPageSize : 50,
+	nPageUnit : 10,
+	
+	/**
+	 * Initialize a pagination
+	 * @param formId - Form ID. 검색조건 폼 ID
+	 * @param callbackFn - Callback function. 데이터 조회 후 처리하는 콜백함수
+	 * @param paginationId - Pagination ID. 페이징을 구성하는 div 태그 ID
+	 * @param pageSize - 조회할 데이터 row수. 옵션
+	 * @param pageUnit - 그룹핑 페이지 단위. 옵션
+	 * 사용 예)
+	 * 		// Initialize a pagination
+	 * 		gagaPaging.init('searchForm', fnSearchGoodsQnaCallback, 'pagination');
+	 * 		or
+	 * 		gagaPaging.init('searchForm', fnSearchGoodsQnaCallback, 'pagination', 50);
+	 * 		or
+	 * 		gagaPaging.init('searchForm', fnSearchGoodsQnaCallback, 'pagination', 50, 10);
+	 */
+	init : function(formId, callbackFn, paginationId, pageSize, pageUnit) {
+		oFormId = formId;
+		oCallbackFn = callbackFn;
+		oPaginationId = paginationId;
+		if (typeof(pageSize) != 'undefined') nPageSize = pageSize;
+		if (typeof(pageUnit) != 'undefined') nPageUnit = pageUnit;
+	},
+	
+	/**
+	 * Create a pagination
+	 * @param pageable - page 정보
+	 */
+	createPagination : function(pageable) {
+		// Generate pagination
+		var html;
+		if (gagajf.isNull(pageable.generatedPagination)) {
+			html = gagaPaging.generatedPagination(pageable);
+		} else {
+			html = pageable.generatedPagination;
+		}
+		$('#' + oPaginationId).html(html);
+		// Bind event
+		gagaPaging.bind();
+	},
+	
+	/**
+	 * Bind the pagination's event
+	 * Paging 처리 시 다음과 같이 지정
+	 * 사용 예)
+	 * 		// HTML 태그는 다음과 같은 구조로 되어 있어야 한다.
+	 * 		<div class="tablePaging" id="pagination">
+	 * 			<a href="#pageNo=1">1</a>
+	 * 			<a href="#pageNo=2">2</a>
+	 * 			...
+	 * 		</div>
+	 */
+	bind : function() {
+		$('#' + oPaginationId + ' a').on('click', function(e) {
+			var params = $(this).attr('href');
+			params = params.replace(/^.*#/, '');
+			
+			var pageNo = 1;
+			
+			if (!gagajf.isNull(params)) {
+				pageNo = params.split("=")[1];
+			}
+			
+			gagaPaging.load(pageNo);
+			
+			return false;
+		});
+	},
+	
+	/**
+	 * Load data and call a callback function
+	 * JSON 형태로 보내고 받는 후 콜백함수에서 데이터를 처리한다.
+	 * @param pageNo - 조회할 페이지번호
+	 */
+	load : function(pageNo) {
+		$('#' + oFormId + ' input[name=pageNo]').val(pageNo);
+		$('#' + oFormId + ' input[name=pageSize]').val(nPageSize);
+		
+		// comma(,) 제거
+		gagajf.removeCommaAtNumberFormattedInput('#' + oFormId);
+		var jsonData = JSON.stringify($('#' + oFormId).serializeObject());
+		
+		$.ajax({
+			type : 'POST',
+			url : $('#' + oFormId).prop('action'),
+			data : jsonData,
+			dataType : 'JSON',
+			beforeSend : function(xhr, settings) {
+				// dataType: "json"일 때
+				xhr.setRequestHeader('Accept', 'application/json');
+				xhr.setRequestHeader('Content-Type', 'application/json');
+				
+				// Button disabled & progressBar creation
+				gagajf.showProgressbar(true);
+			},
+			complete : function() {
+				// Button abled & progressBar remove
+				gagajf.showProgressbar(false);
+			},
+			success : function(result) {
+				try {
+					if (!gagajf.isNull(result.error.message)) {
+						mcxDialog.alert(result.error.message);
+						return;
+					}
+				} catch(e) {
+					// Do nothing
+				}
+				
+				if (typeof(oCallbackFn) == 'function') {
+					oCallbackFn.call(this, result);
+				}
+			},
+			error : function(result) {
+				console.log(result);
+				mcxDialog.alert('오류로 인해 처리되지 않았습니다.');
+			}
+		});
+	} ,
+	generatedPagination : function (pageable) {
+		let pageNo = pageable.pageNo;
+		let pageSize = pageable.pageSize;
+		let pageUnit = pageable.pageUnit;
+		let totalCount = pageable.totalCount;
+		let firstCount = (getPageGroup() - 1) * pageable.pageUnit + 1;
+		let loopCount = firstCount + pageUnit;
+		if (loopCount > getTotalPage()) {
+			loopCount = Number(getTotalPage() + 1);
+		}
+		
+		var pageTag = '';
+		if (!(firstCount == 1 && loopCount == 1)) {
+			if (pageNo == 1) {
+				pageTag += "<a class=\"arrow\" href=\"#\"><i class=\"fa fa-angle-double-left\" alt=\"맨처음\"></i></a>\n";
+			} else {
+				pageTag += "<a class=\"arrow\" href=\"#pageNo=1\"><i class=\"fa fa-angle-double-left\" alt=\"맨처음\"></i></a>\n";
+			}
+
+			if (getPageGroup() == 1) {
+				pageTag += "<a class=\"arrow\" href=\"#\"><i class=\"fa fa-angle-left\" alt=\"이전페이지\"></i></a>\n";
+			} else {
+				pageTag += "<a class=\"arrow\" href=\"#pageNo="+ (getPageGroup()-1) * pageUnit + "\"><i class=\"fa fa-angle-left\" alt=\"이전페이지\"></i></a>\n"
+			}
+
+			for (let i = firstCount; i < loopCount; i++) {
+				if (pageNo == i) {
+					pageTag += "<a class=\"num on\" href=\"#\">"+ i + "</a>\n";
+				} else {
+					pageTag += "<a class=\"num\" href=\"#pageNo=" + i + "\">" + i +"</a>\n"
+				}
+			}
+			
+			if (loopCount <= (Number(getTotalPage() + 1))) {
+				//if (getTotalPage() <= pageUnit) {
+				//	pageTag += "<a class=\"arrow\" href=\"#\"><i class=\"fa fa-angle-right\" alt=\"다음페이지\"></i></a>\n";
+				//	pageTag += "<a class=\"arrow\" href=\"#\"><i class=\"fa fa-angle-double-right\" alt=\"맨마지막\"></i></a>\n";
+				//} else {
+					if (getTotalPage() > loopCount){
+						pageTag += "<a class=\"arrow\" href=\"#pageNo=" + (getPageGroup() * pageUnit + 1) + "\"><i class=\"fa fa-angle-right\" alt=\"다음페이지\"></i></a>\n";	
+					}else{
+						pageTag += "<a class=\"arrow\" href=\"#pageNo=" + getTotalPage() + "\"><i class=\"fa fa-angle-right\" alt=\"다음페이지\"></i></a>\n";
+					}
+					pageTag += "<a class=\"arrow\" href=\"#pageNo=" + getTotalPage() + "\"><i class=\"fa fa-angle-double-right\" alt=\"맨마지막\"></i></a>\n";
+				//}
+			}
+		}
+
+		function getPageGroup() {
+			return parseInt((pageNo-1) / pageUnit + 1 );
+		}
+
+		function getTotalPage() {
+			let totalPage = parseInt(totalCount / pageSize);
+			if (totalCount % pageSize > 0) {
+				totalPage++;
+			}
+			return totalPage;
+		}
+
+		return pageTag;
+	}
+
+}
+

+ 73 - 0
src/main/webapp/ux/plugins/gaga/gaga.se2.js

@@ -0,0 +1,73 @@
+/*
+ * Smart Editor Java Script written by gagamel.
+ *
+ * Copyright (c) 2010 gagamel
+ * Dual licensed under GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2019-07-02 $
+ * 
+ * 사용 예)
+ * 		// HTML 태그는 textarea로 구성
+ * 		// id는 gagaSe2.createSmartEditor 함수 호출 시에 넘겨줘야 한다.
+ * 		<textarea name="contentKorWeb" id="contentKorWeb" rows="5" cols="50" style="width: 100%; height: 400px;"></textarea>
+ * 
+ * 		// Import할 자바스크립트 파일
+ * 		<script type="text/javascript" src="/se2/js/service/HuskyEZCreator.js?v=2019070211"></script>
+ *		<script type="text/javascript" src="/ux/plugins/gaga/gaga.se2.js?v=2019070222"></script>
+ *
+ * 		<script type="text/javascript">
+ * 			// Get a SmartEditor2 options
+ * 			var se2Options = gagaSe2.getEditorOptions();
+ * 
+ * 			$(document).ready(function() {
+ * 				// Create a SmartEditor2
+ * 				gagaSe2.createSmartEditor(se2Options, 'contentKorWeb');
+ * 			});
+ * 		</script>
+ */
+
+var gagaSe2 = {
+	obj : {
+		oEditors : []
+	},
+	
+	/**
+	 * Get a Smart Editor options
+	 */
+	getEditorOptions : function() {
+		return {
+				oAppRef: this.obj.oEditors,
+				sSkinURI: '/se2/SmartEditor2Skin.html',
+				htParams : {
+					bUseToolbar : true, // 툴바사용
+					bUseVerticalResizer : true, // 입력창크기조절바사용
+					bUseModeChanger : true, // 모드탭(Editor|HTML|TEXT)사용
+//					bSkipXssFilter : true, // client-side xss filter 무시
+//					aAdditionalFontList : aAdditionalFontSet, // 추가 글꼴 목록
+					fOnBeforeUnload : function() {
+					}
+				},
+				fCreator: 'createSEditor2'
+		};
+	},
+	
+	/**
+	 * Create a Smart Editor
+	 * @param editorId - 에디터 ID
+	 */
+	createSmartEditor : function(editorOptions, editorId) {
+		editorOptions.elPlaceHolder =editorId;
+		nhn.husky.EZCreator.createInIFrame(editorOptions);
+	},
+	
+	/**
+	 * 스마트에디터에 입력한 내용이 editorId로 지정된 textarea에 설정되고,
+	 * textarea에 설정된 값을 반환한다.
+	 * @param editorId - 에디터 ID
+	 */
+	getContents : function(editorId) {
+		this.obj.oEditors.getById[editorId].exec("UPDATE_CONTENTS_FIELD", []);
+		return document.getElementById(editorId).value;
+	}
+	
+}

+ 129 - 0
src/main/webapp/ux/plugins/gaga/gaga.smarteditor.js

@@ -0,0 +1,129 @@
+/*
+ * Smart Editor Java Script written by gagamel.
+ *
+ * Copyright (c) 2010 gagamel
+ * Dual licensed under GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2019-07-02 $
+ *
+ * 사용 예)
+ * 		// HTML 태그는 textarea로 구성
+ * 		// id는 gagaSe.createSmartEditor 함수 호출 시에 넘겨줘야 한다.
+ * 		<textarea name="contentKorWeb" id="contentKorWeb" rows="5" cols="50" style="width: 100%; height: 400px;"></textarea>
+ *
+ * 		// Import할 자바스크립트 파일
+ * 		<script type="text/javascript" src="/smartEditor/js/HuskyEZCreator.js?v=2019070303" charset="utf-8"></script>
+ * 		<script type="text/javascript" src="/ux/plugins/gaga/gaga.smarteditor.js?v=2019070301"></script>
+ *
+ * 		<script type="text/javascript">
+ * 			// Get a SmartEditor options
+ * 			var seOptions = gagaSe.getEditorOptions();
+ *
+ * 			$(document).ready(function() {
+ * 				// Create a SmartEditor
+ * 				gagaSe.createSmartEditor(seOptions, 'contentKorWeb');
+ * 			});
+ * 		</script>
+ */
+
+var gagaSe = {
+	obj : {
+		oEditors : []
+	},
+
+	/**
+	 * Get a Smart Editor options
+	 */
+	getEditorOptions : function() {
+		return {
+				oAppRef: this.obj.oEditors,
+				sSkinURI: '/smartEditor/SEditorSkin.html',
+				htParams : {
+					bUseToolbar : true, // 툴바사용
+					bUseVerticalResizer : true, // 입력창크기조절바사용
+					bUseModeChanger : true, // 모드탭(Editor|HTML|TEXT)사용
+					fOnBeforeUnload : function() {
+					}
+				},
+
+				fCreator: 'createSEditorInIFrame'
+		};
+	},
+
+	/**
+	 * Create a Smart Editor
+	 * @param editorId - 에디터 ID
+	 */
+	createSmartEditor : function(editorOptions, editorId) {
+		editorOptions.elPlaceHolder = editorId;
+		nhn.husky.EZCreator.createInIFrame(editorOptions);
+	},
+
+	/**
+	 * 스마트에디터에 입력한 내용이 editorId로 지정된 textarea에 설정되고,
+	 * textarea에 설정된 값을 반환한다.
+	 * @param editorId - 에디터 ID
+	 */
+	getContents : function(editorId) {
+		this.obj.oEditors.getById[editorId].exec("UPDATE_IR_FIELD", []);
+		return document.getElementById(editorId).value;
+	},
+
+	setContents : function(editorId, content) {
+		var newContent = "";
+		if (content != null) {
+			newContent = content.replaceAll("&lt;", "<").replaceAll("&gt;",">");
+		}
+
+		try {
+			this.obj.oEditors.getById[editorId].exec("SET_IR", [newContent]);
+		} catch(e) {
+			$('#'+editorId).val(newContent);
+		}
+	},
+
+	/**
+	 * @type   : function
+	 * @access : public
+	 * @desc   : 스마트에디터 유효성 검사
+	 * <pre>
+	 *     gagaSe.getContents('contentKorWeb');
+	 *     getContents 를 먼저 해줘야 textarea에 스마트에디터 내용이 들어간다.
+	 *     if (!gagaSe.validationCheck($('#registerwebKorViewYn') , $('#contentKorWeb'))) return;
+	 *     또는
+	 *     if (!gagaSe.validationCheck($('#detailForm input:checkbox[name=webKorViewYn]') , $('#detailForm textarea[name=contentKorWeb]'))) return;
+	 * </pre>
+	 * @param  : elTarget - 국가/디바이스 스마트에디터(필수)
+	 * @param  : elCheck - 국가/디바이스 체크박스(옵션)
+	 * @since  : 2019/08/21
+	 * @author : rladbwnd5
+	 */
+	validationCheck : function(elTarget, elCheck) {
+		if (elTarget.prop('tagName') != 'TEXTAREA') {
+			mcxDialog.alert('파라메터 입력 오류');
+			console.log('입력하신 파라메터는 ' + elTarget.prop('tagName') + '입니다. textarea를  입력하세요.');
+			return false;
+		}
+
+		var checkStr = $(elTarget).val().replaceAll("<p>", "").replaceAll("</p>","").replaceAll("<br>","").replaceAll("<span style=\"white-space:pre\">","").replaceAll("&nbsp;","").replaceAll("</span>","");
+		var checked = $(elCheck).is(":checked") ? true : false;
+
+		if (checked) {
+			if (gagajf.isNull(checkStr)) {
+				var TargetNm = $(elCheck)[0].labels[0].textContent;
+				mcxDialog.alert(TargetNm + '에 체크된 내용을 입력하세요.');
+				return false;
+			}
+		} else {
+			if (elCheck == null) {
+				if (gagajf.isNull(checkStr)) {
+					mcxDialog.alert('내용을 입력하세요.');
+					return false;
+				}
+			}
+		}
+
+		return true;
+	}
+
+}

+ 110 - 0
src/main/webapp/ux/plugins/gaga/gaga.summernote.js

@@ -0,0 +1,110 @@
+/*
+ * Summernote Java Script written by gagamel.
+ *
+ * Copyright (c) 2010 gagamel
+ * Dual licensed under GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2020-10-29 $
+ *
+ * 사용 예)
+ * 		// HTML 태그는 textarea로 구성
+ * 		// id는 gagaSn.summernote 함수 호출 시에 넘겨줘야 한다.
+ * 		<textarea class="textareaR4" name="clauseContent" id="clauseContent"></textarea>
+ *
+ * 		// Import할 자바스크립트 파일
+ * 		<script type="text/javascript" src="/ux/plugins/summernote/summernote.js?v=2020102902"></script>
+ * 		<script type="text/javascript" src="/ux/plugins/gaga/gaga.summernote.js?v=2020102902"></script>
+ *
+ * 		<script type="text/javascript">
+ * 			// Get a summernote options
+ * 			var snOptions = gagaSn.getToolbarOptions();
+ *
+ * 			$(document).ready(function() {
+ * 				// Create a summernote
+ * 				gagaSn.createSummernote(snOptions, '#clauseContent');
+ * 			});
+ * 		</script>
+ */
+
+var gagaSn = {
+	/**
+	 * Get a Toolbar options
+	 * @param type - 유형(default, media: 사진/동영상 업로드)
+	 */
+	getToolbarOptions : function(type) {
+		if (typeof(type) == 'undefined' || type == 'default') {
+			return [
+				['style', ['style']],
+				['Font Style', ['fontname']],
+				['fontsize', ['fontsize']],
+				['height', ['height']],
+				['style', ['bold', 'italic', 'underline','clear']],
+				['font', ['strikethrough', 'superscript', 'subscript']],
+				['color', ['color']],
+				['para', ['ul', 'ol', 'paragraph']],
+				['Insert', ['table']],
+				['Insert', ['link']],
+				['misc', [ 'print']], //  프린트
+				['code', ['fullscreen', 'codeview', 'help']]
+			];
+		} else if (type == 'media') {
+			return [
+				['style', ['style']],
+				['Font Style', ['fontname']],
+				['fontsize', ['fontsize']],
+				['height', ['height']],
+				['style', ['bold', 'italic', 'underline','clear']],
+				['font', ['strikethrough', 'superscript', 'subscript']],
+				['color', ['color']],
+				['para', ['ul', 'ol', 'paragraph']],
+				['Insert', ['table']],
+				['Insert', ['link', 'picture', 'video']],
+				['misc', [ 'print']], // 프린트
+				['code', ['fullscreen', 'codeview', 'help']]
+			];
+		}
+	},
+
+	/**
+	 * Create a summernote
+	 * @param toolbarOptions - 툴바옵션
+	 * @param editorId - 에디터 ID
+	 * @param editorHeight - 에디터 height
+	 */
+	createSummernote : function(toolbarOptions, editorId, editorHeight) {
+		if (typeof(editorHeight) == 'undefined') editorHeight = 300;
+		
+		$(editorId).summernote({
+			disableDragAndDrop: true, //drag&drop 사용안함
+			placeholder: '내용을 입력하세요',
+			height: editorHeight, //에디터 기본 높이
+			lang : 'ko-KR', //기본 언어 인코딩
+			fontNames: ['Malgun Gothic', 'HY견고딕', 'Helvetica', 'Verdana', 'Arial', 'Arial Black'], //폰트 스타일
+			fontNamesIgnoreCheck: ['Malgun Gothic'], //기본폰트 스타일
+			focus: false, //로드시 에디터창에 포커싱
+			fontSizes: ['8','9','10','11','12','13','14','15','16','17','18','19','20','24','30','36'],
+			toolbar: toolbarOptions,
+			callbacks: {
+				onImageUpload: function(files, editor, welEditable) { //이미지 업로드
+					for (var i = files.length - 1; i >= 0; i--) {
+						sendFile(files[i], this);
+					}
+				}
+			}
+		});
+	},
+
+	/**
+	 * Set value to summernote
+	 */
+	setContents : function(editorId, content) {
+		var content = content.replaceAll("&lt;", "<").replaceAll("&gt;",">");
+
+		try {
+			$(editorId).summernote('code', content);
+		} catch(e) {
+			// Do nothing
+		}
+	}
+
+}

+ 1198 - 0
src/main/webapp/ux/plugins/gaga/gaga.validation.js

@@ -0,0 +1,1198 @@
+/*
+ * Form Validation Java Script written by gagamel
+ *
+ * Copyright (c) 2017 gagamel
+ * Dual licensed under GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2017-09-20 $
+ * $Modify: 2019-03-07 $
+ *
+ * Using)
+ * 		1. Add "data-valid-type" and "data-valid-name" attribute to Elements of form
+ * 			ex) <input type="text" name="userNm" data-valid-type="alpahNumeric" data-valid-name="User Name"/>
+ *
+ * 		2. data-valid-type
+ * 			numeric, alphaNumeric, email, cellPhone, ipAddress
+ *
+ * 		3. When submit a form, call "validation()" function.
+ * 			ex)
+ * 				if (!$('#aForm').validation())
+ * 					return;
+ */
+(function($) {
+	/**
+	 * Spring Security를 사용하는 경우 Default로 CSRF(Cross Site Request Forgery)가 활성화 되어 있다.
+	 * 이 때문에 $.ajax 함수를 호출 시 404 에러가 발생한다. 이는 다음과 같이 해결한다.
+	 *
+	 * 		1. SecurityConfig에 csrf().disable() 설정. <= CSRF(Cross Site Request Forgery) 해제
+	 * 		또는
+	 * 		2. Thymeleaf에서는 csrf 토큰이 hidden input에 자동으로 추가되어 있음으로 아래와 같이 ajax 호출 시 토큰을 Header에 설정한다.
+	 */
+	/*var token = $("meta[name='_csrf']").attr("content");
+	var header = $("meta[name='_csrf_header']").attr("content");
+	$.ajaxSend(function(e, xhr, options) {
+		xhr.setRequestHeader(header, token);
+	});*/
+});
+
+var gagajf = {
+	/**
+	 * @type   : function
+	 * @access : public
+	 * @desc   : 값이 null 이거나 white space 문자로만 이루어진 경우 true를 리턴한다.
+	 * <pre>
+	 *     gagajf.isNull("  ");
+	 * </pre>
+	 * 위와같이 사용했을 경우 true를 리턴한다.
+	 * @param  : value - 필수 입력 값
+	 * @return : boolean. null(혹은 white space) 여부
+	 * @author : gagamel
+	 */
+	isNull : function(value) {
+		if (value == null || (typeof(value) == "string" && value.trim() == ""))
+			return true;
+
+		return false;
+	},
+
+	/**
+	 * @type   : function
+	 * @access : public
+	 * @desc   : 값이 null str로 true를 리턴한다.
+	 * <pre>
+	 *     gagajf.convNull(item.value, '');
+	 * </pre>
+	 * 위와같이 사용했을 경우 item.value 가 null일경우 ''을 리턴한다.
+	 * @param  : value - 필수 입력 값
+	 * @param  : str - 필수 입력 값
+	 * @return : value or str
+	 * @author : gagamel
+	 */
+	convNull : function(value, str) {
+		if (value == null)
+			return str;
+
+		return value;
+	},
+
+	/**
+	 * 유효한 이벤트 키코드인지 체크
+	 * @return : 유효한 이벤트 키코드이면 키코드 값, 아니면 -1
+	 * @since  : 2017/09/20
+	 * @author : gagamel
+	 */
+	getKeyCode : function() {
+		// 이벤트 객체와 문자 코드를 호환 가능한 방식으로 얻는다.
+		var e = event || window.event; // 키 이벤트 객체
+		var keyCode = e.charCode || e.keyCode; // 어떤 키가 눌러졌는가?
+//		console.log('keyCode: ' + keyCode);
+
+		// Ctrl 키나 Alt 키, ASCII 제어문자, 화살표 등 skip
+		if (e.ctrlKey || e.altkey || keyCode < 47) {
+			return -1;
+		}
+
+		return keyCode;
+	},
+
+	/**
+	 * 값이 정규표현식에 부합하는지는 체크한다.
+	 * @param  : el - 엘리먼트
+	 * @param  : regexp - 정규표현식
+	 * @param  : type - 엘리먼트 type
+	 * @return : 부합하는 경우 true, 그 외 false
+	 * @since  : 2017/09/21
+	 * @author : gagamel
+	 */
+	testRegexp : function(el, regexp, type) {
+		var val = $(el).val();
+
+		if (type == 'integer' || type == 'real') {
+			// 콤마(,) 제거
+			val = val.removeComma();
+		}
+
+		if (!regexp.test(val)) {
+			mcxDialog.alertC($(el).data('validName') + '의 형식이 잘못되었습니다.', {
+				sureBtnText: "확인",
+				sureBtnClick: function() {
+					$(el).select();
+					$(el).focus();
+				}
+			});
+			return false;
+		}
+
+		return true;
+	},
+
+	/**
+	 * alert 메시지
+	 * @param  : el - 엘리먼트
+	 *           mgsType - 메시지유형(input, select)
+	 * @since  : 2017/09/21
+	 * @author : gagamel
+	 */
+	alertMessage : function(el, mgsType) {
+		var validNm = $(el).data('validName');
+
+		if (mgsType == 'input') {
+			mcxDialog.alertC(validNm + '을(를) 입력해 주세요.', {
+				sureBtnText: "확인",
+				sureBtnClick: function() {
+					$(el).focus();
+				}
+			});
+		} else if (mgsType == 'select') {
+			mcxDialog.alertC(validNm + '을(를) 선택해 주세요.', {
+				sureBtnText: "확인",
+				sureBtnClick: function() {
+					$(el).focus();
+				}
+			});
+		}
+	},
+
+	/**
+	 * 체크박스와 라디오버튼을 선택한 것이 있는지 체크한다.
+	 * @param  : el - 엘리먼트
+	 *           mgsType - 메시지유형(input, select)
+	 * @since  : 2017/09/21
+	 * @author : gagamel
+	 */
+	isCheckedCheckbox : function(el) {
+		$(el).each(function(idx) {
+			if ($(el).eq(idx).is(':checked')) {
+				return true;
+			} else {
+				return false;
+			}
+		});
+	},
+
+	/**
+	 * 비밀번호 체크
+	 * 		1.영문대문자, 영문소문자, 특수문자, 숫자로만 구성
+	 * 		2.이 중에 3가지 이상으로 구성 시 8자 이상, 2가지 이상으로 구성 시 10자 이상
+	 * @param  : el - 엘리먼트
+	 * @since  : 2017/09/21
+	 * @author : gagamel
+	 */
+	checkPassword : function(el) {
+		var passwd = $(el).val();
+		var cnt = 0;
+		if (/[a-z]{1,}/.test(passwd)) cnt++;
+		if (/[A-Z]{1,}/.test(passwd)) cnt++;
+		if (/[0-9]{1,}/.test(passwd)) cnt++;
+		if (/[\~,\!,\@,\#,\$,\%,\^,\&,\*,\(,\),\_,\?,\{,\},\[,\]]{1,}/.test(passwd)) cnt++;
+
+		if (cnt >= 3) {
+			if (passwd.length < 8) {
+				mcxDialog.alertC('3가지 이상으로 구성 시 8 자리 이상으로 입력해 주세요.', {
+					sureBtnText: "확인",
+					sureBtnClick: function() {
+						$(el).select();
+						$(el).focus();
+					}
+				});
+				return false;
+			}
+		} else if (cnt >= 2) {
+			if (passwd.length < 10) {
+				mcxDialog.alertC('2가지 이상으로 구성 시 10 자리 이상으로 입력해 주세요.', {
+					sureBtnText: "확인",
+					sureBtnClick: function() {
+						$(el).select();
+						$(el).focus();
+					}
+				});
+				return false;
+			}
+		} else {
+			mcxDialog.alertC('대/소문자, 특수문자, 숫자로 구성해 주세요.', {
+				sureBtnText: "확인",
+				sureBtnClick: function() {
+					$(el).select();
+					$(el).focus();
+				}
+			});
+			return false;
+		}
+
+		return true;
+	},
+
+	/**
+	 * 필수입력항목 엘리먼트를 체크해서 alert를 표시한다.
+	 */
+	checkRequired : function(oForm) {
+		var isInvalid = true;
+
+		$(oForm).find(':input').each(function(idx, el) {
+			if ($(el).attr('required') != 'required')
+				return true;
+
+			var type = $(el).attr('type');
+			if (!type) type = 'select';
+//			var msg = $(el).data('validName');
+
+			switch (type) {
+				case 'text':
+				case 'password':
+				case 'textarea':
+//				case 'select-one':
+					var value = $(el).val();
+					if (!gagajf.isNull(value))
+						return true;
+
+					gagajf.alertMessage($(el), 'input');
+					isInvalid = false;
+					return false;
+				case 'select':
+					var value = $(el).val();
+					if (!gagajf.isNull(value))
+						return true;
+
+					gagajf.alertMessage($(el), 'select');
+					isInvalid = false;
+					return false;
+				case 'checkbox':
+				case 'radio':
+					if (gagajf.isCheckedCheckbox($(el)))
+						return true;
+
+					gagajf.alertMessage($(el), 'select');
+					isInvalid = false;
+					return false;
+				case 'file':
+					var value = $(el).val();
+					if (!gagajf.isNull(value))
+						return true;
+
+					gagajf.alertMessage($(el), 'select');
+					isInvalid = false;
+					return false;
+			}
+		});
+
+		return isInvalid;
+	},
+
+	/**
+	 * 값이 형식에 맞는지 패턴을 체크한다.
+	 */
+	checkPattern : function(oForm) {
+		var isInvalid = true;
+
+		$(oForm).find('input').each(function(idx, el) {
+			if (gagajf.isNull($(el).val()))
+				return true;
+
+			var validType = $(el).data('validType');
+
+			// data-valid-type이 지정되지 않은 엘리먼트는 skip
+			if (!validType) return true;
+
+			// 값이 없으면 skip
+			if (gagajf.isNull($(el).val())) return true;
+
+			switch (validType) {
+				case 'numeric': // 숫자
+					if (gagajf.testRegexp($(el), /^[0-9]+$/))
+						return true;
+
+					isInvalid = false;
+					return false;
+				case 'integer': // 정수
+					if (gagajf.testRegexp($(el), /(^-?[0-9]+\d*$)|(^-$)/, 'integer'))
+						return true;
+
+					isInvalid = false;
+					return false;
+				case 'real': // 실수
+					if (gagajf.testRegexp($(el), /^-?(([0-9]+\.?)|(\.?))\d*$/, 'real'))
+						return true;
+
+					isInvalid = false;
+					return false;
+				case 'alphaNumeric': // 알파벳+숫자
+					if (gagajf.testRegexp($(el), /^[a-zA-Z0-9]+$/))
+						return true;
+
+					isInvalid = false;
+					return false;
+				case 'email': // 이메일
+					if (gagajf.testRegexp($(el), /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/))
+						return true;
+					isInvalid = false;
+					return false;
+				case 'password': // 이메일
+					if (gagajf.checkPassword($(el)))
+						return true;
+					isInvalid = false;
+					return false;
+				case 'cellPhone': // 휴대전화번호
+					if (gagajf.testRegexp($(el), /^(01(?:0|1|[6-9])-(?:\d{3}|\d{4})-\d{4})$/))
+						return true;
+
+					isInvalid = false;
+					return false;
+				case 'phone': // 일반전화번호
+					if (gagajf.testRegexp($(el), /^\d{2,3}-\d{3,4}-\d{4}$/))
+						return true;
+
+					isInvalid = false;
+					return false;
+				case 'ipAddress': // IP주소
+					if (gagajf.testRegexp($(el), /^(([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3}))$/))
+						return true;
+
+					isInvalid = false;
+					return false;
+			}
+		});
+
+		return isInvalid;
+	},
+
+	checkValue : function(oForm){
+		var isInvalid = true;
+
+		$(oForm).find('input').each(function(idx, el) {
+
+			var validType = $(el).data('validType');
+
+			// data-valid-type이 지정되지 않은 엘리먼트는 skip
+			if (!validType) return true;
+
+			// 값이 없으면 skip
+			if (gagajf.isNull($(el).val())) return true;
+
+			switch (validType) {
+				case 'numeric': // 숫자
+					//최대값
+					if ($(el).attr("max")) {
+						if ($(el).val() > $(el).attr("max")) {
+							mcxDialog.alertC($(el).data('validName') + '은(는) 최대 ' + $(el).attr("max") + '보다 작아야 합니다.', {
+								sureBtnText: "확인",
+								sureBtnClick: function() {
+									$(el).focus();
+								}
+							});
+							isInvalid = false;
+						}
+					}
+					//최소값
+					if ($(el).attr("min")) {
+						if ($(el).val() < $(el).attr("min")) {
+							mcxDialog.alertC($(el).data('validName') + '은(는) 최소 ' + $(el).attr("min") + '보다 커야 합니다.', {
+								sureBtnText: "확인",
+								sureBtnClick: function() {
+									$(el).focus();
+								}
+							});
+							isInvalid = false;
+						}
+					}
+
+				case 'integer': // 정수
+					//최대값
+					if ($(el).attr("max")) {
+						if ($(el).val() > $(el).attr("max")) {
+							mcxDialog.alertC($(el).data('validName') + '은(는) 최대 ' + $(el).attr("max") + '보다 작아야 합니다.', {
+								sureBtnText: "확인",
+								sureBtnClick: function() {
+									$(el).focus();
+								}
+							});
+							isInvalid = false;
+						}
+					}
+					//최소값
+					if ($(el).attr("min")) {
+						if ($(el).val() < $(el).attr("min")) {
+							mcxDialog.alertC($(el).data('validName') + '은(는) 최소 ' + $(el).attr("min") + '보다 커야 합니다.', {
+								sureBtnText: "확인",
+								sureBtnClick: function() {
+									$(el).focus();
+								}
+							});
+							isInvalid = false;
+						}
+					}
+
+				case 'real': // 실수
+					//최대값
+					if ($(el).attr("max")) {
+						if ($(el).val() > $(el).attr("max")) {
+							mcxDialog.alertC($(el).data('validName') + '은(는) 최대 ' + $(el).attr("max") + '보다 작아야 합니다.', {
+								sureBtnText: "확인",
+								sureBtnClick: function() {
+									$(el).focus();
+								}
+							});
+							isInvalid = false;
+						}
+					}
+					//최소값
+					if ($(el).attr("min")) {
+						if ($(el).val() < $(el).attr("min")) {
+							mcxDialog.alertC($(el).data('validName') + '은(는) 최소 ' + $(el).attr("min") + '보다 커야 합니다.', {
+								sureBtnText: "확인",
+								sureBtnClick: function() {
+									$(el).focus();
+								}
+							});
+							isInvalid = false;
+						}
+					}
+			}
+		});
+		
+		return isInvalid;
+	},
+
+	/**
+	 * form을 validation 한다.
+	 * 예)
+	 * 		if (gagajf.validation('#registerForm');
+	 */
+	validation : function(formId) {
+		var $form = $(formId);
+
+		if (!this.checkRequired($form))
+			return false;
+
+		if (!this.checkPattern($form))
+			return false;
+
+		if(!this.checkValue($form))
+			return false;
+
+		return true;
+	},
+
+	/**
+	 * formId의 input의 data-valid-type이 integer, real인 경우에 값에 comma(,)를 자동으로 붙여 표시한다.
+	 * 사용) gagajf.addCommaAtNumberFormattedInput('#registerForm');
+	 */
+	addCommaAtNumberFormattedInput : function(formId) {
+		$(formId).find('input').each(function(idx, el) {
+			if ($(el).data('validType') == 'integer' || $(el).data('validType') == 'real') {
+				$(el).val($(el).val().removeComma().addComma());
+			}
+		});
+	},
+
+	/**
+	 * formId의 input의 data-valid-type이 integer, real, numeric 인 경우에 값에 comma(,)를 자동으로 제거한다.
+	 * 사용) gagajf.removeCommaAtNumberFormattedInput('#registerForm');
+	 */
+	removeCommaAtNumberFormattedInput : function(formId) {
+		$(formId).find('input').each(function(idx, el) {
+			if ($(el).data('validType') == 'integer' || $(el).data('validType') == 'real' || $(el).data('validType') == 'numeric') {
+				$(el).val($(el).val().removeComma());
+			}
+		});
+	},
+
+	/**
+	 * Progress bar
+	 */
+	showProgressbar : function(isLoading) {
+		if (isLoading) {
+			// Button disabled & progressBar creation
+			//$('.btn').each(function(idx) { $(this).attr('disabled', true); });
+			var load_AjaxSubmit = '<div id="load_AjaxSubmit" style="'
+				+ 'background: url(/ux/plugins/gaga/loader.gif); border-style: none; background-repeat: no-repeat; '
+				+ 'position: absolute; top: 45%; left: 50%; width: auto; '
+				+ 'z-index: 101; padding: 16px; margin: 5px;'
+				+ '"></div>';
+			$('#content').append(load_AjaxSubmit);
+		} else {
+			// Button activated & progressBar remove
+			//$('.btn').each(function(idx) { $(this).attr('disabled', false); });
+			$('#load_AjaxSubmit').remove();
+		}
+	},
+
+	/**
+	 * form의 데이터를 json으로 변환 후 ajax 방식으로 submit 한다.
+	 * 모든 form의 ajax 처리는 이것으로 진행한다.
+	 * <pre>
+	 *     ajaxFormSubmit('/rest/commoncode/create', '#registerForm', jfRegisterSaveCallback);
+	 * </pre>
+	 * @param actionUrl - Request URL
+	 * @param formId - form ID
+	 * @param callbackFn - Callback function
+	 * @author gagamel
+	 * @since 2019. 4. 8
+	 */
+	ajaxFormSubmit : function(actionUrl, formId, callbackFn) {
+		// comma(,) 제거
+		gagajf.removeCommaAtNumberFormattedInput(formId);
+
+		var jsonData = JSON.stringify($(formId).serializeObject());
+		
+		$.ajax({
+			type : 'POST',
+			url : actionUrl,
+			data : jsonData,
+			dataType : 'json',
+			beforeSend : function(xhr, settings) {
+				// AJAX call
+				xhr.setRequestHeader("AJAX", "true");
+
+				// dataType: "json"일 때
+				xhr.setRequestHeader('Accept', 'application/json');
+				xhr.setRequestHeader('Content-Type', 'application/json');
+
+				// Button disabled & progressBar creation
+				gagajf.showProgressbar(true);
+			},
+			complete : function(xhr) {
+				// Button abled & progressBar remove
+				gagajf.showProgressbar(false);
+
+				// 세션이 없다. 로그인 페이지로 이동
+				if (xhr.status == 901) {
+					mcxDialog.alertC('세션이 없습니다. 로그인 페이지로 이동합니다.', {
+						sureBtnText: "확인",
+						sureBtnClick: function() {
+							document.location.href = "/error/noSession";
+						}
+					});
+				}
+			},
+			success : function(result) {
+				if (typeof(result.status) == 'undefined' || result.status == 200) { // 성공
+					if (!gagajf.isNull(result.message)) {
+						mcxDialog.alertC(result.message, {
+							sureBtnText: "확인",
+							sureBtnClick: function() {
+								if (typeof(callbackFn) == "function") {
+									callbackFn.call(this, result);
+								}
+							}
+						});
+					} else {
+						if (typeof(callbackFn) == "function") {
+							callbackFn.call(this, result);
+						}
+					}
+				} else { // 실패
+					if (!gagajf.isNull(result.error.message)) {
+						mcxDialog.alert(result.error.message);
+					}
+
+					return;
+				}
+			},
+			error : function(result) {
+				console.log(result);
+				mcxDialog.alert('오류로 인해 처리되지 않았습니다.');
+			}
+		});
+	},
+
+	/**
+	 * json 데이터를 가지고 ajax 방식으로 submit 한다.
+	 * 모든 ajax 처리는 이것으로 진행한다.
+	 * <pre>
+	 *     gagajf.removeCommaAtNumberFormattedInput('#registerForm'); // comma(,) 제거
+	 *     var jsonData = JSON.stringify($('#registerForm').serializeObject());
+	 *     gagajf.ajaxJsonSubmit('/rest/commoncode/create', jsonData, jfRegisterSaveCallback);
+	 * </pre>
+	 * @param actionUrl - Request URL
+	 * @param jsonData - Data of json format
+	 * @param callbackFn - Callback function
+	 * @author gagamel
+	 * @since 2019. 4. 8
+	 */
+	ajaxJsonSubmit : function(actionUrl, jsonData, callbackFn) {
+		$.ajax({
+			type : 'POST',
+			url : actionUrl,
+			data : jsonData,
+			dataType : 'json',
+			beforeSend : function(xhr, settings) {
+				// AJAX call
+				xhr.setRequestHeader("AJAX", "true");
+
+				// dataType: "json"일 때
+				xhr.setRequestHeader('Accept', 'application/json');
+				xhr.setRequestHeader('Content-Type', 'application/json');
+
+				// Button disabled & progressBar creation
+				gagajf.showProgressbar(true);
+			},
+			complete : function(xhr) {
+				// Button abled & progressBar remove
+				gagajf.showProgressbar(false);
+
+				// 세션이 없다. 로그인 페이지로 이동
+				if (xhr.status == 901) {
+					mcxDialog.alertC('세션이 없습니다. 로그인 페이지로 이동합니다.', {
+						sureBtnText: "확인",
+						sureBtnClick: function() {
+							document.location.href = "/error/noSession";
+						}
+					});
+				}
+			},
+			success : function(result) {
+				if (typeof(result.status) == 'undefined' || result.status == 200) { // 성공
+					if (!gagajf.isNull(result.message)) {
+						mcxDialog.alertC(result.message, {
+							sureBtnText: "확인",
+							sureBtnClick: function() {
+								if (typeof(callbackFn) == "function") {
+									callbackFn.call(this, result);
+								}
+							}
+						});
+					} else {
+						if (typeof(callbackFn) == "function") {
+							callbackFn.call(this, result);
+						}
+					}
+				} else { // 실패
+					if (!gagajf.isNull(result.error.message)) {
+						mcxDialog.alert(result.error.message);
+					}
+
+					return;
+				}
+			},
+			error : function(result) {
+				console.log(result);
+				mcxDialog.alert('오류로 인해 처리되지 않았습니다.');
+			}
+		});
+	},
+
+	/**
+	 * ajax 방식으로 파일을 업로드 한다.
+	 * <pre>
+	 *     gagajf.ajaxFileUpload('/common/file/upload?subDir=notice', this.files[0], jfCallback);
+	 * </pre>
+	 * @param actionUrl - Request URL
+	 * @param file - A file to upload
+	 * @param callbackFn - Callback function
+	 * @param policy - Upload policy
+	 * @author gagamel
+	 * @since 2019. 7. 9
+	 */
+	ajaxFileUpload : function(actionUrl, file, callbackFn, policy) {
+		var formData = new FormData();
+		formData.append("file", file);
+
+		if (typeof policy != 'undefined') {
+			formData.append("policy", policy);
+		}
+
+		$.ajax({
+			type : 'POST',
+			url : actionUrl,
+			data : formData,
+			dataType: 'json',
+			processData : false, // true: data의 파일 형태가 query String으로 전송. false : non-processed data
+			contentType : false, // multipart/form-data 형태로 전송되기 위한 옵션 값
+			beforeSend : function(xhr, settings) {
+				// AJAX call
+				xhr.setRequestHeader("AJAX", "true");
+
+				// Button disabled & progressBar creation
+				gagajf.showProgressbar(true);
+			},
+			complete : function(xhr) {
+				// Button abled & progressBar remove
+				gagajf.showProgressbar(false);
+
+				// 세션이 없다. 로그인 페이지로 이동
+				if (xhr.status == 901) {
+					mcxDialog.alertC('세션이 없습니다. 로그인 페이지로 이동합니다.', {
+						sureBtnText: "확인",
+						sureBtnClick: function() {
+							document.location.href = "/error/noSession";
+						}
+					});
+				}
+			},
+			success : function(result) {
+				if (typeof(result.status) == 'undefined' || result.status == 200) { // 성공
+					if (!gagajf.isNull(result.message)) {
+						mcxDialog.alertC(result.message, {
+							sureBtnText: "확인",
+							sureBtnClick: function() {
+								if (typeof(callbackFn) == "function") {
+									callbackFn.call(this, result);
+								}
+							}
+						});
+					} else {
+						if (typeof(callbackFn) == "function") {
+							callbackFn.call(this, result);
+						}
+					}
+				} else { // 실패
+					if (!gagajf.isNull(result.error.message)) {
+						mcxDialog.alert(result.error.message);
+					}
+
+					return;
+				}
+			},
+			error: function(result) {
+				console.log(result);
+				mcxDialog.alert('오류로 인해 처리되지 않았습니다.');
+			}
+		});
+	},
+
+	/**
+	 * 대용량 json 데이터를 가지고 ajax 방식으로 submit 한다.
+	 * <pre>
+	 *     gagajf.removeCommaAtNumberFormattedInput('#registerForm'); // comma(,) 제거
+	 *     var jsonData = JSON.stringify($('#registerForm').serializeObject());
+	 *     gagajf.ajaxJsonBatchSubmit('/rest/commoncode/create', jsonData, 1, 3, jfRegisterSaveCallback);
+	 * </pre>
+	 * @param actionUrl - Request URL
+	 * @param jsonData - Data of json format
+	 * @param callIdx - 호출인덱스(실제 호출한 횟수)
+	 * @param callCnt - 호출해야할횟수(몇 번 호출해야 하는지)
+	 * @param callbackFn - Callback function
+	 * @author gagamel
+	 * @since 2019. 4. 8
+	 */
+	ajaxJsonBatchSubmit : function(actionUrl, jsonData, callIdx, callCnt, callbackFn) {
+		$.ajax({
+			type : 'POST',
+			url : actionUrl,
+			data : jsonData,
+			dataType : 'json',
+			beforeSend : function(xhr, settings) {
+				// AJAX call
+				xhr.setRequestHeader("AJAX", "true");
+
+				// dataType: "json"일 때
+				xhr.setRequestHeader('Accept', 'application/json');
+				xhr.setRequestHeader('Content-Type', 'application/json');
+
+				// Button disabled & progressBar creation
+				if (callIdx == 1) { // 첫번째 호출이면
+					gagajf.showProgressbar(true);
+				}
+			},
+			complete : function(xhr) {
+				// Button abled & progressBar remove
+				if (callIdx == callCnt) { // 마지막 호출이면
+					gagajf.showProgressbar(false);
+				}
+
+				// 세션이 없다. 로그인 페이지로 이동
+				if (xhr.status == 901) {
+					mcxDialog.alertC('세션이 없습니다. 로그인 페이지로 이동합니다.', {
+						sureBtnText: "확인",
+						sureBtnClick: function() {
+							document.location.href = "/error/noSession";
+						}
+					});
+				}
+			},
+			success : function(result) {
+				if (callIdx == callCnt) { // 마지막 호출이면
+					mcxDialog.alertC('성공적으로 처리되었습니다.', {
+						sureBtnText: "확인",
+						sureBtnClick: function() {
+							if (typeof(callbackFn) == "function") {
+								callbackFn.call(this, result);
+							}
+						}
+					});
+				} else {
+					if (typeof(callbackFn) == "function") {
+						callbackFn.call(this, result);
+					}
+				}
+			},
+			error : function(result) {
+				console.log(result);
+				mcxDialog.alert('오류로 인해 처리되지 않았습니다.');
+			}
+		});
+	},
+
+	/**
+	 * JQuery를 이용한 비동기 submit 처리
+	 * 파라미터의 명칭은 쿼리문과 동일하게 작성해야 한다.
+	 * <pre>
+	 *     var params = new Object();
+	 *     params.cdGb = "G900";
+	 *     params.cd = "0202";
+	 *
+	 *     gagajf.ajaxSubmit("/rest/commoncode/create", "json", jfCallback, params);
+	 *
+	 *     or
+	 *
+	 *     gagajf.ajaxSubmit("/rest/commoncode/create", "json", jfCallback);
+	 * </pre>
+	 * @param  : actionUrl - action url. 필수
+	 *           type - 처리결과 형식(text, html, xml, json). 필수
+	 *           callback - type이 text, xml, json 일 때는 콜백함수명
+	 *                      type이 html일 때는 target명. 필수
+	 *           params - 파라미터 오브젝트. 옵션
+	 * @author gagamel
+	 * @since 2019. 4. 8
+	 */
+	ajaxSubmit : function(actionUrl, type, callback, params) {
+		$.ajaxSetup({
+			beforeSend: function(xhr, settings) {
+				// AJAX call
+				xhr.setRequestHeader("AJAX", "true");
+
+				// type: "json"일 때
+				if (type == "json") {
+					xhr.setRequestHeader('Accept', 'application/json');
+					xhr.setRequestHeader('Content-Type', 'application/json');
+				}
+			},
+			complete: function(xhr) {
+				// 세션이 없다. 로그인 페이지로 이동
+				if (xhr.status == 901) {
+					mcxDialog.alertC('세션이 없습니다. 로그인 페이지로 이동합니다.', {
+						sureBtnText: "확인",
+						sureBtnClick: function() {
+							document.location.href = "/error/noSession";
+						}
+					});
+				}
+			}
+		});
+
+		if (!params) params = new Object();
+
+		var paramData = $.param(params);
+
+		// dataType: "json"일 때
+		if (type == "json") {
+			paramData = JSON.stringify(params);
+		}
+
+		$.post(actionUrl
+			, paramData
+			, function(result) {
+				if (type == "html") {
+					if (!gagajf.isNull(callback))
+						$(document.getElementById(callback)).html(result);
+				} else {
+					// Callback 함수 호출
+					if (typeof(callback) == "function")
+						callback.call(this, result);
+				}
+			}
+			, type);
+	},
+
+	/**
+	 * @type   : function
+	 * @access : public
+	 * @desc   : 기간의 시작일자와 종료일자를 설정한다.
+	 * <pre>
+	 *     gagajf.setDate($('#sellStdt'), $('#sellEddt'), 't');
+	 * </pre>
+	 * @param  : fromObj - 시작일자 오브젝트
+	 * @param  : toObj - 종료일자 오브젝트
+	 * @param  : type - 유형(오늘: t, 어제: y, 최근한주: 7d, 이번주: tw, 지난주: pw, 최근한달: 1m, 이번달: tm, 지난달: pm, 최근3개월: 3m
+	 * @since  : 2019/08/09
+	 * @author : gagamel
+	 */
+	setDate : function(tgtId, fromObj, toObj, type) {
+		var date = new Date();
+
+		if (type == '') { // 기간 X
+			$(tgtId +' #' + fromObj).val('');
+			$(tgtId +' #' + toObj).val('');
+		} else if (type == 't') { // 오늘
+			$(tgtId +' #' + fromObj).val(date.format("YYYY-MM-DD"));
+			$(tgtId +' #' + toObj).val(date.format("YYYY-MM-DD"));
+		} else if (type == 'y') { // 어제
+			$(tgtId +' #' + fromObj).val(date.before(0, 0, 1).format("YYYY-MM-DD"));
+			$(tgtId +' #' + toObj).val(date.before(0, 0, 1).format("YYYY-MM-DD"));
+		} else if (type == '7d') { // 최근한주
+			$(tgtId +' #' + fromObj).val(date.before(0, 0, 6).format("YYYY-MM-DD"));
+			$(tgtId +' #' + toObj).val(date.format("YYYY-MM-DD"));
+		} else if (type == 'tw') { // 이번주
+			var wdays = date.getDate() - date.getDay();
+			$(tgtId +' #' + fromObj).val((date.format('YYYY-MM-') + '01').toDate('YYYY-MM-DD').after(0, 0, wdays).format("YYYY-MM-DD"));
+			$(tgtId +' #' + toObj).val((date.format('YYYY-MM-') + '01').toDate('YYYY-MM-DD').after(0, 0, wdays + 6).format("YYYY-MM-DD"));
+		} else if (type == 'pw') { // 지난주
+			var wdays = date.getDate() - date.getDay();
+			$(tgtId +' #' + fromObj).val((date.format('YYYY-MM-') + '01').toDate('YYYY-MM-DD').after(0, 0, wdays - 7).format("YYYY-MM-DD"));
+			$(tgtId +' #' + toObj).val((date.format('YYYY-MM-') + '01').toDate('YYYY-MM-DD').after(0, 0, wdays - 1).format("YYYY-MM-DD"));
+		} else if (type == '1m') { // 최근한달
+			$(tgtId +' #' + fromObj).val(date.before(0, 1, 0).after(0, 0, 1).format("YYYY-MM-DD"));
+			$(tgtId +' #' + toObj).val(date.format("YYYY-MM-DD"));
+		} else if (type == 'tm') { // 이번달
+			$(tgtId +' #' + fromObj).val(date.format("YYYY-MM-") + '01');
+			$(tgtId +' #' + toObj).val((date.format('YYYY-MM-') + '01').toDate('YYYY-MM-DD').after(0, 1, 0).before(0, 0, 1).format("YYYY-MM-DD"));
+		} else if (type == 'pm') { // 지난달
+			$(tgtId +' #' + fromObj).val(date.before(0, 1, 0).format("YYYY-MM-")  + '01');
+			$(tgtId +' #' + toObj).val((date.format('YYYY-MM-') + '01').toDate('YYYY-MM-DD').before(0, 0, 1).format("YYYY-MM-DD"));
+		} else if (type == '3m') { // 최근3개월
+			$(tgtId +' #' + fromObj).val(date.before(0, 3, 0).after(0, 0, 1).format("YYYY-MM-DD"));
+			$(tgtId +' #' + toObj).val(date.format("YYYY-MM-DD"));
+		}
+	},
+
+	/**
+	 * @type   : function
+	 * @access : public
+	 * @desc   : Set Cookie
+	 * <pre>
+	 *     gagajf.setCookie("COOKIE_TODAY_PROD", "HUE00C105GE", 1);
+	 * </pre>
+	 * @param  : name - 쿠키명
+	 * @param  : value - 쿠키 값
+	 * @param  : expiredays - 만료기간
+	 * @return : None
+	 * @since  : 2019/07/01
+	 * @author : gagamel
+	 */
+	setCookie : function(name, value, expiredays) {
+		var todayDate = new Date();
+		todayDate.setDate(todayDate.getDate() + expiredays);
+		document.cookie = name + "=" + escape(value) + "; path=/; expires=" + todayDate.toGMTString() + ";";
+	},
+
+	/**
+	 * @type   : function
+	 * @access : public
+	 * @desc   : Get Cookie
+	 * <pre>
+	 *     gagajf.getCookie("COOKIE_TODAY_PROD");
+	 * </pre>
+	 * @param  : name - 쿠키명
+	 * @return : None
+	 * @since  : 2019/07/01
+	 * @author : gagamel
+	 */
+	getCookie : function(name) {
+		var nameOfCookie = name + "=";
+		var x = 0;
+		while (x <= document.cookie.length) {
+			var y = (x+nameOfCookie.length);
+			if (document.cookie.substring(x, y) == nameOfCookie) {
+				if ((endOfCookie=document.cookie.indexOf(";", y)) == -1) endOfCookie = document.cookie.length;
+				return unescape(document.cookie.substring(y, endOfCookie));
+			}
+			x = document.cookie.indexOf(" ", x) + 1;
+			if (x == 0) break;
+		}
+
+		return "";
+	},
+	
+	/**
+	 * 데이터를 배열로 변환
+	 * 예)
+	 * 		convertToArray({cd: "KNE", cdNm: "KNE"});
+	 * @param  : data - 데이터
+	 * @param  : isCodeDisplay - 코드표시여부(true/false). default false
+	 * @author : gagamel
+	 * @since  : 2019. 6. 7
+	 */
+	convertToArray : function(data, isCodeDisplay) {
+		if (data.length == 0)
+			return [];
+
+		if (typeof(isCodeDisplay) == 'undefined')
+			isCodeDisplay = false;
+
+		var arrValue = {};
+
+		$.each(data, function(idx, item) {
+			arrValue[item.cd] = (isCodeDisplay ? '[' + item.cd + '] ' : '') + item.cdNm;
+		});
+
+		return arrValue;
+	}
+};
+
+/**
+ * @type   : function
+ * @access : document
+ * @desc   : <input> 태그에 대한 키눌림에 대해 validation을 체크한다.
+ *           data-valid-type="numeric" : 숫자. 속성 지정시 숫자만 입력 가능
+ *           data-valid-type="integer" : 정수. 속성 지정시 숫자와 +, - 만 입력 가능
+ *           data-valid-type="real" : 실수. 속성 지정시 숫자와 +, -, . 만 입력 가능
+ *           data-valid-type="alphaNumeric" : 알파벳과 숫자. 속성 지정 시 영문과 숫자만 형식에 맞게 입력 가능
+ *           data-valid-type="date" : 숫자와 / 만 입력 가능
+ *           data-valid-type="korean" : 한글. 속성 지정 시 한글만 형식에 맞게 입력 가능
+ *           data-valid-type="email" : 이메일
+ *           data-valid-type="password" : 비밀번호
+ *           data-valid-type="cellPhone" : 휴대전화번호
+ *           data-valid-type="phone" : 일반전화번호
+ *           data-valid-type="ipAddress" : IP주소
+ *           data-valid-type="bizRegNo" : 사업자등록번호
+ * <pre>
+ *     <input type="text" data-valid-type="numeric" />
+ *     <input type="text" data-valid-type="integer" />
+ *     <input type="text" data-valid-type="real" />
+ *     <input type="text" data-valid-type="alphaNumeric" />
+ *     <input type="text" data-valid-type="date" />
+ *     <input type="text" data-valid-type="korean" />
+ *     <input type="text" data-valid-type="email" />
+ *     <input type="text" data-valid-type="password" />
+ *     <input type="text" data-valid-type="cellPhone" />
+ *     <input type="text" data-valid-type="phone" />
+ *     <input type="text" data-valid-type="ipAddress" />
+ *     <input type="text" data-valid-type="bizRegNo" />
+ * </pre>
+ * @author : gagamel
+ * @since  : 2017/09/20
+ */
+$(document).on("keyup", "[data-valid-type=numeric]", function() { $(this).val($(this).val().replace(/[^0-9]/gi,"")); });
+$(document).on("keydown", "[data-valid-type=numeric]", function() {
+	var value = $(this).val();
+
+	var keyCode = gagajf.getKeyCode();
+	if (keyCode == -1)
+		return true;
+
+	if (!((keyCode >= 48 && keyCode <= 57 && !event.shiftKey) // 0 ~ 9
+		|| (keyCode >= 96 && keyCode <= 105) // 0 ~ 9 (Num Lock)
+		)) {
+		$(this).val(value);
+		event.returnValue = false;
+	}
+});
+$(document).on("blur", "[data-valid-type=integer]", function() { $(this).val($(this).val().removeComma().addComma()); });
+$(document).on("click", "[data-valid-type=integer]", function() { $(this).val($(this).val().removeComma()); });
+$(document).on("keyup", "[data-valid-type=integer]", function() { $(this).val($(this).val().replace(/[^0-9-\+]/gi,"")); });
+$(document).on("keydown", "[data-valid-type=integer]", function() {
+	var value = $(this).val();
+
+	var keyCode = gagajf.getKeyCode();
+	if (keyCode == -1)
+		return true;
+
+	if (!((keyCode >= 48 && keyCode <= 57 && !event.shiftKey) // 0 ~ 9
+		|| (keyCode >= 96 && keyCode <= 105) // 0 ~ 9 (Num Lock)
+		|| (keyCode == 187 && event.shiftKey) // Shift 하고 +
+		|| (keyCode == 107) // + (Num Lock)
+		|| (keyCode == 189 && !event.shiftKey) // Shift 없이 -
+		|| (keyCode == 109) // - (Num Lock)
+		)) {
+		$(this).val(value);
+		event.returnValue = false;
+	}
+});
+$(document).on("blur", "[data-valid-type=real]", function() { $(this).val($(this).val().removeComma().addComma()); });
+$(document).on("click", "[data-valid-type=real]", function() { $(this).val($(this).val().removeComma()); });
+$(document).on("keyup", "[data-valid-type=real]", function() { $(this).val($(this).val().replace(/[^0-9-\+\.]/gi,"")); });
+$(document).on("keydown", "[data-valid-type=real]", function() {
+	var value = $(this).val();
+
+	var keyCode = gagajf.getKeyCode();
+	if (keyCode == -1)
+		return true;
+
+	if (!((keyCode >= 48 && keyCode <= 57 && !event.shiftKey) // 0 ~ 9
+		|| (keyCode >= 96 && keyCode <= 105) // 0 ~ 9 (Num Lock)
+		|| (keyCode == 187 && event.shiftKey) // Shift 하고 +
+		|| (keyCode == 107) // + (Num Lock)
+		|| (keyCode == 189 && !event.shiftKey) // Shift 없이 -
+		|| (keyCode == 109) // - (Num Lock)
+		|| (keyCode == 190 && !event.shiftKey) // .
+		)) {
+		$(this).val(value);
+		event.returnValue = false;
+	}
+});
+$(document).on("keyup", "[data-valid-type=alphaNumeric]", function() { $(this).val($(this).val().replace(/[^a-zA-Z0-9]/gi, "")); });
+$(document).on("keyup", "[data-valid-type=date]", function() { $(this).val($(this).val().replace(/[^0-9\/]/gi,"")); });
+$(document).on("keyup", "[data-valid-type=calendar]", function() { $(this).val($(this).val().replace(/[^0-9\/]/gi,"")); });
+$(document).on("blur", "[data-valid-type=calendar]", function() {
+	var val = $(this).val();
+	if(val==''){
+		return;
+	}
+	val = val.replace(/-/gi, "");
+	var temp = val.substring(0, 4)+" ";
+	if(Number(val.substring(4, 6))>12){
+		temp += "12";
+	}else if(Number(val.substring(4, 6))==0){
+		temp += "1";
+	}else{
+		temp += Number(val.substring(4, 6));
+	}
+	temp += " ";
+	if(Number(val.substring(6, 8))>31){
+		temp += "31";
+	}else if(Number(val.substring(6, 8))==0){
+		temp += "1";
+	}else{
+		temp += Number(val.substring(6, 8));
+	}
+	
+	var date = new Date(temp);
+	var yyyy = date.getFullYear().toString();
+	var mm = (date.getMonth() + 1).toString();
+	var dd = date.getDate().toString();
+	$(this).val(yyyy + '-' + (mm[1] ? mm : '0'+mm[0])+ '-'  + (dd[1] ? dd : '0'+dd[0]));
+});
+$(document).on("keyup", "[data-valid-type=korean]", function() { $(this).val($(this).val().replace(/[^가-힣]/gi, "")); });
+$(document).on("keyup", "[data-valid-type=email]", function() { $(this).val($(this).val().replace(/[^a-zA-Z0-9\@\+\_\.\@\-]/gi, "")); });
+$(document).on("keyup", "[data-valid-type=password]", function() { $(this).val($(this).val().replace(/[^a-zA-Z0-9\~\!\@\#\$\%\^\&\*\?\(\)\_\+\{\}\[\]]/gi, "")); });
+$(document).on("keyup", "[data-valid-type=cellPhone]", function() { $(this).val($(this).val().replace(/[^\d-]/gi, "")); });
+$(document).on("keydown", "[data-valid-type=cellPhone]", function() {
+	var value = $(this).val();
+
+	var keyCode = gagajf.getKeyCode();
+	if (keyCode == -1)
+		return true;
+
+	if (!((keyCode >= 48 && keyCode <= 57 && !event.shiftKey) // 0 ~ 9
+		|| (keyCode >= 96 && keyCode <= 105) // 0 ~ 9 (Num Lock)
+		|| (keyCode == 189 && !event.shiftKey) // Shift 없이 -
+		|| (keyCode == 109) // - (Num Lock)
+		)) {
+		$(this).val(value);
+		event.returnValue = false;
+	}
+});
+$(document).on("keyup", "[data-valid-type=phone]", function() { $(this).val($(this).val().replace(/[^\d-]/gi, "")); });
+$(document).on("keydown", "[data-valid-type=phone]", function() {
+	var value = $(this).val();
+
+	var keyCode = gagajf.getKeyCode();
+	if (keyCode == -1)
+		return true;
+
+	if (!((keyCode >= 48 && keyCode <= 57 && !event.shiftKey) // 0 ~ 9
+		|| (keyCode >= 96 && keyCode <= 105) // 0 ~ 9 (Num Lock)
+		|| (keyCode == 189 && !event.shiftKey) // Shift 없이 -
+		|| (keyCode == 109) // - (Num Lock)
+		)) {
+		$(this).val(value);
+		event.returnValue = false;
+	}
+});
+$(document).on("keyup", "[data-valid-type=ipAddress]", function() { $(this).val($(this).val().replace(/[^\d\.]/gi, "")); });
+$(document).on("keydown", "[data-valid-type=ipAddress]", function() {
+	var value = $(this).val();
+
+	var keyCode = gagajf.getKeyCode();
+	if (keyCode == -1)
+		return true;
+
+	if (!((keyCode >= 48 && keyCode <= 57 && !event.shiftKey) // 0 ~ 9
+		|| (keyCode >= 96 && keyCode <= 105) // 0 ~ 9 (Num Lock)
+		|| (keyCode == 190 && !event.shiftKey) // .
+		)) {
+		$(this).val(value);
+		event.returnValue = false;
+	}
+});
+$(document).on("keyup", "[data-valid-type=bizRegNo]", function() { $(this).val($(this).val().replace(/[^0-9\-]/gi,"")); });

BIN
src/main/webapp/ux/plugins/gaga/loader.gif


File diff suppressed because it is too large
+ 1 - 0
src/main/webapp/ux/plugins/jquery/jquery-1.12.4.min.js


File diff suppressed because it is too large
+ 5 - 0
src/main/webapp/ux/plugins/jquery/jquery-ui.min.js


+ 1 - 0
src/main/webapp/ux/plugins/jquery/jquery.serializeObject.min.js

@@ -0,0 +1 @@
+$.fn.serializeObject=function(){"use strict";var a={},b=function(b,c){var d=a[c.name];"undefined"!=typeof d&&d!==null?$.isArray(d)?d.push(c.value):a[c.name]=[d,c.value]:a[c.name]=c.value};return $.each(this.serializeArray(),b),a};

Some files were not shown because too many files changed in this diff