if (!$) { throw 'jQuery에 의존하는 컴포넌트입니다. jQuery를 로드해주세요.' }function bind(card) {
card._$toggle.on('click', onClickToggle.bind(card._$toggle, card));
card._$save.on('click', onClickSave.bind(card._$save, card));
card._$edit.on('click', onClickEdit.bind(card._$edit, card));
card._$delete.on('click', onClickDelete.bind(card._$delete, card));
}
function onClickToggle(card, e) {
e.preventDefault();
card.toggleContent();
}
function onClickSave(card, e) {
e.preventDefault()
card.save();
}
function onClickEdit(card, e) {
e.preventDefault();
card.edit();
}
function onClickDelete(card, e) {
e.preventDefault();
card.delete();
}-
생성자 함수 Card
-
new를 강제화하는 검증을 한다.
-
초기에 카드 컴포넌트로 사용할 DOM 요소를 jQuery화 해준다.
-
카드 컴포넌트를 초기화 한다. (init 함수 실행)
function Card(o) {
if (!this) { throw 'new Card() 문법으로 사용해주세요.'; }
this._$card = $(o);
this.init();
}-
생성자 함수 Card의 프로토타입의 속성을 지정 (constructor, version)
-
생성자 함수 Card의 프로토타입의 속성으로 메서드 지정 (init, isOpenedContent, openContent, closeContent, toggleContent, save, edit, delete)
-
init 메서드
Card {} 내부의 버튼 컴포넌트를 참조시킨다. bind 함수를 통해 this, 즉 Card {} 를 전달하는 이벤트 바인딩을 실행시킨다.
-
isOpenedContent 메서드
-
openContent 메서드
aria-label 속성값이 open인 토글 버튼의 속성값을 close로 바꿔준다. fa-angle-up 클래스를 가진 토글 버튼의 클래스를 지우고 fa-angle-down 클래스를 추가해준다. card-content 클래스를 찾아 slideDown 메서드를 실행시킨다.
* 참고링크 slideDown 메서드 - jQuery.com
-
closeContent 메서드
aria-label 속성값이 close인 토글 버튼의 속성값을 open으로 바꿔준다. fa-angle-down 클래스를 가진 토글 버튼의 클래스를 지우고 fa-angle-up 클래스를 추가해준다. card-content 클래스를 찾아 slideUp 메서드를 실행시킨다.
* 참고링크 slideDown 메서드 - jQuery.com
-
toggleContent 메서드
-
save 메서드
content 클래스를 가진 객체의 <br>, <hr> 요소를 제외한 모든 요소를 찾아 contenteditable 속성을 지워준다.
* 참고링크 contenteditable 속성 - MDN
-
edit 메서드
-
delete 메서드
Card.fn = Card.prototype = {
constructor: Card,
version: '1.0.0',
init: function(){
this._$toggle = this._$card.find('.card-toggle-btn');
this._$save = this._$card.find('.card-save-btn');
this._$edit = this._$card.find('.card-edit-btn');
this._$delete = this._$card.find('.card-delete-btn');
bind(this);
},
isOpenedContent: function() {
return !this._$toggle.attr('aria-label').includes('open');
},
openContent: function(){
var change_open_text = this._$toggle.attr('aria-label').replace('open', 'close');
this._$toggle.attr('aria-label', change_open_text);
this._$toggle.find('.fa-angle-up').addClass('fa-angle-down').removeClass('fa-angle-up');
this._$card.find('.card-content').slideDown(100);
},
closeContent: function(){
var $toggle = this._$toggle;
var change_open_text = $toggle.attr('aria-label').replace('close', 'open');
$toggle.attr('aria-label', change_open_text);
$toggle.find('.fa-angle-down').addClass('fa-angle-up').removeClass('fa-angle-down');
this._$card.find('.card-content').slideUp(100);
},
toggleContent: function(){
this.isOpenedContent() ? this.closeContent() : this.openContent();
},
save: function(){
this._$card.find('.content *:not(br,hr)').removeAttr('contenteditable');
},
edit: function(){
this._$card.find('.content *:not(br,hr)').attr('contenteditable', true).eq(0).focus();
},
delete: function(){
this._$card.remove();
}
}; $.Card = Card; global.t_card = new $.Card('.twitter-card');
global.f_card = new $.Card('.facebook-card');
global.t_card.toggleContent();
global.f_card.toggleContent();
$('.demo-radio').find('[role="tab"]').on('click', function(){
$(this).parent().radioClass('is-active');
});- jQuery 인스턴스 메서드(플러그인) 작성 패턴
(function($){
'use strict';
if ( !$.fn.radioClass ) {
$.fn.radioClass = function(name='active'){
// this === jQuery {}
// jQuery의 능력을 사용할 수 있다.
// 플러그인 내에서는....
this.siblings(`.${name}`).removeClass(name);
this.addClass(name);
// 메서드 체이닝을 위해서
return this;
};
}
})(window.jQuery);- 모바일 환경에서 사용 가능한 touch event와 데스크탑 환경에서 사용 가능한 mouse event를 사용한 swipe 메서드
// 사용자 접속 환경이 mobile 인지 desktop 인지 확인
if ('ontouchstart' in window || navigator.msMaxTouchPoints)...jQuery.event 객체의 type, client 좌표 값 사용
-
jQuery.event.type
- mousedown
- mousemove
- mouseup
-
client 좌표
- jQuery.event.clientX
- jQuery.event.clientY
-
mousedown 이벤트 발생 시점의 client X,Y 좌표값과 mousemove, mouseup 이벤트 발생 시점의 client X,Y 좌표값을 비교하여 swipe direction을 판별할 수 있다.
-
명확한 mousemove 이벤트 감지를 위해 document context의 mousedown 상태 값을 확인하는 부분이 필요
$(document).on({
'mousedown': function(){
global.is_global_mousedown_state = true;
},
'mouseup': function(){
global.is_global_mousedown_state = false;
}
});jQuery.event 객체의 type, changedTouches의 좌표 값 사용
-
jQuery.event.type
- touchstart
- touchmove
- touchend
-
jQuery.event.changedTouches[0]
- jQuery.event.changedTouches[0].clientX
- jQuery.event.changedTouches[0].clientY
- touchstart 이벤트 발생 시점의 client X,Y 좌표값과 touchmove, touchend 이벤트 발생 시점의 client X,Y 좌표값을 비교하여 swipe direction을 판별할 수 있다.
참고링크1. jQuery Mobile에서 제공하고 있는 swipe 참고링크2. jQuery의 Plug-in
carousel의 구조는 아래와 같다. 구조를 설명하기 위해 구체적인 아이템 숫자 등은 제외했다. 이어지는 예제에서 아이템 갯수는 8개로 가정한다.
<article id="bs3-headphone" class="ui-carousel">
<h1 class="ui-carousel-headline">Beats Solo3 Wireless On-Ear Headphones – Rose Gold</h1>
<div role="tablist" class="ui-carousel-tablist">
<ul role="presentation">
<li class="active"><a class="ui-carousel-tab" href="" role="tab" aria-label="Headphone View Front"></a></li>
<li><a class="ui-carousel-tab" href="" role="tab" aria-label="Headphone View 1"></a></li>
<li><a class="ui-carousel-tab" href="" role="tab" aria-label="Headphone View 2"></a></li>
<li><a class="ui-carousel-tab" href="" role="tab" aria-label="Headphone View 3"></a></li>
<li><a class="ui-carousel-tab" href="" role="tab" aria-label="Headphone View 4"></a></li>
<li><a class="ui-carousel-tab" href="" role="tab" aria-label="Headphone View 5"></a></li>
<li><a class="ui-carousel-tab" href="" role="tab" aria-label="Headphone View 6"></a></li>
<li><a class="ui-carousel-tab" href="" role="tab" aria-label="Headphone View 7"></a></li>
</ul>
</div>
<div class="ui-carousel-button-group" role="group">
<button type="button" class="ui-carousel-prev-button" aria-label="previous content"></button>
<button type="button" class="ui-carousel-next-button" aria-label="next content"></button>
</div>
<div role="group" class="ui-carousel-tabpanel-wrapper">
<figure class="ui-carousel-tabpanel" role="tabpanel">
<img src="images/MNET2.jpeg" alt="MNET2">
</figure>
<figure class="ui-carousel-tabpanel" role="tabpanel">
<img src="images/MNET2_AV1.jpeg" alt="MNET2_AV1">
</figure>
<figure class="ui-carousel-tabpanel active" role="tabpanel">
<img src="images/MNET2_AV2.jpeg" alt="MNET2_AV2">
</figure>
<figure class="ui-carousel-tabpanel" role="tabpanel">
<img src="images/MNET2_AV3.jpeg" alt="MNET2_AV3">
</figure>
<figure class="ui-carousel-tabpanel" role="tabpanel">
<img src="images/MNET2_AV4.jpeg" alt="MNET2_AV4">
</figure>
<figure class="ui-carousel-tabpanel" role="tabpanel">
<img src="images/MNET2_AV5.jpeg" alt="MNET2_AV5">
</figure>
<figure class="ui-carousel-tabpanel" role="tabpanel">
<img src="images/MNET2_AV6.jpeg" alt="MNET2_AV6">
</figure>
<figure class="ui-carousel-tabpanel" role="tabpanel">
<img src="images/MNET2_AV7.jpeg" alt="MNET2_AV7">
</figure>
</div>
</article>이 예제에서 의존하는 script는 jQuery library 외에 아래 세가지다
<!-- jQuery용 cache plug-in -->
<script src="../jQuery.Extension/utilities/jquery.cache.js"></script>
<!-- jQuery용 radioClass plug-in -->
<script src="../jQuery.Extension/plugins/jquery.radioClass.js"></script>
<!-- jQuery 캐러셀 플러그인 모듈 로드 -->
<script src="js/jquery.ui.carousel.js"></script>cache plug-in은 jQuery의 .data() static method를 이용하여 인자값으로 받은 DOM element, dom_el의 jQuery화 객체를 그 자신에게 bind한다. 이때 bind 된 jQuery화 객체의 member name은 .$this이다.
(function(global, $){
'use strict';
var cache = function(dom_el) {
// 검증
// document.ELEMENT_NODE
if ( dom_el.nodeType !== 1 ) { throw new Error('문서객체여야 합니다.') }
// 메모리 대상
// jQuery() 팩토리 함수를 사용(1회)한 결과를 저장
// jQuery.data() 유틸리티 메소드 활용
// <설정할 때> jQuery.data(dom_el, key, value)
// <가져올 때> jQuery.data(dom_el, key)
var $this = $.data(dom_el, '$this');
// console.log('$this:', $this);
return $this ? $this : $.data(dom_el, '$this', $(dom_el));
};
// jQuery에 본 module을 추가하는 loutine
if ( !$.cache ) {
$.cache = cache;
$.$ = $.cache; // Alias
}
})(this, this.jQuery);같은 부모를 가진 형제 요소들의 class_name을 제거하고 그 자신(요소)에게만 class_name을 할당한다.
(function(global, $){
'use strict';
var radioClass = function(class_name) {
// this === jQuery {}
// console.log('this:', this);
// console.log('this.jquery:', this.jquery);
// radioClass 가 적용된 자신(jQuery {})의 형제 중에
// class_name 값을 가진 형제에게서 해당 class_name을 제거
this.siblings('.'+class_name).removeClass(class_name);
// radioClass 적용된 jQuery {} 객체는
// class_name 이름의 class 속성이 추가
this.addClass(class_name);
};
// $.fn === jQuery.prototype
if ( !$.fn.radioClass ) {
$.fn.radioClass = radioClass;
}
})(this, this.jQuery);setting 객체가 이 컴포넌트 조작의 중심이다. active 동작은 activeTabPanel() 함수 호출을 통해 이루어 진다. 인디케이터나 <버튼, >버튼을 누르면 인디케이터의 경우 눌려진 인디케이터의 index를 인자로 activeTabPanel()을 호출하고 버튼들은 prevContent(), nextContent()를 통해 현재 인덱스에서 +1 또는 -1된 값을 인자로 activeTabPanel()을 호출한다. 이후 주역은 settings 객체다. settings 객체의 정보가 업데이트 된 후에 moveWrapperPosX()에 업드이트된 settings의 .active_index 값을 인자로 주며 호출하고 실질적인 이동이 일어난다. 또한 버튼을 이용한 이동시 인디케이터가 바뀌어야 함을 알려줄 때도 이 settings 객체가 활용된다. activeTabPanel() 내부에서 updateIndicators()에 settings의 .active_index 값을 인자로 주며 호출함으로써 이 동작이 일어난다. passive 동작인 animation auto play는 settings의 using_autoPlay값에 따라 기능 전체를 끄고 킬수 있다. 1주기에 걸리는 시간 역시 settings의 rolling_time이 관여한다. 실행중에 이 둘을 바꾸는 내부함수는 이 모듈에 없다. 자동재생을 일시정지/재생 하는 일은 전역객체의 setInterval과 clearInterval로 구현했다.
(function(global, $){
'use strict';
// 문서객체 참조
var document = global.document;
// 전역에 공개하여 사용하는 공통 함수
global.playAnimation = function(callback, time) {
time = time || 3000;
return global.setInterval(callback, time);
};
global.stopAnimation = function(stop_id) {
global.clearInterval(stop_id);
};
// 플러그인 모듈 내부 어디에서든 참조 가능하도록 객체 참조 변수 선언
var $widget,
$wrapper,
$panels,
$tablist,
$tabs,
$button_group,
panel_width,
// 초기 변수
stop_id = 0,
// 옵션 설정
settings,
defaults = {
'active_index' : 0,
'using_animation': true,
'using_autoPlay' : true,
'rolling_time' : 3000,
};
// 플러그인: 인스턴스 메소드
var carousel = function(options) {
settings = $.extend({}, defaults, options);
$widget = this;
// 컴포넌트 구현 초기화
init();
// 이벤트 연결
bindEvents();
};
// 초기화 함수
var init = function() {
// 캐러셀 컴포넌트 객체 참조
$tablist = $widget.find('.ui-carousel-tablist');
$tabs = $widget.find('.ui-carousel-tab');
$button_group = $widget.find('.ui-carousel-button-group');
$wrapper = $widget.find('.ui-carousel-tabpanel-wrapper');
$panels = $wrapper.find('.ui-carousel-tabpanel');
// 초기 실행
settingWrapperSize();
resizeCarouselHeight();
settings.using_autoPlay && autoPlay();
};
// 이벤트 연결 함수
var bindEvents = function() {
// 리사이즈 이벤트 핸들링
$(global).on({
'resize.change_carousel': resizeCarouselHeight,
'resize.change_wrapper': settingWrapperSize
});
// 탭 이벤트 핸들링
$.each($tabs, function(index) {
var $tab = $tabs.eq(index);
$tab.on({
'click': $.proxy(activeTabPanel, $tab, index),
'focus': stopPlay
});
});
// 버튼 이벤트 핸들링
$button_group.on('click', 'button', function(e){
$.$(this).index() ? nextContent() : prevContent();
});
// 자동재생 이벤트 핸들링
$widget.on({
'mouseenter': stopPlay,
'mouseleave': autoPlay
});
};
// 래퍼 객체의 너비 설정 함수
var settingWrapperSize = function() {
// 컴포넌트 너비 구하기
panel_width = $widget.width();
// 패널 너비를 컴포넌트 너비로 설정
$panels.width( panel_width );
// 래퍼 너비를 컴포넌트 너비 x 패널 개수로 설정
var wrapper_width = panel_width * $panels.length;
$wrapper.width(wrapper_width);
// 현재 활성화된 페이지를 재정렬
activeTabPanel(settings.active_index);
};
// 창 크기 조정에 따른 캐러셀 높이 조정 함수
var resizeCarouselHeight = function () {
$widget.height( $panels.height() );
};
// 탭패널 활성화하는 함수
var activeTabPanel = function(index, e) {
e && e.preventDefault();
// index 값으로 활성화 인덱스 업데이트
settings.active_index = index;
moveWrapperPosX( settings.active_index );
updateIndicators( settings.active_index );
};
// 래퍼 객체를 이동시키는 함수
var moveWrapperPosX = function(active_index) {
var distance_x = -panel_width * active_index + 'px';
settings.using_animation ?
$wrapper.stop().animate({'left': distance_x}, 600) :
$wrapper.css('left', distance_x);
};
// 업데이트 인디케이터 함수
var updateIndicators = function(active_index) {
$tabs.eq(active_index).parent().radioClass('active');
};
// 다음 콘텐츠 보기 함수
var nextContent = function() {
settings.active_index = ++settings.active_index % $panels.length;
activeTabPanel(settings.active_index);
};
// 이전 콘텐츠 보기 함수
var prevContent = function() {
settings.active_index = --settings.active_index < 0 ? ($panels.length - 1) : settings.active_index;
activeTabPanel(settings.active_index);
};
// 자동 재생 함수
var autoPlay = function() {
stop_id = global.playAnimation(nextContent, settings.rolling_time);
};
// 멈춤 함수
var stopPlay = function() {
global.stopAnimation(stop_id);
};
// 플러그인으로 함수 연결
if (!$.fn.carousel) {
$.fn.carousel = carousel;
// 초기 옵션 설정(외부에서 접근 가능)
$.fn.carousel.defaults = defaults;
}
})(this, this.jQuery);@font-face {
font-family: "Nova Thin";
src: url("../fonts/proximanova-thin-webfont.woff") format("woff")
}
@font-face {
font-family: "Nova Regular";
src: url("../fonts/proximanova-regular-webfont.woff") format("woff")
}
@font-face {
font-family: "Nova Bold";
src: url("../fonts/proximanova-bold-webfont.woff") format("woff")
}
.ui-carousel-tablist .ui-carousel-tab:focus,
.ui-carousel-button-group button:focus {
outline: none;
outline-offset: 10px;
box-shadow: 0 0 0 4px rgba(90, 206, 255, 0.45);
border-radius: 2px
}
.ui-carousel-tablist ul {
list-style: none;
margin-top: 0;
margin-bottom: 0;
padding-left: 0
}
.ui-carousel-button-group button {
border: none;
padding: 0;
background: transparent
}
.readable-hidden {
overflow: hidden;
position: absolute;
clip: rect(0, 0, 0, 0);
width: 1px;
height: 1px;
margin: -1px;
border: 0;
padding: 0
}
.readable-hidden.focusable:focus {
overflow: visible;
position: static;
clip: auto;
width: auto;
height: auto;
margin: 0
}
body {
margin: 0;
font-family: "Nova Regular";
color: #2b2b2b;
background: #fff;
height: 100vh;
display: flex;
justify-content: center;
align-items: center
}
::selection {
background: #ebc9c7;
color: #4f1e19
}
.ui-carousel {
overflow: hidden;
position: relative;
flex: 0 1 1200px;
height: 700px;
background: #fff;
margin-left: auto;
margin-right: auto
}
@media screen and (min-width: 660px) {
.ui-carousel {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(43, 43, 43, 0.2)
}
}
.ui-carousel-headline {
position: absolute;
z-index: 1000;
left: 30px;
top: -3px;
width: 10em;
margin: 0;
font-family: "Nova Thin";
font-size: 14px;
line-height: 1.34;
color: #ac807a
}
@media screen and (min-width: 660px) {
.ui-carousel-headline {
top: 30px
}
}
.ui-carousel-tablist {
position: absolute;
z-index: 1000;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
display: none
}
@media screen and (min-width: 660px) {
.ui-carousel-tablist {
display: block
}
}
.ui-carousel-tablist ul {
display: flex;
justify-content: center
}
.ui-carousel-tablist li {
margin-left: 8px;
margin-right: 8px
}
.ui-carousel-tablist li.active .ui-carousel-tab {
position: relative;
top: -1px;
background: #fff;
border: 1px solid #5aceff;
box-shadow: inset 0 0 10px rgba(90, 206, 255, 0.3)
}
.ui-carousel-tablist .ui-carousel-tab {
color: #999;
text-decoration: none;
display: block;
width: 10px;
height: 10px;
border-radius: 50%;
background: currentColor
}
.ui-carousel-tablist .ui-carousel-tab:focus {
border-radius: 50%
}
.ui-carousel-tablist .ui-carousel-tab:hover {
color: #44ace2
}
.ui-carousel-button-group button {
cursor: pointer;
opacity: 0.45;
position: absolute;
z-index: 1000;
top: 50%;
width: 30px;
height: 60px;
transform: translateY(-50%)
}
.ui-carousel-button-group button:hover {
opacity: 1
}
.ui-carousel-button-group button::before {
content: '';
position: absolute;
width: 40px;
height: 40px
}
.ui-carousel-button-group .ui-carousel-prev-button {
left: 20px
}
.ui-carousel-button-group .ui-carousel-prev-button::before {
top: 10px;
left: 10px;
transform: rotate(-45deg);
border-top: 1px solid rgba(43, 43, 43, 0.7);
border-left: 1px solid rgba(43, 43, 43, 0.7)
}
.ui-carousel-button-group .ui-carousel-next-button {
right: 20px
}
.ui-carousel-button-group .ui-carousel-next-button::before {
top: 10px;
right: 10px;
transform: rotate(45deg);
border-top: 1px solid rgba(43, 43, 43, 0.7);
border-right: 1px solid rgba(43, 43, 43, 0.7)
}
.ui-carousel-tabpanel-wrapper {
overflow: hidden;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center
}
.ui-carousel-tabpanel-wrapper img {
margin-top: 30px;
width: 50%;
height: auto
}
.ui-carousel-tabpanel {
margin: 0;
float: left;
width: 100%
}ICO MOON은 여러가지 폰트 아이콘을 한곳에 모아놓고, 원하는 아이콘만 뽑아 쓸 수 있는 사이트입니다. 이 사이트의 웹 아이콘을 이용하기 위해서는 유료 아이콘 팩을 구입하거나, 폰트 아이콘을 웹 폰트 형식으로 링크하여 이용할 수 있습니다. ICOMOON 사이트 우 상단 IcoMoon App 버튼 클릭 후 Add Icons From Library를 들어가면 다양한 폰트 아이콘을 이용할 수 있습니다. 원하는 아이콘을 선택 후, 필요에 따라 Cenerate SVG & More 버튼 또는 Generate Font 버튼을 클릭합니다.
https://youtu.be/_xCTEEacOpI?list=PLJJh8Uu1TAKGR7lr7aLa1K3cgkY6sEbcl
위의 영상 왼쪽은 WAI-ARIA를 적용하지 않은 경우이고 오른쪽은 WAI-ARIA를 적용한 경우입니다. 이때 왼쪽의 경우에는 <h1> 요소에 있는 내용을 음성 출력하기 때문에 팝업인지 정확히 알 수 없지만, 오른쪽 영상의 경우는 <h1> 요소에 있는 내용과 역할(Role)에 부여된 dialog로 인해 다이얼로그라는 음성을 추가적으로 출력해 주어 레이어 팝업임을 알 수 있습니다.
스크린 리더의 지원은 데스크톱에서도 괜찮은 편이지만, 모바일 OS 및 브라우저 (iOS / Safari / VoiceOver, Android / Firefox / Talkback)에서는 여전히 화면이 다릅니다.
- Win 7, IE 9, JAWS 14 : 지원됨
- Win 7, FF 18, JAWS 14 : 지원되지 않음
- Win 7, IE9, NVDA 2012.2.1 지원
- Win 7, FF 18, NVDA 2012.2.1 : 지원됨
- Mac OS 10.8.X, Safari, VoiceOver : 지원됨
- iOS 6.1, Safari, VoiceOver (iPad mini) : 부분 지원 (역할 대화 상자는 사용하지 않지만 aria-labelledby를 통해 연결된 대화 제목)
- Android 4.2, Firefox, Talkback (Nexus 7) : 부분 지원 (대화 상자가 표시되지만 대화 상자를 닫은 후에 만 가능)
role=dialog 모달 대화 상자를 구현하기 위해 ARIA 를 사용하는 경우 다음을 따르세요
- 사용자 정의 대화 상자로 사용되는
role=dialog컨테이너 (예 : adiv) 의 속성인지 확인하세요. - 사용자 상호 작용이나 다른 이벤트에 따라 자바 스크립트를 통해 컨테이너가 삽입되었는지 확인하세요
- 대화 상자가 활성화되면 컨테이너의 요소에 포커스가 설정되어 있는지 확인하세요.
- 대화 상자가 활성화되면 컨테이너에없는 요소에 포커스가 설정되지 않았는지 확인하세요.
- 대화 상자가 비활성화되면 원래 대화 상자를 활성화 한 컨트롤에 포커스가 설정되어 있는지 확인하십시오.
출처: https://www.w3.org/WAI/GL/wiki/Using_ARIA_role%3Ddialog_to_implement_a_modal_dialog_box
http://html2jade.org/ 변환과정에서 Space & Tab 간격을 설정할 수 있습니다.