반응형

특정 영역을 지정하여 해당 영역에 달력을 생성하고 클릭에 따라 선택 이벤트, 이전달, 다음달로 이동되는 달력 code를 작성해보겠습니다.

아래는 동작 달력입니다.

 

 

 

 

 

 

 

달력 만들기

 

 

 

달력만들기

calendar.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>달력 만들기</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
    <div id="calendarForm"></div>
</body>
</html>

 

calendar.css

* {
    margin: 0;
    padding: 0
}

.custom_calendar_table td {
    text-align: center;
}

.custom_calendar_table thead.cal_date th {
    font-size: 1.5rem;
}

.custom_calendar_table thead.cal_date th button {
    font-size: 1.5rem;
    background: none;
    border: none;
}

.custom_calendar_table thead.cal_week th {
    background-color: #288CFF;
    color: #fff;
}

.custom_calendar_table tbody td {
    cursor: pointer;
}

.custom_calendar_table tbody td:nth-child(1) {
    color: red;
}

.custom_calendar_table tbody td:nth-child(7) {
    color: #288CFF;
}

.custom_calendar_table tbody td.select_day {
    background-color: #288CFF;
    color: #fff;
}

 

calendar.js

(function () {
    calendarMaker($("#calendarForm"), new Date());
})();

var nowDate = new Date();
function calendarMaker(target, date) {
    if (date == null || date == undefined) {
        date = new Date();
    }
    nowDate = date;
    if ($(target).length > 0) {
        var year = nowDate.getFullYear();
        var month = nowDate.getMonth() + 1;
        $(target).empty().append(assembly(year, month));
    } else {
        console.error("custom_calendar Target is empty!!!");
        return;
    }

    var thisMonth = new Date(nowDate.getFullYear(), nowDate.getMonth(), 1);
    var thisLastDay = new Date(nowDate.getFullYear(), nowDate.getMonth() + 1, 0);


    var tag = "<tr>";
    var cnt = 0;
    //빈 공백 만들어주기
    for (i = 0; i < thisMonth.getDay(); i++) {
        tag += "<td></td>";
        cnt++;
    }

    //날짜 채우기
    for (i = 1; i <= thisLastDay.getDate(); i++) {
        if (cnt % 7 == 0) { tag += "<tr>"; }

        tag += "<td>" + i + "</td>";
        cnt++;
        if (cnt % 7 == 0) {
            tag += "</tr>";
        }
    }
    $(target).find("#custom_set_date").append(tag);
    calMoveEvtFn();

    function assembly(year, month) {
        var calendar_html_code =
            "<table class='custom_calendar_table'>" +
            "<colgroup>" +
            "<col style='width:81px'/>" +
            "<col style='width:81px'/>" +
            "<col style='width:81px'/>" +
            "<col style='width:81px'/>" +
            "<col style='width:81px'/>" +
            "<col style='width:81px'/>" +
            "<col style='width:81px'/>" +
            "</colgroup>" +
            "<thead class='cal_date'>" +
            "<th><button type='button' class='prev'><</button></th>" +
            "<th colspan='5'><p><span>" + year + "</span>년 <span>" + month + "</span>월</p></th>" +
            "<th><button type='button' class='next'>></button></th>" +
            "</thead>" +
            "<thead  class='cal_week'>" +
            "<th>일</th><th>월</th><th>화</th><th>수</th><th>목</th><th>금</th><th>토</th>" +
            "</thead>" +
            "<tbody id='custom_set_date'>" +
            "</tbody>" +
            "</table>";
        return calendar_html_code;
    }

    function calMoveEvtFn() {
        //전달 클릭
        $(".custom_calendar_table").on("click", ".prev", function () {
            nowDate = new Date(nowDate.getFullYear(), nowDate.getMonth() - 1, nowDate.getDate());
            calendarMaker($(target), nowDate);
        });
        //다음날 클릭
        $(".custom_calendar_table").on("click", ".next", function () {
            nowDate = new Date(nowDate.getFullYear(), nowDate.getMonth() + 1, nowDate.getDate());
            calendarMaker($(target), nowDate);
        });
        //일자 선택 클릭
        $(".custom_calendar_table").on("click", "td", function () {
            $(".custom_calendar_table .select_day").removeClass("select_day");
            $(this).removeClass("select_day").addClass("select_day");
        });
    }
}

 

반응형
반응형

이번 포스팅에선 달력 라이브러리를 하나 다뤄볼까 합니다.

 

air - datepicker라는 녀석이고 jqeury가 필요합니다.

아래는 공식 홈페이지입니다. 다양한 예제와 사용법이 정리되어 있습니다.

http://t1m0n.name/air-datepicker/docs/

 

Air Datepicker

Datepicker's language. If string is passed, then language will be searched inDatepicker.languageobject. If object is passed, then data will be taken from this object directly. If some fields are missing, they will be taken from default localization object

t1m0n.name

 

웹 페이지를 구성하다보면 달력은 굉장이 자주 집어넣게 되는 요소인데, jquery ui의 기본 datepicker 아무래도 너무 단순하고 디자인을 잘 모르는 제가 봐도 족히 10년전쯤에 사용했을 것 같은 디자인이라 꺼려지게 되었는데, air-datepicker는 jquery ui datepicker에 비하면 굉장히 깔끔합니다.

 

