숫자 3자리 단위로 comma 추가 - 정규식 활용
숫자를 한글 숫자로 변환
가끔 견적서에 한글 숫자를 써야할 때가 있다.
소스
/**
* [num2han 숫자를 한글로 변환]
* @param {[integer]} num [숫자]
* @return {[string]} [한글 숫자]
* @author http://www.phpschool.com/gnuboard4/bbs/board.php?bo_table=tipntech&wr_id=14981
*/
const num2han = function (num) {
let i,
j = 0,
k = 0;
const han1 = ["", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구"];
const han2 = ["", "만", "억", "조", "경", "해", "시", "양", "구", "간"];
const han3 = ["", "십", "백", "천"];
let result = "";
let hangul = String(num);
let pm = ""; // 부호
let str = [],
str2 = "";
const strTmp = [];
if (Number(num) === 0) {
return "영";
}
if (hangul.substring(0, 1) === "-") {
pm = "마이너스 ";
hangul = hangul.substring(1, hangul.length);
}
if (hangul.length > han2.length * 4) {
return "too much number";
}
for (i = hangul.length; i > 0; i = i - 4) {
str[j] = hangul.substring(i - 4, i);
for (k = str[j].length; k > 0; k--) {
strTmp[k] = str[j].substring(k - 1, k) ? str[j].substring(k - 1, k) : "";
strTmp[k] = han1[parseInt(strTmp[k])];
if (strTmp[k]) {
strTmp[k] += han3[str[j].length - k];
}
str2 = strTmp[k] + str2;
}
str[j] = str2;
if (str[j]) {
result = str[j] + han2[j] + result;
}
// 4자리마다 한칸씩 띄고 보여준다.
//result = (str[j])? " "+str[j]+han2[j]+result : " " + result;
j++;
str2 = "";
}
return pm + result;
};
설명
숫자를 파라미터로 보내 사용하면 변환된 한글 숫자가 리턴된다. 출처는 소스상에 남겼다. 해당 스크립트를 문법에 맞게 조금 변경했다.
jQuery Validation Custom Methods
기본적으로 사용하는 기능 외에 custom method 를 추가해서 validation 을 해보자.
목차
- 사업자등록번호
- 법인등록번호
- 바이트 제한
- 아이디 체크 (alphanumeric, 숫자 첫글자 불가능)
- 비밀번호 체크 (alpha && (number || special char))
- datetime (YYYY-MM-DD HH:mm:ss)
- date (YYYY-MM-DD)
- Kakaotalk Yellow ID
- alphanumeric (hyphen, underscore, space 포함)
- phone (hyphen 포함)
- mobile (hyphen 포함)
소스
(function ($) {
$.validator.addMethod(
"biznum",
function (bizID, element) {
const checkID = [1, 3, 7, 1, 3, 7, 1, 3, 5, 1];
let tmpBizID,
i,
chkSum = 0,
c2,
remander;
bizID = bizID.replace(/-/gi, "");
for (i = 0; i <= 7; i++) {
chkSum += checkID[i] * bizID.charAt(i);
}
c2 = "0" + checkID[8] * bizID.charAt(8);
c2 = c2.substring(c2.length - 2, c2.length);
chkSum += Math.floor(c2.charAt(0)) + Math.floor(c2.charAt(1));
remander = (10 - (chkSum % 10)) % 10;
return this.optional(element) || Math.floor(bizID.charAt(9)) === remander;
},
"사업자등록번호 형식에 맞지 않습니다",
);
$.validator.addMethod(
"corpnum",
function (corpID, element) {
let result = true;
if (corpID.length === 13) {
const arr_regno = corpID.split("");
const arr_wt = [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2];
let iSum_regno = 0;
let iCheck_digit = 0;
for (i = 0; i < 12; i++) {
iSum_regno += Number(arr_regno[i]) * Number(arr_wt[i]);
}
iCheck_digit = 10 - (iSum_regno % 10);
iCheck_digit = iCheck_digit % 10;
if (iCheck_digit !== arr_regno[12]) {
result = false;
}
} else {
result = false;
}
return this.optional(element) || result;
},
"법인등록번호 형식에 맞지 않습니다",
);
$.validator.addMethod(
"byte",
function (str, element, param) {
let byte = 0;
let result = true;
for (let i = 0, len = text.length; i < len; i++) {
if (escape(text.charAt(i)).length > 4) {
byte = byte + 2;
} else {
byte = byte + 1;
}
}
if (byte > param) {
result = false;
}
return this.optional(element) || result;
},
"최대 Byte 값을 넘었습니다",
);
// id 체크 (alphanumeric, _- 가능, 숫자가 처음에 올수 없음)
$.validator.addMethod(
"user",
function (id, element) {
return (
this.optional(element) ||
/^([a-zA-Z])[a-zA-Z_-]*[\w_-]*[\S]$|^([a-zA-Z])[0-9_-]*[\S]$|^[a-zA-Z]*[\S]$/.test(
id,
)
);
},
"올바른 아이디 형식이 아닙니다",
);
// pw 영문 && (숫자 || 특수문자)
$.validator.addMethod(
"pass",
function (pass, element) {
return (
this.optional(element) ||
/^(?=.*[a-zA-Z])((?=.*\d)|(?=.*\W))./.test(pass)
);
},
"올바른 비밀번호 형식이 아닙니다",
);
// datetime 형식
$.validator.addMethod(
"datetime",
function (datetime, element) {
return (
this.optional(element) ||
/^\d{4}-(0[1-9]|1[0-2])-([0-2]\d|3[01]) (0\d|1\d|2[0-3]):[0-5]\d:[0-5]\d$/.test(
datetime,
)
);
},
"올바른 날짜, 시간형식이 아닙니다",
);
// date 형식
$.validator.addMethod(
"date",
function (dt, element) {
return (
this.optional(element) ||
/^\d{4}-(0[1-9]|1[0-2])-([0-2]\d|3[01])$/.test(dt)
);
},
"올바른 날짜 형식이 아닙니다",
);
// 옐로아이디 형식
$.validator.addMethod(
"yellowid",
function (yid, element) {
return this.optional(element) || /^@[\W|\w]{2,15}/.test(yid);
},
"올바른 옐로아이디가 아닙니다",
);
// alpahnumeric _ - space
$.validator.addMethod(
"alphanumeric",
function (v, element) {
return this.optional(element) || /^[a-zA-Z\d\-_\s]+$/.test(v);
},
"올바른 형식이 아닙니다",
);
// 하이픈을 포함한 전화번호
$.validator.addMethod(
"phone",
function (p, element) {
return this.optional(element) || /^\d{2,3}-\d{3,4}-\d{4}$/.test(p);
},
"올바른 전화번호 형식이 아닙니다",
);
// 하이픈을 포함한 휴대폰 번호
$.validator.addMethod(
"mobile",
function (m, element) {
return (
this.optional(element) ||
/^01([0|1|6|7|8|9]?)-(\d{3,4})-(\d{4})$/.test(m)
);
},
"올바른 휴대폰 번호 형식이 아닙니다",
);
})(jQuery);
설명
biznum, byte...와 같은 속성을 추가해 사용하면 된다. 필요한 부분만 복사해 가져가도 되고.
예제
$("form").validate({
rules: {
text_field: { byte: 80 },
date_field: { date: true },
},
messages: {
text_field: { byte: "80자 초과" },
date_field: { date: "날짜 형식 아님" },
},
});
여담
byte check 의 함수 로직이 많지만, 한글 및 특수문자를 2byte 로 정확히 체크해주는 것은 위의 함수 뿐이였다.
jQuery Validation과 Materialize의 연동
jQuery Validation with Materialize CSS Materialize CSS 와 연동해 사용할 수 있다.
소스
// p 태그를 이용하는 방법
$.validator.setDefaults({
errorClass: "invalid form-error red-text",
errorElement: "p",
errorPlacement: function (error, element) {
const e = element.get(0);
if (e.type === "radio" || e.type === "checkbox") {
const $a = error.appendTo(element.parent());
$a.css({ "margin-top": "10px" });
} else {
error.appendTo(element.parent());
}
},
});
// 더 예쁜 방법
$.validator.setDefaults({
errorClass: "invalid",
validClass: "valid",
errorPlacement: function (error, element) {
const $label = $(element)
.closest("form")
.find("label[for='" + element.attr("id") + "']");
$label.attr("data-error", error.text());
$label.addClass("active");
},
});
여담
bootstrap tooltip 을 활용한 validation 처럼 toast 를 활용한 플러그인이 나오면 좋으련만...
jQuery Validation Error Handling 및 focus, target 설정
기본 기능만으론 checkbox 나 radio 사용시에 첫번째 element 뒤에 글자가 삽입이 되서 위치를 지정해주어야한다.
소스
<script>
$.validator.setDefaults({
onfocusout: false,
invalidHandler: function (form, validator) {
// 커스텀 포커스 핸들링
if (validator.numberOfInvalids()) {
validator.errorList[0].element.focus();
//alert(validator.errorList[0].message); // 경고창
}
},
errorClass: "text-danger", // 에러 스타일을 입힐 클래스 지정
errorPlacement: function (error, element) {
// data-error 속성으로 해당 위치 삽입
var placement = $(element).data("error");
if (placement) {
$(placement).append(error);
} else {
// 없을경우 마지막노드 뒤에 삽입
element.parent().children().last().after(error);
}
},
});
</script>
<!-- data-error 속성 사용 예시 -->
<input type="text" name="id" data-error="#id_error" />
<p id="id_error"></p>
설명
data-error attribute 를 통해 원하는 위치에 에러를 띄울 수 있고, 그렇지 않을 경우 마지막 노드 뒤에 에러를 출력한다.
7 줄에 주석을 지우면 첫번째 오류를 alert 으로 띄울 수 있다.
비동기 이미지 업로드 - 리사이징 및 이미지 회전을 포함
비동기 이미지 업로드
비동기 업로드를 이용하면 멀티 이미지 업로드나 모바일 이미지 업로드를 쉽게 처리할 수 있다.
모바일 이미지 업로드시에는 exif 속성에 따라 (카메라로 찍은 방향에 따라) 이미지가 회전되어서 올라가게 되고, 비율로 이미지를 표시해야 되기 때문에 리사이징이 필요하다.
이 부분은 load-image 모듈을 사용하면 다 해결된다. 아니라면.. 사진 값을 바이너리로 읽어 처리해야한다.
설치
## bower install https://github.com/blueimp/JavaScript-Load-Image.git --save
$ yarn add blueimp-load-image
소스
<script type="text/javascript">
var imageModule = (function () {
"use strict";
var possible = window.File && window.FileReader && window.FormData; // html5 업로드를 지원하는지의 여부
var apiPath = "/api/test.php"; // 업로드 서버 처리 경로
var folder = "/upload/review/"; // 기본 이미지 업로드 폴더
var $input;
var maxWidth = 600;
var maxHeight = 480;
var imageArray = []; // callback에 사용할 이미지 파일명 배열
var callback;
/**
* [sendImageFile 이미지 비동기 업로드]
* @scope {[private]}
* @param {[string]} imageData [이미지 binary]
* @return {[function]} callback [업로드 후처리]
*/
function sendImageFile(imageData) {
if (imageData) {
var formData = new FormData();
formData.append("type", "upload");
formData.append("folder", folder);
formData.append("imageData", imageData);
$.ajax({
type: "post",
url: apiPath,
data: formData,
dataType: "json",
contentType: false, // contentType header 제거
processData: false, // Dom 객체를 전송하려면 false 처리해야함
})
.then(function (data) {
if (data.data) {
imageArray.push(data.data.file); // 이미지 배열에 저장
$input.val("");
if (callback) {
callback(imageArray);
} else {
console.log("callback function not initialized");
}
}
})
.catch(function (error) {
console.log("upload fail: ", error);
});
} else {
console.log("image data is null");
}
}
return {
/**
* [init 이미지 모듈 초기화]
* @param {[string]} fileId [변경할 input id]
* @param {[function]} callbackFunc [후처리]
* @param {[object]} settings [width, height, folder 설정가능]
*/
init: function (fileId, callbackFunc, settings) {
if (possible) {
if (typeof settings === "object") {
if (settings.width) {
maxWidth = settings.width;
}
if (settings.height) {
maxHeight = settings.height;
}
if (settings.folder) {
folder = settings.folder;
}
}
if ($.isFunction(callbackFunc)) {
callback = callbackFunc;
} else {
console.log("callback is not defined");
}
var options = {
maxWidth: maxWidth, // resize width 값
maxHeight: maxHeight, // resize height 값
canvas: true, // 사진을 돌리려면 canvas 객체로 받아야함
downsamplingRatio: 0.5, // 비율에 맞추어 크기 감소
};
$input = $("#" + fileId);
$input.on("change", function (e) {
e.preventDefault();
e = e.originalEvent;
var target = e.dataTransfer || e.target;
var file = target && target.files && target.files[0];
loadImage.parseMetaData(file, function (data) {
// exif js를 사용해 이미지의 tag를 가져옴
if (data.exif) {
options.orientation = data.exif.get("Orientation"); // 화면이 돌아간 비율을 지정
} else {
if (options.orientation) {
options.orientation = null;
}
}
// 캔버스 이미지 리사이징 처리 후 서버호출
loadImage(
file,
function (img) {
sendImageFile(img.toDataURL());
},
options,
);
});
});
} else {
console.log("file upload is not supported");
}
},
/**
* [getFolder 업로드 경로 호출]
* @return {[string]} [경로]
*/
getFolder: function () {
return folder;
},
/**
* [delete 이미지 삭제]
* @param {[number]} idx [이미지 배열의 인덱스]
* @param {[function]} callbackFunc [delete 후처리]
*/
delete: function (idx, callbackFunc) {
if (imageArray[idx]) {
$.ajax({
url: apiPath,
type: "post",
data: {
type: "delete",
folder: folder,
filename: imageArray[idx],
},
dataType: "json",
})
.then(function (data) {
console.log("file delete success");
console.log(data);
if ($.isFunction(callbackFunc)) {
callbackFunc(data);
}
})
.catch(function (error) {
console.log("server file delete failed");
console.log(error);
})
.done(function () {
imageArray.splice(idx, 1);
$input.val("");
if (callback) {
callback(imageArray);
} else {
console.log("callback function not initialized");
}
});
} else {
console.log("delete index not defined");
}
},
clear: function () {
console.log("clear array");
imageArray = [];
},
};
})();
$(function () {
imageModule.init(
"fileInput",
function (data) {
var folder = imageModule.getFolder();
var str = "";
for (var i = 0, len = data.length; i < len; i++) {
str +=
'<img src="' +
folder +
data[i] +
'" style="width:100%;height:100%;">';
}
$("#area").html(str);
$("#fileData").val(JSON.stringify(data));
},
{},
);
// options, width height folder..
// {width:500, height:300, folder:'/upload/board/'}
});
// after insert into database
$("#area").empty();
$("#fileData").val("");
imageModule.clear();
</script>
<input type="file" name="imageFile" id="fileInput" />
설명
코드양이 되게 긴데, 5-6 줄에서 업로드 서버 처리 경로 및 기본 업로드 폴더를 지정해준다. 9-10 줄에서는 최대 가로 세로(px)을 지정해준다. 비율로 줄어들기 때문에 걱정하지말고 지정하자
사용법은 169 줄 이후를 보면 된다. callback 함수를 통해 올라간 이미지를 바로 보여주게 처리할 수 있다. 184 줄부터는 db 와의 통신이 끝난 후 화면을 초기화해주는 부분이다.
127 줄의 imageModule.delete
함수를 사용해 업로드 된 사진을 삭제할 수 있다.
현재는 api/test.php
로 서버의 업로드 처리 로직을 구현해놨는데,
type 을 받아 delete 일 경우 해당 이미지를 삭제,
아닐 경우 binary 데이터를 이미지로 변환해주게 로직을 짜면 된다.
jQuery DateTimePicker와 Moment JS의 연동
Datetimepicker JS with Moment JS
개발을 하다보면 datetime 이 같이 필요한 경우가 생긴다. pickadate 를 사용해 date 와 time picker 를 모두 사용해 만들 수도 있지만 datetimepicker가 편하고 쉽다.
설치
## datetimepicker
$ bower install https://github.com/xdan/datetimepicker.git --save
## moment
$ bower install moment --save
예제
<link
rel="stylesheet"
type="text/css"
href="/bower_components/datetimepicker/build/jquery.datetimepicker.min.css"
/>
<!-- php.date.formatter.js 와 jquery.mousewheeel.js 를 포함하는 full.js-->
<!-- moment는 이미 선언되어있다고 가정합니다 -->
<script src="/bower_components/datetimepicker/build/jquery.datetimepicker.full.min.js"></script>
<script>
// dateTimePicker localization
$.datetimepicker.setLocale("ko");
// dateTimePicker moment.js와 연동
$.datetimepicker.setDateFormatter({
parseDate: function (date, format) {
var d = moment(date, format);
return d.isValid() ? d.toDate() : false;
},
formatDate: function (date, format) {
return moment(date).format(format);
},
});
$(function () {
// datetimepicker init
$(".datetimepicker").datetimepicker({
format: "YYYY-MM-DD HH:mm:ss",
formatTime: "HH:mm",
formatDate: "YYYY-MM-DD",
});
});
</script>
<input type="text" name="startDate" class="datetimepicker" /> ~
<input type="text" name="endDate" class="datetimepicker" />
여담
pickadate 에서 datetime 을 같이 지원해주는 모듈도 만들어주면 얼마나 좋을까?
Clipboard JS 사용법 - 브라우저 텍스트 복사
Clipboard JS
텍스트 복사는 zeroclipboard 를 사용하라고 많이 나오는데, flash 를 이용해 복사하는 방법이다. 더 쉽고 간편하게 ClipboardJS 라이브러리를 사용해보자
설치
## npm
$ npm install clipboard --save
$ yarn add clipboard
소스
<script src="/bower_components/clipboard/dist/clipboard.min.js"></script>
<!-- 1. URL copy -->
<a href="#" id="btnCopyUrl" data-clipboard-action="copy">url 복사</a>
<script>
$(function ({
// 복사 버튼을 만들시 data-clipboard-text 안에 복사할 문구를 넣어준다
$('#btnCopyUrl').attr('data-clipboard-text', document.location.href);
// callback 설정
var clipboard = new Clipboard('#btnCopyUrl');
clipboard.on('success', function(e) {
alert('복사되었습니다');
});
clipboard.on('error', function(e) {
console.log(e);
});
});
</script>
<!-- 2. Text copy -->
<textarea id="textBody" cols="30" rows="5"></textarea>
<button
type="button"
id="btnCopyText"
data-clipboard-action="copy"
data-clipboard-target="#textBody"
>
텍스트복사
</button>
설명
html5 attribute 를 사용해 쉽게 제어가 가능하다.
신규 브라우저들은 다 지원하지만 (window.clipboard 객체가 있는 브라우저) iPhone 에선 clipboard 액세스가 모두 막혀있어서 복사를 할 수 없다.
다음 주소 검색 API
다음 주소검색 API
소스
/**
* [searchAddr 다음주소검색 API]
* @return {[JsonArray]} [주소데이터]
* <script src="https://ssl.daumcdn.net/dmaps/map_js_init/postcode.v2.js"> 선행되어야함
*/
const searchAddr = function () {
new daum.Postcode({
oncomplete: function (data) {
let fullAddr = ""; // 최종 주소 변수
let extraAddr = ""; // 조합형 주소 변수
let engAddr = "";
let zipcode = "";
// 사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다.
if (data.userSelectedType === "R") {
// 사용자가 도로명 주소를 선택했을 경우
fullAddr = data.roadAddress;
zipcode = data.zonecode;
engAddr = data.roadAddressEnglish;
//법정동명이 있을 경우 추가한다.
if (data.bname !== "") {
extraAddr += data.bname;
}
// 건물명이 있을 경우 추가한다.
if (data.buildingName !== "") {
extraAddr +=
extraAddr !== "" ? ", " + data.buildingName : data.buildingName;
}
// 조합형주소의 유무에 따라 양쪽에 괄호를 추가하여 최종 주소를 만든다.
fullAddr += extraAddr !== "" ? " (" + extraAddr + ")" : "";
} else {
// 사용자가 지번 주소를 선택했을 경우(J)
fullAddr = data.jibunAddress;
zipcode = data.postcode;
engAddr = data.jibunAddressEnglish;
}
// 구버전일 경우 getElementById 로 변경
document.querySelector("#zip").value = zipcode;
document.querySelector("#address").value = fullAddr;
document.querySelector("#address_eng").value = engAddr;
document.querySelector("#address_detail").focus();
},
}).open();
};
설명
다음 주소검색 API 를 입맛에 맞게 조금 변경했다.