API 통신시 Unix Timestamp가 필요한 경우가 있다.

소스

1
2
3
4
// === PHP time();
var timestamp = Math.round(new Date().getTime() / 1000);
// 또는
// new Date().getTime() 을 Date.now() 로 바꿀 수 있다.

여담

lodash 라이브러리를 사용하면 _.now() 함수로 현재 타임스탬프를 가져올 수 있다.
소스처럼 1,000으로 나눠줘야한다.

소스

1
2
3
4
5
6
7
8
9
// function
function comma(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

// prototype
Number.prototype.format = function () {
return this.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};

설명

첫번째는 함수 호출방식이고, 두번째는 NumberValue.format(); 으로 호출하면 된다.

가끔 견적서에 한글 숫자를 써야할 때가 있다.

소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* [num2han 숫자를 한글로 변환]
* @param {[integer]} num [숫자]
* @return {[string]} [한글 숫자]
* @author http://www.phpschool.com/gnuboard4/bbs/board.php?bo_table=tipntech&wr_id=14981
*/
var num2han = function (num) {
var i,
j = 0,
k = 0;
var han1 = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구'];
var han2 = ['', '만', '억', '조', '경', '해', '시', '양', '구', '간'];
var han3 = ['', '십', '백', '천'];
var result = '';
var hangul = String(num);
var pm = ''; // 부호
var str = [],
str2 = '';
var 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;
};

설명

숫자를 파라미터로 보내 사용하면 변환된 한글 숫자가 리턴된다.
출처는 소스상에 남겼다. 해당 스크립트를 문법에 맞게 조금 변경했다.

기본적으로 사용하는 기능 외에 custom method 를 추가해서 validation 을 해보자.

목차

  1. 사업자등록번호
  2. 법인등록번호
  3. 바이트 제한
  4. 아이디 체크 (alphanumeric, 숫자 첫글자 불가능)
  5. 비밀번호 체크 (alpah && (number || special char))
  6. datetime (YYYY-MM-DD HH:mm:ss)
  7. date (YYYY-MM-DD)
  8. Kakaotalk Yellow ID
  9. alphanumeric (hyphen, underscore, space 포함)
  10. phone (hyphen 포함)
  11. mobile (hyphen 포함)

소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
(function ($) {
$.validator.addMethod(
'biznum',
function (bizID, element) {
var checkID = [1, 3, 7, 1, 3, 7, 1, 3, 5, 1];
var 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) {
var result = true;
if (corpID.length === 13) {
var arr_regno = corpID.split('');
var arr_wt = [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2];
var iSum_regno = 0;
var 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) {
var byte = 0;
var result = true;

for (var 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…와 같은 속성을 추가해 사용하면 된다.
필요한 부분만 복사해 가져가도 되고.

예제

1
2
3
4
5
6
7
8
9
10
$('form').validate({
rules:{
text_field: {byte:80},
date_field: {date:true}
},
messages:{
text_field: {byte:'80자 초과'}
date_field: {date:'날짜 형식 아님'}
}
});

여담

byte check 의 함수 로직이 많지만, 한글 및 특수문자를 2byte 로 정확히 체크해주는 것은 위의 함수 뿐이였다.

jQuery Validation with Materialize CSS
Materialize CSS 와 연동해 사용할 수 있다.

소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// p 태그를 이용하는 방법
$.validator.setDefaults({
errorClass: 'invalid form-error red-text',
errorElement: 'p',
errorPlacement: function (error, element) {
var e = element.get(0);
if (e.type === 'radio' || e.type === 'checkbox') {
var $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) {
var $label = $(element)
.closest('form')
.find("label[for='" + element.attr('id') + "']");

$label.attr('data-error', error.text());
$label.addClass('active');
},
});

여담

bootstrap tooltip 을 활용한 validation 처럼 toast 를 활용한 플러그인이 나오면 좋으련만…

기본 기능만으론 checkbox 나 radio 사용시에 첫번째 element 뒤에 글자가 삽입이 되서 위치를 지정해주어야한다.

소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<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 모듈을 사용하면 다 해결된다.
아니라면… 사진 값을 바이너리로 읽어 처리해야한다.

설치

1
2
# bower install https://github.com/blueimp/JavaScript-Load-Image.git --save
$ yarn add blueimp-load-image

소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<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 데이터를 이미지로 변환해주게 로직을 짜면 된다.

Datetimepicker JS with Moment JS

개발을 하다보면 datetime 이 같이 필요한 경우가 생긴다.
pickadate 를 사용해 date 와 time picker 를 모두 사용해 만들 수도 있지만
datetimepicker가 편하고 쉽다.

설치

1
2
3
4
# datetimepicker
$ bower install https://github.com/xdan/datetimepicker.git --save
# moment
$ bower install moment --save

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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

텍스트 복사는 zeroclipboard 를 사용하라고 많이 나오는데, flash 를 이용해 복사하는 방법이다.
더 쉽고 간편하게 ClipboardJS 라이브러리를 사용해보자

설치

1
2
3
4
# npm
$ npm install clipboard --save

$ yarn add clipboard

소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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

소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* [searchAddr 다음주소검색 API]
* @return {[JsonArray]} [주소데이터]
* <script src="https://ssl.daumcdn.net/dmaps/map_js_init/postcode.v2.js"> 선행되어야함
*/
var searchAddr = function () {
new daum.Postcode({
oncomplete: function (data) {
var fullAddr = ''; // 최종 주소 변수
var extraAddr = ''; // 조합형 주소 변수
var engAddr = '';
var 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 를 입맛에 맞게 조금 변경했다.