색상도 많이 들어가지 않아 깔끔하고 어지간한 페이지에 조화로운 느낌이며 옵션 또한 많은 기능을 제공하고 있어서 큰 문제없이 기능을 제어하고 동작시킬 수 있고, 달력 + 시간조합도 가능하기 때문에 일자와 시분초까지 표현해야 하는 경우에도 유용하게 사용할 수 있습니다.

 

사용법을 알아보겠습니다.

 

Air-datepicker

먼저 동작을 위해 js와 css를 다운받아야합니다.

https://github.com/t1m0n/air-datepicker/tree/master/dist

 

t1m0n/air-datepicker

Cool jQuery datepicker. Contribute to t1m0n/air-datepicker development by creating an account on GitHub.

github.com

개발자님의 github로 가셔서 다운로드 받습니다.

 

귀찮으신분들은 아래의 제가올린 압축파일 받습니다.

datepicker.zip
0.07MB

 

단일 달력

<html>

<head>
    <title>datepicker example</title>
    <script src="./jquery-3.1.1.min.js"></script> <!-- 값 제어를 위해 jquery -->
    <link href="./datepicker/css/datepicker.min.css" rel="stylesheet" type="text/css" media="all">
    <!-- Air datepicker css -->
    <script src="./datepicker/js/datepicker.js"></script> <!-- Air datepicker js -->
    <script src="./datepicker/js/datepicker.ko.js"></script> <!-- 달력 한글 추가를 위해 커스텀 -->
</head>

<body>
    <div>
        단일 달력<br />
        <input type="text" id="datepicker">
    </div><br /><br /><br />
</body>
<script>
    $("#datepicker").datepicker({
    	language: 'ko'
    }); 
</script>

</html>

추가한 input text박스를 클릭하면 달력이 생성되고 원하는 날짜를 입력하면 input태그에 날짜가 들어갑니다.

태그안에 속성값을 추가하여 input text박스를 달력으로 사용하거나 스크립트에 요소를 지정해서 사용할 수 있습니다.

 

* 참고로 한국어 언어가 없을 수 있습니다. 저는 datepicker.ko.js 파일을 따로 만들어서 사용중이고, 해당 파일은 위에서 제가 제공하는 파일을 받으시면 됩니다.

 

 

 

단일 달력에 시간 선택하기

$("#datepicker").datepicker({
    language: 'ko',
    timepicker: true,
    timeFormat: "hh:ii AA"
});

아까와 같은 소스에서 이번엔 timepicker 옵션만 true처리 해주면 시간 선택이 가능해집니다.

여기에 추가적으로 시간이 표현될 표현식을 만들어주면 정상적으로 시간 선택까지 할 수 있습니다.

 

 

 

연결된 달력 만들기(날짜 제한처리)

단일 달력의 경우 그냥 일자를 선택하게 해주면 되는데, 시작일 ~ 종료일형태로 되어있는 경우

종료일이 시작일보다 과거로 가거나 시작일이 종료일보다 미래로 선택되는것은 막아야 할텐데, 옵션을 통해 막도록 설정해보겠습니다.

 

datepicker.js

datePickerSet($("#datepicker1"), $("#datepicker2"), true); //다중은 시작하는 달력 먼저, 끝달력 2번째

/*
    * 달력 생성기
    * @param sDate 파라미터만 넣으면 1개짜리 달력 생성
    * @example   datePickerSet($("#datepicker"));
    * 
    * 
    * @param sDate, 
    * @param eDate 2개 넣으면 연결달력 생성되어 서로의 날짜를 넘어가지 않음
    * @example   datePickerSet($("#datepicker1"), $("#datepicker2"));
    */
function datePickerSet(sDate, eDate, flag) {

    //시작 ~ 종료 2개 짜리 달력 datepicker	
    if (!isValidStr(sDate) && !isValidStr(eDate) && sDate.length > 0 && eDate.length > 0) {
        var sDay = sDate.val();
        var eDay = eDate.val();

        if (flag && !isValidStr(sDay) && !isValidStr(eDay)) { //처음 입력 날짜 설정, update...			
            var sdp = sDate.datepicker().data("datepicker");
            sdp.selectDate(new Date(sDay.replace(/-/g, "/")));  //익스에서는 그냥 new Date하면 -을 인식못함 replace필요

            var edp = eDate.datepicker().data("datepicker");
            edp.selectDate(new Date(eDay.replace(/-/g, "/")));  //익스에서는 그냥 new Date하면 -을 인식못함 replace필요
        }

        //시작일자 세팅하기 날짜가 없는경우엔 제한을 걸지 않음
        if (!isValidStr(eDay)) {
            sDate.datepicker({
                maxDate: new Date(eDay.replace(/-/g, "/"))
            });
        }
        sDate.datepicker({
            language: 'ko',
            autoClose: true,
            onSelect: function () {
                datePickerSet(sDate, eDate);
            }
        });

        //종료일자 세팅하기 날짜가 없는경우엔 제한을 걸지 않음
        if (!isValidStr(sDay)) {
            eDate.datepicker({
                minDate: new Date(sDay.replace(/-/g, "/"))
            });
        }
        eDate.datepicker({
            language: 'ko',
            autoClose: true,
            onSelect: function () {
                datePickerSet(sDate, eDate);
            }
        });

        //한개짜리 달력 datepicker
    } else if (!isValidStr(sDate)) {
        var sDay = sDate.val();
        if (flag && !isValidStr(sDay)) { //처음 입력 날짜 설정, update...			
            var sdp = sDate.datepicker().data("datepicker");
            sdp.selectDate(new Date(sDay.replace(/-/g, "/"))); //익스에서는 그냥 new Date하면 -을 인식못함 replace필요
        }

        sDate.datepicker({
            language: 'ko',
            autoClose: true
        });
    }


    function isValidStr(str) {
        if (str == null || str == undefined || str == "")
            return true;
        else
            return false;
    }
}

