반응형

Table 태그를 구성하다보면 데이터가 길어질 경우를 대비해서 스크롤을 내리더라도 헤더컬럼부분을 고정으로 주고

아래 데이터부분만 스크롤이 되도록 처리하는 경우가 있습니다.

헤더가 고정된 테이블

 

"<table>" 태그를 "<thead>"와 "<tbody>" 형태로 구성하시고 "<thead> <th>"에는 position: sticky를 처리하면 고정형 테이블을 구성할 수 있습니다.

 

다만, sticky의 경우 IE에서 지원이 안되기때문에 static으로 동작하여 일반적인 테이블처럼 동작하게 됩니다.

아래는 sticky를 사용하여 고정헤더 테이블 샘플 예제 소스입니다.

 

헤더 고정하기

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Table</title>
    <style>
        *{margin:0; padding:0;}
        html, body, .wrapper{width:100%; height: 100vh;}
        .wrapper .tableBox{position: relative; top: 50px; left: 50px; width: 700px; height: 100px; overflow-y: scroll; border-top: 2px solid dodgerblue;}
        .tableData{width: 100%; border-collapse: collapse; text-align: center;}
        .tableData thead tr th{position: sticky; top: 0; background: #ebeaea;}
        .tableData thead tr th,
        .tableData tbody tr td{padding: 15px 0; box-sizing: border-box; border-bottom: 1px solid #dedede; border-right: 1px solid #ddd; word-break: break-all;}
    </style>
</head>
<body>
    <div class="wrapper">
        <div class="tableBox">
            <table class="tableData">
                <colgroup>
                    <col width="8%">
                    <col width="15%">
                    <col width="25%">
                    <col width="16%">
                    <col width="12%">
                    <col width="12%">
                    <col width="12%">
                </colgroup>
                <thead>
                    <tr>
                        <th class="stiky">순번</th>
                        <th class="stiky">구분</th>
                        <th class="stiky">제목</th>
                        <th class="stiky">글쓴이</th>
                        <th class="stiky">날짜</th>
                        <th class="stiky">첨부파일</th>
                        <th class="stiky">비고</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>1</td>
                        <td>공지사항</td>
                        <td>제목입니다.</td>
                        <td>홍길동</td>
                        <td>2020-11-26</td>
                        <td>-</td>
                        <td>-</td>
                    </tr>
                    <tr>
                        <td>2</td>
                        <td>공지사항</td>
                        <td>제목 "1","2","3","4","5","6","7","8","9","10","11"</td>
                        <td>홍길동</td>
                        <td>2020-11-26</td>
                        <td>-</td>
                        <td>-</td>
                    </tr>
                    <tr>
                        <td>3</td>
                        <td>게시판</td>
                        <td>123456789123456789123456789</td>
                        <td>홍길동</td>
                        <td>2020-11-26</td>
                        <td>-</td>
                        <td>-</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</body>
</html>

thead 태그의 th들에게 position:sticky를 처리하여 고정시킨 예제입니다.

주의점으로 theadthead tr에 sticky를 줘도 동작하지 않기에 꼭 thead영역의 th들에게 sticky처리를 하셔야 합니다.

 

 

myhappyman.tistory.com/185

 

HTML - Table td안에 특수문자(")가 존재할 경우 넓이 초과 현상

DB데이터를 가져와서 테이블에 데이터를 만들어 append를 처리하고 있었는데, 데이터 문자열 내부에 "(더블 쿼드)가 포함된 긴 문자열이 들어가 있었고, 정상적인 넓이로 table이 동작하지 않았다. '

myhappyman.tistory.com

이전 글인 table을 두개로 넓이를 동일하게 처리하여 구성한 테이블에서 발생할 수 있는 현상도 방지할 수 있고, 깔끔하게 동작이 됩니다!

 

IE를 신경쓰지 않아도 되는 프로젝트에서는 유용하게 사용할 수 있겠습니다.

반응형
반응형

DB데이터를 가져와서 테이블에 데이터를 만들어 append를 처리하고 있었는데, 데이터 문자열 내부에 "(더블 쿼드)가 포함된 긴 문자열이 들어가 있었고, 정상적인 넓이로 table이 동작하지 않았다.

 

'<colgroup>' 태그로 width를 주고 css로 width : 넓이값% !important까지 처리하였지만 여전히 먹통이였다.

word-break: break-all 속성을 추가하여도 문제가 발생하였는데,

원인은 특수문자 "(더블 쿼드)가  존재하면서 발생하는 문제였고, 파싱하는 과정에서 replace메소드로 치환하여 처리하니 문제없이 동작하였다.

 

아래는 비슷한 현상을 재현하기 위한 sample 소스이다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Table</title>
    <style>
        *{margin:0; padding:0;}
        html, body, .wrapper{width:100%; height: 100vh;}
        .wrapper .tableBox{position: relative; top: 50px; left:50px; width: 700px;}
        .wrapper .tableBox .scroll{width: 100%; height: 100px; overflow-y: scroll;}
        table{border-collapse:collapse;text-align: center;}
        table th{border-top: 2px solid dodgerblue; position: sticky; top: 0px;}
        table th, table td{border-bottom: 1px solid #dedede;border-right: 1px solid #ddd;word-break: break-all;}
        .tableHeader{width: calc(100% - 17px);}
        .tableHeader th{height: 40px;}
        .tableData{width: 100%;}
    </style>
</head>
<body>
    <div class="wrapper">
        <div class="tableBox">
            <table class="tableHeader">
                <colgroup>
                    <col width="8%">
                    <col width="15%">
                    <col width="25%">
                    <col width="16%">
                    <col width="12%">
                    <col width="12%">
                    <col width="12%">
                </colgroup>
                <thead>
                    <tr>
                        <th>순번</th>
                        <th>구분</th>
                        <th>제목</th>
                        <th>글쓴이</th>
                        <th>날짜</th>
                        <th>첨부파일</th>
                        <th>비고</th>
                    </tr>
                </thead>
            </table>
            <div class="scroll">
                <table class="tableData">
                    <colgroup>
                        <col width="8%">
                        <col width="15%">
                        <col width="25%">
                        <col width="16%">
                        <col width="12%">
                        <col width="12%">
                        <col width="12%">
                    </colgroup>
                    <tbody>
                        <tr>
                            <td>1</td>
                            <td>공지사항</td>
                            <td>제목입니다.</td>
                            <td>홍길동</td>
                            <td>2020-11-26</td>
                            <td>-</td>
                            <td>-</td>
                        </tr>
                        <tr>
                            <td>2</td>
                            <td>공지사항</td>
                            <td>제목 "1","2","3","4","5","6","7","8","9","10","11"</td>
                            <td>홍길동</td>
                            <td>2020-11-26</td>
                            <td>-</td>
                            <td>-</td>
                        </tr>
                        <tr>
                            <td>3</td>
                            <td>게시판</td>
                            <td>123456789123456789123456789</td>
                            <td>홍길동</td>
                            <td>2020-11-26</td>
                            <td>-</td>
                            <td>-</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</body>
</html>

테이블 컬럼을 고정화 시키기 위해 table 태그를 2개로 구성하였다.

 

이후 로드된 페이지는 아래와 같다.

제목 컬럼의 내용이 특수문자가 추가되면서 주르륵 붙으면서 발생하는 현상이다.

 

더블쿼드가 붙으면 문제가 발생한다.

 

 

다음은 더블쿼드를 삭제하였을때 정상적으로 동작하는 것을 볼 수 있다.

 

더블쿼드를 지우니 깨지지 않는다.

 

 

반응형
반응형

스프링 프로젝트에서 Netty 프레임워크를 사용하여 'ServerBootstrap'을 구동하고 사용하였는데, 톰캣을 종료하면 자연스럽게 죽을줄 알았던 Netty 서버가 좀비처럼 계속 살아있는 현상이 발생했다.

 

덕분에 톰캣을 재기동하게 되면 같은 포트로 바인딩하면서 BindException이 발생하는 등 여러가지 문제가 생겼고, 대처법으로 netstat을 통해 port의 pid를 찾아와 kill을 시킬까 했지만, 애초에 종료를 안하는 원초적인 문제를 해결하는것이 맞다고 판단하여 아래와 같이 '@PreDestroy' 어노테이션을 추가하여, 스프링을 종료하게되면 ServerBootstrp을 종료 할 수 있도록 구성하였다.

 

Netty Server 안전하게 종료하기

Controller.java

private NettyServer server;

@PostConstruct
private void start() {
	new Thread(new Runnable() {
		
		@Override
		public void run() {
			try {
				server = new NettyServer(SERVER_PORT, service);
				server.run();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
	}).start();
}

/**
 * 바인딩된 NettyServer를 종료한다.
 * @throws InterruptedException
 */
@PreDestroy
private void destory() throws InterruptedException {
	logger.info("================ Spring @PreDestroy ================");
	server.serverDestroy();
}

 

NettyServer.java

private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private ChannelFuture future;

public void serverDestroy() throws InterruptedException {
	logger.info("================ Netty BootStrapServer Destroy() ================");
	bossGroup.shutdownGracefully().sync();
	workerGroup.shutdownGracefully().sync();
	future.channel().closeFuture().sync();
}

구동하면서 동작시킨 이벤트 그룹들을 shutdown시켜서 종료되도록 처리하였다.

 

 

반응형
반응형

파일 업로드 형태의 게시판 등 웹 UI에서 파일을 긁어와서 드롭하였을때 등록하고 ajax를 통해 업로드 하는 예제까지 진행해보겠습니다.(Spring Legacy 기준)

 

Drag And Drop

html, css

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<style>
    *{padding:0;margin:0}
    html, body, .wrap{width: 100%;}
    .clear{clear:both;}
    .wrap>.fileBox{padding: 20px;}
    .fileBox input, textarea{width: 100%;}
    .fileBox textarea{resize:none;}
    .fileBox .fileDrop{display: inline-block;width: 700px;height: 75px;border: 1px solid #000;overflow: auto;}
    .fileDrop .fileList .fileName{padding-left: 20px;}
    .fileDrop .fileList .fileSize{padding-right: 20px; float:right;}
</style>
<body>
    <div class="wrap">
        <div class="fileBox">
            <form id="fileForm" name="fileForm" enctype="multipart/form-data" method="post">
                <table>
                    <tr>
                        <td><input type="text" name="title"></td>
                    </tr>
                    <tr>
                        <td><textarea name="contents"></textarea></td>
                    </tr>
                    <tr>
                        <td><div id="fileDrop" class="fileDrop"></div></td>
                    </tr>
                </table>
                <div class="buttonBox">
                    <button type="button" id="save">저장</button>
                </div>
            </form>
        </div>
    </div>
</body>
</html>

js

var fileList = []; //파일 정보를 담아 둘 배열
$(function(){

    //드래그앤드랍
    $("#fileDrop").on("dragenter", function(e){
        e.preventDefault();
        e.stopPropagation();
    }).on("dragover", function(e){
        e.preventDefault();
        e.stopPropagation();
        $(this).css("background-color", "#FFD8D8");
    }).on("dragleave", function(e){
        e.preventDefault();
        e.stopPropagation();
        $(this).css("background-color", "#FFF");
    }).on("drop", function(e){
        e.preventDefault();

        var files = e.originalEvent.dataTransfer.files;
        if(files != null && files != undefined){
            var tag = "";
            for(i=0; i<files.length; i++){
                var f = files[i];
                fileList.push(f);
                var fileName = f.name;
                var fileSize = f.size / 1024 / 1024;
                fileSize = fileSize < 1 ? fileSize.toFixed(3) : fileSize.toFixed(1);
                tag += 
                        "<div class='fileList'>" +
                            "<span class='fileName'>"+fileName+"</span>" +
                            "<span class='fileSize'>"+fileSize+" MB</span>" +
                            "<span class='clear'></span>" +
                        "</div>";
            }
            $(this).append(tag);
        }

        $(this).css("background-color", "#FFF");
    });

    //저장
    $(document).on("click", "#save", function(){
        var formData = new FormData($("#fileForm")[0]);
        if(fileList.length > 0){
            fileList.forEach(function(f){
                formData.append("fileList", f);
            });
        }         

        $.ajax({
            url : "서버 맵핑 URL",
            data : formData,
            type:'POST',
            enctype:'multipart/form-data',
            processData:false,
            contentType:false,
            dataType:'json',
            cache:false,
            success:function(res){
                alert("저장에 성공하셨습니다.");
            },error:function(res){
                alert("오류 발생.\n관리자에게 문의해주세요.");
            }
        });
    });
});

drag관련 이벤트를 처리하고 싶은 개체에 등록하여 enter, over, leave, drop에 따른 처리를 각각 처리하였습니다.

drop하였을때는 파일을 내려놨을때 드래그앤드롭의 동작 중 가지고 있던 데이터의 정보를 확인하기 위해 dataTransfer 파일 정보를 가져옵니다.

 

이후 파일 정보가 담긴 object를 통해 하나씩 확인하여 tag를 생성하고 별도의 파일정보는 배열에 담아두었다가 서버에 전송시 사용합니다.

 

java - Server Controller

@ResponseBody
@RequestMapping(value = { "uploadPath" }, method = RequestMethod.POST, produces = "json/plain;charset=UTF-8")
public int uploadPath(MultipartHttpServletRequest mtfRequest, 
		final HttpServletRequest request, 
		final HttpServletResponse response) {
	int res = 1;
	System.out.println("제목 > " + request.getParameter("title"));
	System.out.println("내용 > " + request.getParameter("contents"));
	if(mtfRequest != null) {
		List<MultipartFile> fileList = mtfRequest.getFiles("fileList");
		for(int i=0; i<fileList.size(); i++) {
			MultipartFile multi = fileList.get(i);
			if(multi == null) {
				return 0;
			}else if(multi.getSize() == 0) {
				return 0;
			}else {
				System.out.println("파일명 : " + multi.getOriginalFilename() + " / 파일 사이즈 : " + multi.getSize());
			}
		}
	}
		
	return res;
}

ajax에서 전달한 데이터를 받을 컨트롤러입니다. 별도의 서비스나 처리는 하지 않았습니다.

전달된 데이터의 파라미터가 출력된 콘솔 결과는 아래와 같습니다.

컨트롤러에 전달된 데이터 정보

 

동작결과

반응형
반응형

체크박스가 포함된 게시판형태의 데이터들을 다루다보면 전체 선택 해제 기능이 자주 들어갑니다.

전체 선택에 따른 하위 체크박스들을 선택하거나 해제하고 전체박스가 선택된 상태에서 하위 체크박스를 해제하면 전체체크박스의 상태값도 해제되도록 변경되는 예제를 진행해보겠습니다.

 

체크박스 전체 선택, 해제 제어하기

allcheckbox.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<body>
    <table>
        <tr>
            <th>
                <input type="checkbox" name="check" class="allcheck">
            </th>
            <th>순번</th>
            <th>제목</th>
        </tr>
        <tr>
            <td><input type="checkbox" name="check"></td>
            <td>1</td>
            <td>제목입니다.</td>
        </tr>
        <tr>
            <td><input type="checkbox" name="check"></td>
            <td>2</td>
            <td>제목입니다.</td>
        </tr>
        <tr>
            <td><input type="checkbox" name="check"></td>
            <td>3</td>
            <td>제목입니다.</td>
        </tr>
        <tr>
            <td><input type="checkbox" name="check"></td>
            <td>4</td>
            <td>제목입니다.</td>
        </tr>
    </table>
</body>
</html>

css

*{margin:0;padding:0;}
table{width:800px;text-align:center;border-collapse:collapse;border-left:1px solid #ddd;border-right: 1px solid #ddd;}
table tr{border-bottom: 1px solid #ddd;}

JS

$(function(){
    $("[type=checkbox][name=check]").on("change", function(){ //0
        var check = $(this).prop("checked"); //1
        //전체 체크
        if($(this).hasClass("allcheck")){ //2
            $("[type=checkbox][name=check]").prop("checked", check);

        //단일 체크
        }else{ //3
            var all = $("[type=checkbox][name=check].allcheck");
            var allcheck = all.prop("checked")
            if(check != allcheck){ //3-1
                var len = $("[type=checkbox][name=check]").not(".allcheck").length; //3-2
                var ckLen = $("[type=checkbox][name=check]:checked").not(".allcheck").length; //3-2
                if(len === ckLen){ //3-3
                    all.prop("checked", true);
                }else{
                    all.prop("checked", false);
                }
            }
        }
    });
});

저는 모든 체크박스의 이름을 check로 두었고 전체선택기능의 체크박스와 일반체크박스를 class에 allcheck라는 이름으로 구분하였습니다.

 

주석번호와 매칭하여 설명을 확인하시면 됩니다.

0. jQuery를 활용하여 페이지가 로드되면 체크박스에 이벤트를 추가합니다.

1. input type이 checkbox이면서 name값이 check인 요소에 change 이벤트가 발생하면 현재 발생한 요소의 checked 값을 받아옵니다.

2. 이벤트가 발생한 요소의 클래스에 allcheck가 존재하면 전체 체크박스로 판단하고 전체 체크를 진행합니다.

(해제가 되었으면 전부다 해제, 선택이면 전부 다 선택 처리이므로 자신의 상태값과 맞춰줍니다.) 

3. 단일 체크일 경우

   3-1. 전체 체크박스의 상태값과 자신의 상태값을 비교합니다. 상태가 다를 경우 확인 작업이 필요합니다.

   3-2. 페이지에 노출된 체크 박스의 개수와 체크된 개수가 같은지 확인을 하기 위해 값을 가져옵니다.

         여기서 .not()메소드를 사용하여 전체 체크박스는 제외하였습니다.

   3-3. 가져온 개수가 서로 같다면 전체가 이미 선택된 것이므로 전체체크박스에도 선택처리를 합니다.

         반대의 경우 전체가 체크된게 아니므로 전체체크박스에 해제처리를 합니다.

 

동작 결과

잘 동작한다!

반응형
반응형

웹 페이지를 제작하면서 아주 편리하게 사용하는 alert, confirm창을 차별점을 두기 위해 디자인하고 팝업 형태로 제작의뢰가 들어오는 경우가 종종 있습니다.

 

사실 alert경우에는 동작하면 모든 javascript가 멈춘다는 특징외에는 출력메시지로 간단하게 경고 박스 띄우는 정도이기에 구현하는데 어려움은 없지만 confirm의 경우 확인, 취소버튼이 존재하며 확인이 눌린 경우 이후의 행위를 정의해야 하기에 callback 형태로 구현을 해야합니다.

 

아무래도 동작을 위해 modal의 태그와 css를 어느정도는 집어넣어야 하는 번거로움이 있지만 적용을 하고 나면 이후에 개인적인 취향으로 커스텀이 가능해집니다.

 

아래는 제가 작성해본 alert, confirm을 동작시키는 메소드와 예제입니다.(prompt도 존재하지만 개인적으로 사용하지 않아서 구성하지 않았습니다.)

 

Confirm, Alert 동작 메소드 정의하기

/**
 *  alert, confirm 대용 팝업 메소드 정의 <br/>
 *  timer : 애니메이션 동작 속도 <br/>
 *  alert : 경고창 <br/>
 *  confirm : 확인창 <br/>
 *  open : 팝업 열기 <br/>
 *  close : 팝업 닫기 <br/>
 */ 
var action_popup = {
    timer : 500,
    confirm : function(txt, callback){
        if(txt == null || txt.trim() == ""){
            console.warn("confirm message is empty.");
            return;
        }else if(callback == null || typeof callback != 'function'){
            console.warn("callback is null or not function.");
            return;
        }else{
            $(".type-confirm .btn_ok").on("click", function(){
                $(this).unbind("click");
                callback(true);
                action_popup.close(this);
            });
            this.open("type-confirm", txt);
        }
    },

    alert : function(txt){
        if(txt == null || txt.trim() == ""){
            console.warn("confirm message is empty.");
            return;
        }else{
            this.open("type-alert", txt);
        }
    },

    open : function(type, txt){
        var popup = $("."+type);
        popup.find(".menu_msg").text(txt);
        $("body").append("<div class='dimLayer'></div>");
        $(".dimLayer").css('height', $(document).height()).attr("target", type);
        popup.fadeIn(this.timer);
    },

    close : function(target){
        var modal = $(target).closest(".modal-section");
        var dimLayer;
        if(modal.hasClass("type-confirm")){
            dimLayer = $(".dimLayer[target=type-confirm]");
            $(".type-confirm .btn_ok").unbind("click");
        }else if(modal.hasClass("type-alert")){
            dimLayer = $(".dimLayer[target=type-alert]")
        }else{
            console.warn("close unknown target.")
            return;
        }
        modal.fadeOut(this.timer);
        setTimeout(function(){
            dimLayer != null ? dimLayer.remove() : "";
        }, this.timer);
    }
}

action_popup 변수에 객체 기반의 메소드화로 구현하였습니다.

timer 속성은 모달이 노출되거나 닫힐때, 자연스러운 처리를 위한 애니메이션 속도이며

confirm 메소드는 confirm효과의 모달을 동작하는 메소드로 첫번째 파라미터는 노출시킬 텍스트, 두번째 파라미터는 callback을 정의합니다.

alert 메소드는 alert 효과의 모달을 동작시켜줍니다. 파라미터는 노출시킬 텍스트만 입력합니다.

open, close 메소드는 모달을 열고 닫는 처리를 위해 정의하였습니다.

 

간단한 설명은 이정도로 하고 사용 예제 및 결과는 아래를 참고해주세요.

 

사용 예제 소스 및 결과

See the Pen Alert And Confirm Custom by myhappyman (@myhappyman) on CodePen.

 

css와 사용을 위한 display: none처리된 모달형태의 태그들을 심어두고 요청에 따라 모달을 노출하는 형태입니다.

 

 

결과

반응형
반응형

xhr(XMLHttpRequest) 객체는 서버와 데이터를 확인하기 위해 사용됩니다.

 

ajax또한 xhr 규격을 사용하여 동작하고 있고, 다양한 메소드를 통해 요청의 상태값이나 시간, 결과, 진행상태 등을 확인 할 수 있습니다.

 

여기서 xhr의 upload.onprogress 메소드를 사용하여 파일의 업로드 진행상황을 확인하고 UI적으로 페이지 진행상태를 표현 할 수 있습니다.

 

대량의 파일이나 다중으로 여러 파일을 넘길 때 상태를 알 수 있다보니 아무래도 기다리는데, 도움이 될 것 같습니다.

 

바로 예제를 통해 확인해 보겠습니다.

 

xhr.upload.progress

사용법 ajax로 넘길때 xhr메소드를 추가하고 내부에 upload.onprogress메소드를 추가하여 정의하고자 하는 파일 내용을 추가하면 됩니다.

xhr: function(){
	var xhr = $.ajaxSettings.xhr();
	xhr.upload.onprogress = function(e){
		var per = e.loaded * 100 / e.total;
		console.log(per);
	};
	return xhr;
},

 

 

사용예제

fileupload.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
<head>
	<title>Home</title>
</head>
<style>
	*{margin:0;padding:0}
	.progressContainer{position:relative;width: 450px;padding:20px 10px;border: 1px solid #eee;margin-top: 15px;background:#000;height:20px;}
	.progress{position:absolute;width: calc(100% - 20px);height: 20px;}
	.progressTotal{background: #5D5D5D;border-radius: 10px;}
	.progressNow{width: calc(0% - 20px);background: #FFF;border-radius: 10px;}
	.progressPer{background: transparent; text-align:center;color:#A6A6A6;}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<body>
<h1>
	File Upload Test
</h1>

<P>  The time on the server is ${serverTime}. </P>
	<div style="width: 100%;padding: 25px;">
		<form id="fileForm" action="/upload.do" method="post" enctype="multipart/form-data">
			<input type="file" name="uploadFile" multiple>
			<button type="button" id="btn">전송</button>
		</form>
		<div class="progressContainer">
			<div class="progress progressTotal"></div>
			<div class="progress progressNow"></div>
			<div class="progress progressPer">0 %</div>
		</div>
	</div>
</body>
<script>
	$(function(){	
		$("#btn").on("click", function(){
			console.log("click Time : " + new Date);
			
			var form = $("#fileForm")[0];
			var formData = new FormData(form);
			$.ajax({
				type: "POST",
				enctype: 'multipart/form-data',
				url: "/upload.do",
				data: formData,
				processData: false,
				contentType: false,
				cache: false,
				xhr: function(){
					var xhr = $.ajaxSettings.xhr();
					xhr.upload.onprogress = function(e){
						var per = e.loaded * 100 / e.total;
						progressBar(per);
					};
					return xhr;
				},
				success: function (data) {
					console.log("SUCCESS : ", data);
				},
				error: function (e) {
					console.log("ERROR : ", e);
				}
			});
		});
	});
	
	function progressBar(per){
		if(per > 55){
			$(".progressPer").css("color", "#000");
		}
		per = per.toFixed(1);
		$(".progressPer").text(per+" %");
		$(".progressNow").css("width", "calc(" + per + "% - 20px)");
	}
</script>
</html>

btn이라는 버튼을 클릭하면 file에 존재하는 데이터를 넘기고 xhr메소드에 정의된 onprogress 메소드에 의해 결과 값을 노출 하는 예제입니다.

 

 

결과

극단적으로 퍼센트를 잘 보기위해 3천개의 텍스트파일을 업로드하는 테스트를 진행해봤습니다.

 

좀 더 자세한 xhr에 대해서는 아래 URL을 참조바랍니다.

 

developer.mozilla.org/ko/docs/Web/API/XMLHttpRequest/upload

 

XMLHttpRequest.upload

XMLHttpRequest upload 프로퍼티는 업로드 진행 상황을 모니터링 할 수 있는 XMLHttpRequestUpload 객체를 반환합니다.

developer.mozilla.org

 

반응형
반응형

기존에 운영중이던 프로젝트를 고도화 시키면서 특정 서비스단에서 전달된 값을 DB에 저장하고 저장된 정보를 바로 특정 소켓 서버로 전달을 해줘야 하는 기능을 작성해야 했습니다.

이미 tomcat으로 was서버를 운영중인데 소켓 서버를 운영하는건 여러가지로 문제가 있을것이라 판단하여 별도로 빼려고 했지만, 실시간성이 좀 더 중요시 되었고, 한 곳이 망가져도 바로 재부팅으로 올려도 괜찮다는 확인을 받아 바로 소켓처리를 진행해보았습니다.

 

먼저 단순 소켓서버만 올린다면 굳이 netty까진 쓰지 않아도 됩니다. io.Socket만으로도 충분히 훌륭하게 작성 할 수 있었지만 몇몇 요구사항들을 좀 더 쉽게 구현하기 위해 netty를 공부해보고 적용해보았던 소스를 공유하고자 합니다.

 

아래는 실제로 프로젝트에 적용해본 Socket Server, Socket Client 소스들입니다.

 

Netty Server

netty사용을 위해 메이븐 추가를 먼저 해줍니다.

 

pom.xml

<!-- netty -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.0.33.Final</version>
</dependency>

먼저 Controller에@PostConstruct어노테이션을 활용하여 was 서비스 구동시netty가 동작하도록 별도의 쓰레드를 돌렸습니다.

 

NettyController.java

@Controller
public class NettyController {

    @PostConstruct
    private void start() {
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    new NettySocketServer(5010).run();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

        }).start();
    }

    @PreDestroy
    private void destory() {

    }
}

서비스가 구동되면 start()가 동작되면서 별도로 작성한 NettySocketServer가 동작합니다.

내부에 5010은 임시로 지정한 소켓 포트입니다.

다음은 netty 소켓 서버를 확인해보겠습니다.

 

NettySocketServer.java

public class NettySocketServer {
    private int port;

    public NettySocketServer(int port) {
        this.port = port;
    }

    public void run() {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             public void initChannel(SocketChannel ch) throws Exception {
                 ChannelPipeline pipeline = ch.pipeline();
                 //handler setting
                 pipeline.addLast(new NettySocketServerHandler());
             }
         })
         .option(ChannelOption.SO_BACKLOG, 128)
         .childOption(ChannelOption.SO_KEEPALIVE, true);


        try {
            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

생성자를 통해 port값을 처리하고 run메소드를 통해 서버가 동작시 응답을 처리할 핸들러를 연결합니다.

다음은 핸들러입니다.

 

NettySocketServerHandler.java

@Sharable
public class NettySocketServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String readMessage = ((ByteBuf) msg).toString(Charset.forName("UTF8"));
        ctx.write(msg);
        System.out.println("message from received: " + readMessage);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

@Sharable 어노테이션을 활용하여 안전하게 데이터를 처리하도록 변경하였습니다.

 

테스트결과

톰캣을 구동하고 서비스를 올립니다.

정상적으로 netty가 동작하는지 확인하기 위해 테스트툴로 5010포트를 통해 연결하고 메시지가 출력되는지 확인하겠습니다.

 

 

Netty Client

다음은 client를 구성해보겠습니다.

 

NettySocketClient.java

public class NettySocketClient {
	
	private String msg;
	private String host;
	private int port;
	
	public NettySocketClient(String host, int port, String msg){
		this.msg = msg;
		this.host = host;
		this.port = port;
	}
	
	public void run() {
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		
		try {
			Bootstrap b = new Bootstrap();
			b.group(workerGroup);
			b.channel(NioSocketChannel.class);
			b.option(ChannelOption.SO_KEEPALIVE, true);
			b.handler(new ChannelInitializer<SocketChannel>() {
	            @Override
	            public void initChannel(SocketChannel ch) throws Exception {
	                ch.pipeline().addLast(new NettySocketClientHandler(msg));
	            }
	        });
	        
	        //client connect
			try {
				ChannelFuture f = b.connect(host, port).sync();
				f.channel().closeFuture().sync();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}finally {
			workerGroup.shutdownGracefully();
		}
	}
}

생성자를 통해 연결할 host, ip, 발신할 message를 담는 문자열 msg값을 처리합니다.

 

해당 값들을 통해 run()메소드가 동작하면 연결정보로 연결이 됩니다.

다음으로 클라이언트 핸들러를 보겠습니다.

 

NettySocketClientHandler.java

public class NettySocketClientHandler extends ChannelInboundHandlerAdapter {

	private String msg;
	
	public NettySocketClientHandler(String msg) {
		this.msg = msg;
	}
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		ByteBuf messageBuffer = Unpooled.buffer();
		messageBuffer.writeBytes(msg.getBytes());
	
		ctx.writeAndFlush(messageBuffer);
	
		System.out.println("send message {" + msg + "}");
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		System.out.println("receive message {" + ((ByteBuf) msg).toString(Charset.defaultCharset()) +"}");
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
		ctx.close();
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		System.out.println(cause);
		ctx.close();
	}
}

 

클라이언트 테스트

서버툴에서 4564포트를 열어두고 연결하여 test msg send!!!라는 문구를 발송하였습니다.

캡처와 같이 서버에 정상적으로 메시지가 수신된 것을 볼 수 있습니다.

반응형