datePickerSet라는 함수를 따로 만들어서 사용해봤습니다.

 

첫번째 파라미터는 시작일의 달력요소를, 두번째 파라미터에는 종료일의 달력요소를 넣어주면 됩니다.

3번째 파라미터는 처음 로드하면서 처리하는 값인지 아닌지 구분용입니다. true처리하고 로드하시면 됩니다.

시작일과 종료일자에 따라 제한이 걸리는 모습을 볼 수 있습니다.

반응형
반응형

자주 유효성체크를 하게 될 로직은 아니므로 별도로 포스팅을 합니다.

 

사업자 번호 유효성 체크하는 방법을 알아보겠습니다.

 

검증절차

ex) 사업자번호: 123-45-67891

1. 사업자 번호는 10자리이다.(숫자만 있다고 가정)

2. 인증키값으로 체크하려는 사업자번호와 연산을 한다.

   (키값 = 1 3 7 1 3 7 1 3 5)

3. 사업자번호 앞 9자리  인증키 9자리의 각 자리수를 각각 곱하여 전부 더해줍니다.

    S = (1 * 1) + (2 * 3) + (3 * 7) + (4 * 1) + (5 * 3) ...  + (9 * 5)

4. 사업자번호 뒤에서 2번째자리와 인증키 마지막 값을 곱하고 10으로 나눈 후 위의 합과 추가로 더해줍니다.

    S += 9 * 5 / 10(소수점은 제거한다.)

5. 합계를 10으로 나머지 연산을 합니다.

   S % 10 = 9

6. 10에서 나머지 연산의 값을 빼줍니다.

   10 - 9 = 1

7. 사업자번호 마지막자리와 마지막 처리한값이 같으면 사업자번호입니다.

   1 = 1 사업자번호 O

 

 

 

구현 소스

function checkCorporateRegiNumber(number){
	var numberMap = number.replace(/-/gi, '').split('').map(function (d){
		return parseInt(d, 10);
	});
	
	if(numberMap.length == 10){
		var keyArr = [1, 3, 7, 1, 3, 7, 1, 3, 5];
		var chk = 0;
		
		keyArr.forEach(function(d, i){
			chk += d * numberMap[i];
		});
		
		chk += parseInt((keyArr[8] * numberMap[8])/ 10, 10);
		console.log(chk);
		return Math.floor(numberMap[9]) === ( (10 - (chk % 10) ) % 10);
	}
	
	return false;
}

 

반응형
반응형

javascript를 통해 자주 사용하는 유효성 체크 함수들을 남겨봅니다.

 

이메일 체크 정규식

function ValidateEmail(inputText) {
    var mailformat = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
    if (mailformat.test(inputText)) {
        return true;
    } else {
        alert("입력하신 값은 이메일 형식이 아닙니다.");
        //focus 처리가 필요하면 이곳에! $("#email").focus();
        return false;
    }
}

사용 결과

//true
ValidateEmail("myhappyman@naver.com");  //true
ValidateEmail("myhappyman@test.co.kr");  //true


//false
ValidateEmail("myhappymantest.co.kr");  //false
ValidateEmail("myhappyman@naver.com2");  //false

IP 체크 정규식

function ValidateIPaddress(inputText) {
    var ipformat = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
    if (ipformat.test(inputText)) {
        return true;
    } else {
        alert("입력하신 값은 IP형식이 아닙니다.");
        //focus 처리가 필요하면 이곳에! $("#ip").focus();
        return false;
    }
}

사용 결과

//true
ValidateIPaddress("0.0.0.0");  //true
ValidateIPaddress("255.255.255.255");  //true

//false
ValidateIPaddress("0");  //false
ValidateIPaddress("-1.0.255.255");  //false
ValidateIPaddress("255.0.11.256");  //false

비밀번호 체크 정규식

비밀번호는 개발하면서 많은 포맷이 사용될텐데 주석을 통해 원하는 방식으로 변경하여 사용하거나 커스텀을 하면 좋을것 같다.

function ValidatePassword(inputText) {
    var pwformat = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$/;
    if (pwformat.test(inputText)) {
        return true;
    } else {
        alert("비밀번호 형식을 지켜주세요.\n비밀번호는 숫자, 소문자, 대문자를 1개이상, 6~20자리 이내로 입력해주세요.");
        return false;
    }
}

사용 결과

//true
ValidatePassword("fdsfdasQ2321!") //true
ValidatePassword("Ffdaf2321!") // true

//false
ValidatePassword("fdaf2321!") //false 대문자가 없음
ValidatePassword("QWERQWER!") //false 소문자가 없음
ValidatePassword("javascript") // false 대문자, 숫자가 없음

다른 정규식 방식

//비밀번호의 다양한 정규식
// * 하나 이상의 숫자와 특수 문자가 포함하는 7~15자 비밀번호
var pwformat = /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{7,15}$/;

// * 하나 이상의 소문자, 대문자, 숫자 및 특수 문자를 포함하는 8~15자 비밀번호
var pwformat = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,15}$/;

체크하는 정규식의 값만 변경 해주면 원하는 비밀번호 패턴을 체크할 수 있습니다.

출처 : https://www.w3resource.com/

반응형
반응형

체크박스나 라디오등을 제어하다보면 jQuery에 의존해서 많이 사용하게 되는데 순수 javascript만의 힘으로 제어하는 방법을 알아보겠습니다.

 

생각보다 사용법이 jQuery와 많이 다르지 않으며 비슷하기에 어렵지 않을 것입니다.

무엇보다도 추가적으로 라이브러리(jQuery)가 필요하지 않기에 빠르기도하고 간단하게 데이터를 확인할 수 있습니다.

 

 

체크박스의 체크 유무

먼저 체크박스값에 체크가 되어있는지 확인해보겠습니다.

.checked 를 통해 체크 유무를 검사할 수 있습니다.

 

html

<input type="checkbox" name="test1" value="test1">TEST1

 

js

var chk = document.querySelector("input[name=test1]").checked;
console.log(chk);

 

결과

체크가 해제되어있으면 false
체크되어 있다면 true

 


체크된 체크박스의 값 가져오기

이번에는 취미생활에 체크된 데이터의 value를 가져와 보겠습니다.

 

hobby.html

<form>
    <input type="checkbox" name="hobby" value="music">음악감상
    <input type="checkbox" name="hobby" value="game">게임
    <input type="checkbox" name="hobby" value="travel">여행
    <input type="checkbox" name="hobby" value="soccer">축구
    <input type="checkbox" name="hobby" value="reading">독서
    <input type="checkbox" name="hobby" value="cooking">요리
</form>

 

hobby.js

var chkList = document.querySelectorAll("input[name=hobby]:checked");
chkList.forEach(function (ch) {
    console.log(ch.value);
});

 

결과

querySelectorAll을 활용하여 name속성이 hobby인 데이터 중에 :checked속성이 있는 데이터만 추출하여 chkList에 담아두었습니다.

chkList를 forEach메소드를 통해 각각 요소마다 접근하여 value값을 console.log를 통해 출력하는 예제입니다.

 

해당부분을 특정 배열에 push하여 담아도 되고, 문자열에 붙여서 처리하면 될 것 같습니다.


모든 체크박스 선택하기

게시판이나 여러 형태를 작성하다보면 특정값이 모두 선택되게 해야하는 경우가 있습니다.

change 이벤트를 추가하여 제어해보겠습니다.

 

See the Pen vanillaJS CheckBox by myhappyman (@myhappyman) on CodePen.

결과

전체선택

hobbyAll의 요소를 선택하여 change 이벤트가 발생하게 되면

name값이 hobby인 데이터를 모두 찾아 속성값을 전체선택된 데이터값으로 변경처리를 해줍니다.

반응형
반응형

 Rx JS를 통해 github로 user정보를 요청하고 응답받는 예제입니다.

 

https://api.github.com/search/users?q="검색할git ID" 

해당 URL을 통해 JSON형태의 메시지를 전달받고 메시지를 출력하는 예제를 진행해보겠습니다.

 

 

 

Github유저의 ID를 검색하고 파싱하기

rx.js

import { fromEvent, Observable } from 'rxjs';
import {
    map,
    debounceTime,
    filter,
    distinctUntilChanged,
    partition,
    tap,
    switchMap,
    retry,
    finalize,
    share
} from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';

let $layer, $loading;
window.onload = function() {
    $layer = document.getElementById('suggestLayer');
    $loading = document.getElementById('loading');
};

function drawLayer(target, items) {
    target.innerHTML = items
        .map(user => {
            return `<li class="user">
                        <img src="${user.avatar_url}" width="50px" height="50px" />
                        <p><a href="${user.html_url}" target="_blank">${user.login}</a></p>
                    </li>`;
        })
        .join('');
}

function showLoading() {
    return new Observable(observer => {
        const subscription = observable.subscribe({
            next() {
                $loading.style.display = 'block';
            },
            complete() {}
        });
    });
}
function hideLoading() {
    return new Observable(observer => {
        const subscription = observable.subscribe({
            next() {
                $loading.style.display = 'none';
            },
            complete() {}
        });
    });
}

const keyup$ = fromEvent(document, 'keyup').pipe(
    debounceTime(300),
    filter(f => f.target.id === 'searchId'),
    map(e => e.target.value),
    distinctUntilChanged(),
    tap(v => console.log('from keyup$', v)),
    share()
);

let [user$, reset$] = keyup$.pipe(partition(query => query.trim().length > 0));

user$ = user$.pipe(
    tap(showLoading),
    switchMap(query =>
        ajax.getJSON(`https://api.github.com/search/users?q=${query}`)
    ),
    tap(hideLoading),
    retry(2),
    finalize(hideLoading)
);
user$.subscribe({
    next: v => drawLayer($layer, v.items),
    error: e => {
        console.log(e);
    }
});
reset$
    .pipe(
        tap(v => ($layer.innerHTML = '')),
        tap(v => console.log('from reset$', v))
    )
    .subscribe();

 

 

 

index.html

<html>
    <link rel="stylesheet" href="./src/style.css" />
    <script src="main_bundle.js"></script>
    <h1>RxJS GitHub User Map</h1>

    <p>Please enter the name of the user you want to search for...</p>
    <div class="autocomplete">
        <input
            type="text"
            id="searchId"
            placeholder="Please enter your ID..."
        />
        <ul id="suggestLayer"></ul>

        <div id="loading">
            <i class="fas fa-spinner fa-pulse"></i>
        </div>
    </div>
</html>

 

 

style.css

ul {
    list-style-type: none !important;
}
li {
    display: block;
}
.autocomplete {
    position: relative;
    width: 300px;
}
#searchId {
    width: 100%;
    height: 50px;
    line-height: 50px;
    font-size: 20px;
}
#suggestLayer {
    position: absolute;
    color: #666;
    padding: 0;
    margin: 0;
    width: 100%;
}
#suggestLayer li {
    border: 1px solid #bec8d8;
}
.user img {
    position: relative;
    float: left;
    margin-right: 10px;
}
.user p {
    line-height: 50px;
    margin: 0;
    padding: 0;
}
#loading {
    position: absolute;
    z-index: 2;
    top: 2px;
    right: 0;
    display: none;
}

 

 

package.json

{
    "name": "rxjsproj",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "publish": "webpack && webpack-dev-server --output-public=/dev/",
        "watch": "webpack --watch",
        "build": "webpack",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "@fortawesome/fontawesome-free": "^5.4.0",
        "@babel/core": "^7.8.7",
        "@babel/preset-env": "^7.8.7",
        "babel-loader": "^8.0.6",
        "esm": "^3.2.25",
        "rxjs": "^6.5.4",
        "webpack": "^4.42.0",
        "webpack-cli": "^3.3.11",
        "webpack-dev-server": "^3.10.3"
    },
    "dependencies": {
        "react-hot-loader": "^4.12.20"
    }
}

 

 

 

webpack.config.js

var path = require('path');

module.exports = {
    entry: {
        app: './src/rx.js'
    },
    output: {
        path: path.resolve(__dirname, 'dev'),
        filename: 'main_bundle.js'
    },
    mode: 'development',
    module: {
        rules: [
            {
                test: /\.(js)$/,
                include: path.resolve(__dirname, 'src'),
                loader: 'babel-loader',
                query: {
                    presets: ['@babel/preset-env']
                }
            }
        ]
    }
};

 

 

결과

동작 gif

 

 

참고사이트 : https://uxicode.tistory.com/m/402?category=456750

 

RxJs study - 자동완성 UI

 

uxicode.tistory.com

 

반응형
반응형

예전에 포스팅한 amchart에서 svg로 표현된 svg파일을 가져와서 svg코드를 수정거나 CSS 변경으로 적용했던 부분을 많이 보러 오시는걸 보고 한국지도를 찾는분들이 많이 계시는걸 알게 되었습니다.

 

이번에는 d3 라이브러리와 맵정보가 담긴 geoJson파일을 읽고 파싱하여 지도의 형태의 도형을 그려 넣을 것입니다. 그려 넣은 지도에 클릭 이벤트, 줌 이벤트 등을 추가할 수 있으며 이를 만들어보겠습니다.

추가적으로 이번엔 웹서버가 필요합니다.

was서버로 유명한 tomcat을 쓰셔도 좋고 간단하게 웹서버를 올릴 수 있는 http-server를 쓰셔도 됩니다. 

 

데이터 시각화를 지원하는 수많은 라이브러리 중에 d3라는 강력한 라이브러리가 있습니다.

데이터를 차트화도 시켜주고 이미지화 지도 등등 다양하게 표현이 가능합니다.

 

특히, 우리가 일반적으로 사용하는 차트형태가 아닌 topology차트와 같은 기하학적인 형태(트리형 표현, 노드 구성도 등)의 차트도 표현이 가능합니다.

 

이렇게 좋은 기능을 제공하는 d3이지만 개인적으로 안좋은 점(?), 힘든점으로 첫 번째 버전업에 따른 변경되는 메소드명, 파라미터등입니다.

버전이 올라감에 따라 과거버전에서 사용된 메소드가 사라지거나 이름이 바뀌는데... 버전 별로 어떤 기능을 쓸 때, 사용해야 하는 메소드명이라던지... 파라미터를 매번 다시 찾아봐야하는 점이 힘듭니다.

두번째는 기능이 많다보니 공식 API문서도 따로 공부를 해야할 정도로 양도 방대하고 영문으로 되어있습니다...(영어를 잘하는분들이 부럽습니다ㅜ.ㅜ) 엄청 깊게 사용해야할게 아니면 따로 공부보단 그때마다 사용해야할 메소드를 찾아서 적용하는편이 나을 것 같습니다.

 

개인적으로 느낀 d3에 대한 설명은 이정도로 하고 진행해보겠습니다.

 

 


d3를 활용한 한국 지도 만들기

 

한국 지도 gif...

만들어볼 한국지도의 UI 형태입니다.

카카오맵이나 구글지도, 오픈레이어스와 같은 형태가 아니니 참고바랍니다.

 

아래는 준비물입니다.

- d3(3.1.7 ver)

- was서버(http-server, node, tomcat등)

- 한국 지도의 데이터가 담긴 geoJSON파일

d3는 3.1.7버전으로 개발을 했었네요... 현재 최신 버전의 d3를 다운받아서 사용하시면 아마 메소드가 충돌나거나 에러가 발생하여 정상적으로 동작하지 않을겁니다.

 

geoJSON파일은 구글링을 하시면 많이 찾으실수 있는데 통계청을 통해 사용하셔도 되고, 아래 URL

https://github.com/vuski/admdongkor

 

vuski/admdongkor

대한민국 행정동 경계 파일. Contribute to vuski/admdongkor development by creating an account on GitHub.

github.com

이분의 github에서 받은 데이터를 QGIC3 툴을 활용하여 잘라서 사용하셔도 됩니다.

실제로 이번포스팅에는 없지만 지역별로 나눠야 하는 작업이 있어서 한국지도를 QGIC3툴로 잘라서 사용했습니다.

 

아래 2개 파일은 과거 d3버전이나, json파일 받는게 어려운 분들을 위해 따로 첨부합니다.

d3.js
0.29MB
korea.json
1.31MB

 


korea.html

<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
        <meta charset="utf-8" />
        <title>koreaMap</title>
        <link rel="stylesheet" href="css/korea.css" />
    </head>
    <script type="text/javascript" src="js/d3.js"></script>
    <script type="text/javascript" src="js/korea.js"></script>
    <body>
        <div id="container"></div>
    </body>
</html>

html은 간단합니다. container라는곳에 데이터를 파싱하여 한국 지도를 넣을겁니다.

css는 korea.css라는 파일에서 간단하게 정의할겁니다. hover와 같은 이벤트들~

 

이제 동작을 위해 d3라이브러리를 추가해야겠죠? 소스를 동작시킬 korea.js도 연결하였습니다.

 

korea.css, korea.js를 만들어보겠습니다.

 

korea.css

@charset "UTF-8";

#container {
	width: 700px;
	min-height: 700px;
	float: left;
	margin: 15px 35px;
}
#states path {
	fill: #585858;
	stroke: #000000;
	stroke-width: 1.5px;
}

#states path:hover {
	fill: #009300;
}

#states .active {
	fill: #00B700;
}

#states .activeDetail {
	fill: #00B700;
}

#states path {
	cursor: pointer;
}

#states text {
	cursor: pointer;
	font-size: 12px;
	fill: #fff;
}

지역별 색상과 hover 이벤트 글씨 색상등을 처리했습니다. 이번에도 svg로 그려지기때문에 fill, stroke등의 속성이 사용되었습니다.

 

korea.js

window.onload = function() {
    drawMap('#container');
};

//지도 그리기
function drawMap(target) {
    var width = 700; //지도의 넓이
    var height = 700; //지도의 높이
    var initialScale = 5500; //확대시킬 값
    var initialX = -11900; //초기 위치값 X
    var initialY = 4050; //초기 위치값 Y
    var labels;

    var projection = d3.geo
        .mercator()
        .scale(initialScale)
        .translate([initialX, initialY]);
    var path = d3.geo.path().projection(projection);
    var zoom = d3.behavior
        .zoom()
        .translate(projection.translate())
        .scale(projection.scale())
        .scaleExtent([height, 800 * height])
        .on('zoom', zoom);

    var svg = d3
        .select(target)
        .append('svg')
        .attr('width', width + 'px')
        .attr('height', height + 'px')
        .attr('id', 'map')
        .attr('class', 'map');

    var states = svg
        .append('g')
        .attr('id', 'states')
        .call(zoom);

    states
        .append('rect')
        .attr('class', 'background')
        .attr('width', width + 'px')
        .attr('height', height + 'px');

    //geoJson데이터를 파싱하여 지도그리기
    d3.json('json/korea.json', function(json) {
        states
            .selectAll('path') //지역 설정
            .data(json.features)
            .enter()
            .append('path')
            .attr('d', path)
            .attr('id', function(d) {
                return 'path-' + d.properties.name_eng;
            });

        labels = states
            .selectAll('text')
            .data(json.features) //라벨표시
            .enter()
            .append('text')
            .attr('transform', translateTolabel)
            .attr('id', function(d) {
                return 'label-' + d.properties.name_eng;
            })
            .attr('text-anchor', 'middle')
            .attr('dy', '.35em')
            .text(function(d) {
                return d.properties.name;
            });
    });

    //텍스트 위치 조절 - 하드코딩으로 위치 조절을 했습니다.
    function translateTolabel(d) {
        var arr = path.centroid(d);
        if (d.properties.code == 31) {
            //서울 경기도 이름 겹쳐서 경기도 내리기
            arr[1] +=
                d3.event && d3.event.scale
                    ? d3.event.scale / height + 20
                    : initialScale / height + 20;
        } else if (d.properties.code == 34) {
            //충남은 조금 더 내리기
            arr[1] +=
                d3.event && d3.event.scale
                    ? d3.event.scale / height + 10
                    : initialScale / height + 10;
        }
        return 'translate(' + arr + ')';
    }

    function zoom() {
        projection.translate(d3.event.translate).scale(d3.event.scale);
        states.selectAll('path').attr('d', path);
        labels.attr('transform', translateTolabel);
    }
}

d3사용으로 인해 여러 메소드들이 사용되었습니다.

d3의 메소드들을 다 설명하기엔 너무 길어지므로 공식 홈페이지의 API들이나 다른 예제들을 참조하여 추가해보시면 좋을것 같습니다.

 

지역명을 입력하는 label부분에서 서로 중간지점이 겹쳐서 글자를 제대로 알아보지 못하는 현상이 발생해서 

공통으로 사용하는 trnslateTolabel이라는 함수에서 약간의 커스텀이 처리되었습니다.

 

소스 작성이 완료되었으면 이제 실행을 해봐야겠죠?

웹 서버가 필요합니다!

톰캣같은 was로 개발하고 계신분들은 확인시 문제가 없겠지만 서버없이 html을 실행하게 되면 이런 에러가 발생할겁니다. 처음에 시작할 때 서버가 필요하다고 했었죠?

 

저는 http-server를 활용해서 실행하였습니다.

터미널 창을 열고 프로젝트 디렉토리로 이동해서 http-server를 입력합니다.

터미널에 http-server 입력!

 

enter입력! 서버가 실행되었습니다.

웹서버 실행

별다른 옵션없이 실행하여 기본포트인 8080으로 실행되었습니다.

 

이제 localhost:8080/korea.html을 입력해서 실행해보겠습니다. (127.0.0.1:8080/korea.html)

정상적으로 실행된 모습

 

http-server 설치법 및 사용법은 매우 간단하므로 구글링을 통해 보시고 사용하는것도 추천드립니다.

(추후에 포스팅하게 되면 연결해놓겠습니다.)

 

 

아래 URL의 github를 가시면 지금까지 작성한 소스를 받아보실수 있습니다.

https://github.com/myhappyman/d3_koreaMap

 

myhappyman/d3_koreaMap

Contribute to myhappyman/d3_koreaMap development by creating an account on GitHub.

github.com

반응형
반응형

https://myhappyman.tistory.com/94

 

HTML, JAVASCRIPT - 테트리스 만들기 - 3

https://myhappyman.tistory.com/93 HTML, JAVASCRIPT - 테트리스 만들기 - 2 https://myhappyman.tistory.com/92 HTML, JAVASCRIPT - 테트리스 만들기 - 1(table요소, 배열값으로 테트리스 만들기) 웹 개발을 해..

myhappyman.tistory.com

 

3장에서 키보드 입력 이벤트까지 처리해보았습니다.

 

이번 포스팅에서는 어떤 입력을 받거나 일정시간마다 내려가더라도 테트리스 블럭이 게임 맵을 뚫고 넘어가지 않는 함수를 작성하고 사용하도록 해보겠습니다.

 

isMove()라는 함수를 추가해서, 키 이벤트값이나 setInterval함수에 따라 진행이 되었을때, 벽인지 아닌지 체크 후 boolean값을 리턴하는 함수를 만들어보겠습니다.

 

움직임 제한 함수 추가 isMove()

function isMove(TYPE, TURN, GX, GY){
    TURN = TURN > 3 ? TURN % 4 : TURN;
    for(var i=0; i<4; i++){
        for(var j=0; j<4; j++){
            if(TETRIS[TYPE][TURN][i][j] == 1){ //테트리스의 블럭모양이 나왔는데
                if(MAP[GX+2+i][GY+2+j] != 0){ //닿을곳이 맵이 아니면
                    return false;
                }
            }
        }
    }
    return true;
}

이러한 함수를 추가할겁니다. 파라미터값들에 대해 설명을 하면 아래와 같습니다.

첫번째 파라미터(TYPE)에는 테트리스의 현재 타입값을 넣습니다.

두번째 파라미터(TURN)에는 테트리스의 현재 회전값을 넣습니다.

세번째 파라미터(GX)는 현재 테트리스가 위치한 위아래 위치값 넣습니다.

네번째 파라미터(GY)는 현재 테트리스가 위치한 좌우의 위치값을 넣습니다.

리턴값은 boolean값인 true, false값을 처리합니다.

 

그리고 맵 데이터에서 현재 이동이 가능한 값 0으로 된 데이터인지 체크를하고 0이 아니라면 움직이지 못하도록 false값을 리턴할겁니다.

모든 조건을 통과했을때만 true값이 나와서 동작을 시킵니다.

 

 

isMove()함수에 대해서 좀 더 알아보겠습니다.

숫자 1번키를 눌러서 왼쪽으로 이동한다!

숫자1번(또는 왼쪽으로 이동하는 키)을 눌러서 왼쪽으로 이동한다고 가정을 했을때,

MAP배열 데이터와 테트리스 데이터가 위 그림처럼 되어있을텐데, 여기서 계속해서 이동이 가능한 영역은 회색입니다. 현재 그림을 기준으로 왼쪽으로 2번까진 이동이 가능하겠죠. 테트리스 모양인 'ㄱ'자의 막대는 넘어가면 안되니까요.

 

즉, 현재의 테트리스의 타입과 회전모양을 값으로 받고 TETRIS배열의 0번째부터 끝까지 배열모양을 조사해서 1이라는건 테트리스의 모양이므로 입력한 동작이 움직일수 있는지 없는지 체크하는 조건문이 진행됩니다. 

if(TETRIS[TYPE][TURN][i][j] == 1)

 

전역변수 GX값, GY값은 MAP배열을 기준으로 2만큼씩 안으로 들어가 있기 때문에 MAP데이터를 검사할때 2만큼 추가하였고, 각각 닿는 부위를 체크하기 위해 위아래는 검사하는 GX축에 i값만큼 더하고, 좌우를 검사하는 GY부분에 j만큼 더하였습니다.

if(MAP[GX+2+i][GY+2+j] != 0)

와 같은 조건문이 만들어졌습니다. 0이 아니라는건 맵이 아니기 때문에 return false가 처리되면서 이동을 못하도록 하죠.

 

그럼 이제 실질적으로 isMove()를 써봐야겠죠 키이벤트를 정의했던 myFunction()을 수정해보겠습니다.

 

myFunction함수에 키이벤트에 따른 이동제한 처리

function myFunction(input){
    //그리기 전에 지우기
    erase(GX, TYPE, TURN);
    switch(input){
        case 53://회전
            if(isMove(TYPE, TURN+1, GX, GY)){
                TURN++;
                TURN = TURN % 4;
            }
            break;
        case 49://블럭왼쪽이동
            if(isMove(TYPE, TURN, GX, GY-1)){
                GY--;
            }
            break;
        case 51://블럭오른쪽이동
            if(isMove(TYPE, TURN, GX, GY+1)){
                GY++;
            }
            break;
        case 50://블럭아래로이동
            if(isMove(TYPE, TURN, GX+1, GY)){
                GX++;
            }
            break;
        case 32://스페이스
            while(isMove(TYPE, TURN, GX+1, GY)){
                GX++;
            }
            break;
    }
    //테트리스 블럭 그리기
    drawTetris(GX, TYPE, TURN);
}

3장에서 입력된 키보드 값을 처리할 때 위 방향키를 입력받으면 TURN값을 ++처리하여 증가하거나

왼쪽으로 이동하면 전역변수 GY값을 --시켰는데 이번 포스팅에서는 if문인 조건문이 추가되었습니다.

회전을 하게되면 파라미터값에 현재의 전역변수의 회전값에 +1을 처리한 값을 넣고 움직일 수 있는지 없는지 체크합니다. 조건이 만족되면 그때서야 TURN의 전역변수 값을 ++하여 증감해줍니다.

 

왼쪽으로 이동할땐, 좌우를 처리하는 GY값에 -1을 처리하고 왼쪽으로 이동이 가능한지 아닌지 체크합니다.

마찬가지로 조건이 만족되었을때만 GY--처리를 합니다.

 

스페이스값은 아래로 이동하는것을 더이상 진행이 불가할때까지 처리해주면 됩니다.

while문을 통해 처리하였고 이동을 못하면 false값이 나오니 자연스럽게 빠져오겠죠.

 

이제 마지막으로 action()함수의 setInterval부분을 추가하겠습니다.

일정시간마다 자연스럽게 내려가다보니 일정시간이 지나면 제한이 없어서 뚫고 지나가게 됩니다.

 

 

action() 함수의 일정시간마다 동작하는 setInterval() 이동제한 추가

function action(){
    //keyEvent
    document.addEventListener("keypress", function(){
        myFunction(event.keyCode);
    });

    var intervalID = setInterval(function(){
        erase(GX, TYPE, TURN); //생성한 전위치 블럭 삭제
        if(isMove(TYPE, TURN, GX+1, GY)){
            GX++; //한줄씩 내리기
        }
        drawTetris(GX, TYPE, TURN); //테트리스를 그린다.
    }, GAME_SPEED);
}

키 이벤트부분과 유사합니다.

먼저 현재의 블럭을 지워주고 아래로 이동하는 키값이 강제로 입력되었다고 생각하시면 됩니다.

아래 위값을 제어하는 GX값이 증가하였다고 생각하고 isMove()함수를 통해 체크합니다.

 

그러면 더이상 맵을 뚫고 블럭이 사라지는 일은 없을 겁니다.

더이상 벽을 뚫고 들어가지 않는다.

 

여기까지 동작하는 소스를 한번 올리도록 하겠습니다.

 

아래의 ZIP파일을 받아주시면 됩니다.

tetris_v3.zip
0.00MB

반응형