ImageEditor9은 모바일 전용 자바스크립트 이미지 편집기로,

모바일의 특징을 고려하여 모바일에서 손쉽게 이미지를 편집할 수 있도록 제작하였다.

소스는 github에서 받을 수 있고, 데모도 확인할 수 있다.

여기에서는 ImageEditor9의 주요 이미지 편집 기능과 자바 스크립트로 실행하는 방법을 정리한다.

 

1. 이미지 편집 기능

모바일용 이미지 편집기이기 때문에 기본적인 편집 기능은 손가락 터치를 활용한 편집 기능이다.

이미지를 한 손가락으로 누른 체 이동하면 이미지의 위치를 옮길 수 있다.

손가락 두 개를 터치한 경우에는 상황에 따라 확대/축소를 하거나 회전하도록 구현하였다.

두 손가락을 대고 좁히거나 넓히면 확대 / 축소를,

두 손가락을 옆으로 움직이면 이미지를 회전 시킬 수 있다

(구현 알고리즘은 기울기를 계산하여 수직 이동에 가까우면 확대 축소를, 수평이면 회전시키는 방식이다.)

 

메뉴를 이용한 편집 기능은 메인 메뉴를 클릭하면 위 그림처럼 세로로 메뉴가 나타난다.

제공되는 메뉴는 다음과 같고, 순서데로 가로/세로 크기변경, 좌우 회전, 확대/축소의 편집기능과 나가기, 저장 기능이 있다.

손가락을 이용해서 사용하는 회전과 크기 조정 기능을 메뉴를 이용하여 사용할 수도 있다.

회전 기능은 왼쪽과 오른쪽으로 10도씩 이동할 수 있다.

크기 조정은 10% 단위로 크기로 확대하거나 축소할 수 있다.

 

비슷한 회전 기능으로 이미지의 크기를 변경하면서 90도로 회전하는 가로/세로 크기변경 기능이 있다.

즉, 앞서의 첫 그림과 같이 가로와 세로의 크기가 바뀌는 기능을 (오른쪽 그림) 제공한다.

모바일은 기기를 회전 시켜서 사진을 찍는 경우가 많아서 구현한 기능이다.

 

편집한 이미지는 저장하거나, 저장없이 프로그램을 종료할 수 있다.

 

2. 사용방법

이러한 모바일 전용 이미지 편집기 ImageEditor9은 자바스크립트(JavaScript) 라이브러로, 다음과 같이 자바스크립트로 간단하게 실행 할 수 있다.

먼저, 사용하고자 하는 HTML 파일에서 ImageEditor9 라이브러리(ImageEditor9.js)와 디자인(ImageEditor9.css) 파일을 포함한다.

<link rel="stylesheet" href="imageEditor9.css">
<script src="imageEditor9.js"></script>

 

ImageEditor9은 이미지 편집을 하려고 할 때 생성해도 되고,

다음과 같이 HTML 문서가 로딩되고(onload) 나면 미리 생성해도(new) 된다.

  window.onload = function() { 
    img = document.getElementById('img1');
    imageEditor = new ImageEditor9 (document.getElementById("imageEditor9"));
    imageEditor.onSave = function() {
      img.src = imageEditor.toDataURL();
    }
  }

ImageEditor9을 생성할 때(new) 이미지 편집기능을 구현한 div 태그(다음 코드)를 매개 변수로 지정한다.
( document.getElementById("imageEditor9") )

이 div 태그 안에 ImageEditor9에 필요한 내용들을 생성해서 편집 기능을 사용한다.

 

ImageEditor9을 생성하고 나면,

ImageEditor9에서 편집한 이미지를 받아서 처리할 저장(onSave) 이벤트를 지정해야 한다.

여기에서는 편집한 이미지를 Base64로 받아서(toDataURL), 원본 이미지(img) 태그의 이미지를(src) 교체한다.

실제 사용할 경우에는 편집한 이미지 정보를 서버로 전송해서 저장하는 형태로 사용할 것이다.

<body>
  <div id="imageEditor9"></div>
</body>

이렇게 생성한 ImageEditor9은 showFullscreen() 함수로 화면에 보이도록 실행한다.

그리고, 편집할 이미지 원본을 지정하면(setImageSource), ImageEditor9에서 해당 이미지를 편집 할 수 있다.

  function ev_fullEditor(){
    imageEditor.showFullscreen();
    imageEditor.setImageSource(img);
  }
  

showFullscreen() 함수외에도 showFulldocument()함수가 제공된다.

showFullscreen() 함수는 모바일의 작은 화면을 최대한 활용하기 위해,

스크린 전체를 편집화면으로(requestFullscreen()) 사용하는 기능으로

웹브라우저에서 F11을 눌렀을때, 전체 화면으로 표시되는 기능을 이용한 것이다.

 

이 기능은 애플의 사파리(Safari)에서 동작하지 않기 때문에 showFulldocument() 함수를 제작하였다.

showFullscreen() 함수로 실행해도 애플의 사파리(Safari)에서는 showFulldocument()로 실행된다.

showFulldocument()는 스크린이 아닌 문서에 꽉찬 실행으로 팝업 다이얼로그와 같은 방식으로 구현했다.

 

이상으로 ImageEditor9은 모바일 전용 자바스크립트 이미지 편집기의 편집 기능과 사용방법에 대해서 정리하였다.

ImageEditor9은

모바일의 특징인 손가락을 이용한 편집 기능과 

모바일의 작은 화면을 극복하기 위해 전체 화면으로 이미지를 편집할 수 있는 기능에 맞추어 제작하였다.

 

gu-upload는 웹 사이트에서 파일을 업로드하는 HTML5 기반 JavaScript 라이브러리로 

2014년에 처음 제작하여 현재까지 조금씩 수정하여 사용하고 있다.

2016년에 수정하고 작성한 문서는 여기서 확인할 수 있고

그 동안 사용하면서 불편하거나 문제가 된 사항을 이번에 수정하면서 새롭게 문서를 작성한다.

 

gu-upload는 기본적으로 HTML5 기반이라 IE(Edge), Firefox, Chrome에서 실행된다.

그리고, IE 9 이전의 웹 브라우저에서는 플래시로 제작된 SWFUpload가 실행된다.

아직도 옛 버전의 IE를 사용하는 사람과 기입들이 있어서 기능 수정 없이

같이 제공하고 있으며, 몇 년 뒤에는 제외할 예정이다.

 

주요 기능은 다음과 같다.

  1. 탐색기에서 드래그 앤 드롭(Drag & Drop)으로 파일을 추가하거나, gu-upload를 더블 클릭하여 파일 추가.
  2. 여러 개의 파일을 선택해서 업로드 및 전송 상태 표시.
  3. 추가한 파일을 리스트나 미리보기(이미지용)로 설정 사용
  4. 기타 파일 크기나 개수 제한 기능 등

 

gu-upload의 소스는 github에서 받을 수 있고,

gu-upload을 사용하는 Java(Spring) 예제도 여기에서 받을 수 있다.

PHP등의 다른 개발 언어도 전송 받은 파일을 저장하는 부분만 다를 뿐,

gu-upload의 사용법은 모두 동일하다.

여기에서는 gu-upload을 사용하는 Java(Spring) 예제를 중심으로 gu-upload의 사용법을 정리한다.



먼저, gu-upload를 사용하기 위해서는 github에서 소스를 받아서 웹 프로젝트의 자바 스크립트 폴더에 넣어주면 된다.

( Java(Spring) 예제를 Eclipse에서 받아서 전체 코드와, 블로그의 내용을 같이 보는 것이 이해하는데 좋을 것 같다.)

guupload폴더에 있는 guuploadManager.js와 css 폴더에 있는 guupload.css가 기본 파일로

gu-upload를 사용하려는 웹 페이지에 다음과 같이 각 파일을 포함해서 사용한다.

<link rel="stylesheet" type="text/css" href="js/gu-upload/css/guupload.css"/>
<script type="text/javascript" src="js/gu-upload/guuploadManager.js"></script>

guupload.css는 gu-upload에서 사용하는 모든 디자인(CSS) 클래스를 모아놓은 파일로

gu-upload를 사용하는 사람의 취향에 맞추어 수정해서 사용하면 된다.

guuploadManager.js는 gu-upload와 SWFUpload를 관리하는 부분으로,

사용하려는 웹 페이지에 이 두 개의 파일을 포함하고, 다음과 같이 사용하면 된다.

var guManager=null;

window.onload = function() {
	var option = {
		fileid: "attachFile",
		uploadURL: "upload",
		maxFileSize: 100,
		maxFileCount: 12,
		useButtons: false,
		afterFileTransfer: afterFileTransfer
	}
	guManager = new guUploadManager(option);
}

전체 소스 코드

웹 페이지 실행이 완료된 후(onload),

guUploadManager를 적절한 옵션으로 생성(new)하면 된다.

옵션 설명
fileid gu-upload를 생성할 태그의 ID (주로 div)
listtype 첨부할 파일을 보여주는 방식으로, 리스트와 미리보기(이미지) 방식이 있다.
별도로 지정하지 않으면 리스트 방식이고, 
thumbnail로 지정하면 미리보기 기능을 이용할 수 있다.
uploadURL gu-upload로 전송한 파일을 서버에서 받아서 저장할 컨트롤 주소
maxFileSize 전송할 파일의 최대 크기. 기본값은 20M.
maxFileCount 한번에 전송할 수 있는 파일의 최대 개수. 기본값은 10개.
useButtons 파일 찾아보기 버튼 사용여부. 기본값은 true
afterFileTransfer 파일을 전송한 후 호출할 콜백 함수.

fileid, uploadURL, afterFileTransfer는 반드시 지정해야 하는 옵션이고,

나머지 옵션은 지정하지 않는 경우 기본값이 적용된다.

 

먼저, fileid는 gu-upload를 생성할 태그의 ID로 주로 div 태그를 다음과 사용한다.

        <tr>
            <td>Contents</td>
            <td><textarea name="brd_contents" cols="55" rows="5" style="width: 500px;"></textarea></td>
        </tr> 
        <tr>
            <td>Attach File</td>
            <td>
                <div id="attachFile" style="width: 500px;"></div>
            </td>
        </tr>

전체 소스 코드

div 태그의 id 로 'attachFile'을 지정하였고, fileid 옵션에 'attachFile'을 지정하다.

지정된 이 div 안에 gu-upload가 생성되어 실행된다.

 

listtype은 첨부할 파일을 보여주는 방식으로, 

단순하게 리스트로 보여주는 방식과 이미지 파일을 위한 미리보기 방식이 있다.

별도로 지정하지 않으면 리스트 방식이고, 

thumbnail로 지정하면 미리보기 기능을 이용할 수 있다.

예제 소스 보기

예제 소스 보기

uploadURL은 gu-upload가 전송한 파일을 받아서, 서버에 파일로 저장하는 컨트롤을 지정하는 부분으로

각각의 서버 언어에 맞는 url을 지정하면 된다.

gu-upload 예제에서는 다음과 같이 Spring으로 제작하였고

url이 upload이기 때문에 uploadURL에 upload로 지정하였다.

	@RequestMapping(value = "/upload")
	public void upload(HttpServletResponse response, HttpServletRequest request, @RequestParam("Filedata") MultipartFile Filedata) {
	   	SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
	   	String newfilename = df.format(new Date()) + Integer.toString((int) (Math.random()*10));
	   	
		File f = new File("d:\\" + newfilename);
		try { 
			Filedata.transferTo(f);
		   	response.getWriter().write(newfilename);
		} catch (IllegalStateException | IOException e) {
			e.printStackTrace();
		}
	}	

소스코드

서버에 파일을 저장하는 부분은 gu-upload와 관련이 없는 부분으로,

(gu-upload는 파일을 서버로 전송하는 부분까지만 담당)

개발환경에 따라 upload.asp, upload.php, upload.do 등을 지정해서 사용하면 된다.

파일을 저장하고 나면, 반드시 반환 값(response.getWriter().write)으로 저장한 파일명을 넘겨야 한다.

파일명 중복, 보안등의 이유로 첨부 파일을 다른 이름으로 저장하는데,

이 파일 명을 반환해야 gu-upload가 저장한 파일명(filename), 파일 크기(filesize)와 조합하여 개발자가 사용할 수 있다.

 

옵션 중 가장 어려운 것이 afterFileTransfer으로 콜백함수를 지정해서 사용하는 것일 것이다.

gu-upload는 파일을 먼저 전송하고 사용자가 입력한 값을 전송하기 때문에

그림과 같이 일반적인 파일 업로드 프로그래밍 방식과 제법 큰 차이가 있다.

일반적인 웹 페이지에서는 사용자가 입력한 값들(Text)과 첨부 파일(file)을 같이 전송한다.

게시판을 예로 하면 제목, 글 내용등의 값(Text)와 첨부 파일(file)을 한 번에 서버로 전송한 뒤,

파일을 저장하고, 파일명(파일크기등)과 제목, 글 내용등을 데이터베이스에 저장한다.

 

gu-upload를 사용하는 경우에는

1. 첨부파일을 먼저 서버에 전송해서 저장하고

2. 서버에서 저장한 실제 파일명(realname)을 받아서, gu-upload가 가지고 있는 파일명(filename), 파일크기(filesize)을 조합해서 gu-upload 생성시 옵션으로 지정한 콜백함수(afterFileTransfer)로 넘겨 준다.

3. 콜백함수에서는 넘겨 받은 파일 정보를 사용자가 입력한 값과 같이 서버에 넘겨서 데이터 베이스에 저장한다.

 

따라서 일반적인 웹 페이지에서는 사용자가 저장 버튼을 선택하면,

지정된 함수(formSubmit)가 호출되어 사용자가 입력한 값을 확인하고,

form의 submit을 호출하여 입력값과 파일을 서버로 전송한다.

function formSubmit(){
	var brd_title = document.getElementById( 'brd_title' );
	if (brd_title.value==="") {
		alert("input!");
		return;
	}
	
	document.form1.submit();
}

전체 소스 코드

gu-upload를 사용하는 경우에는, 웹 페이지에서 사용자가 저장 버튼을 선택하면,

지정된 함수(formSubmit)함수가 호출되어 사용자가 입력한 값을 확인하고,

uploadFiles()를 호출하여 파일을 먼저 서버에 전송해서 저장한다.

function formSubmit(){
	var brd_title = document.getElementById( 'brd_title' );
	if (brd_title.value==="") {
		alert("input!");
		return;
	}
	guManager.uploadFiles();
}

function afterFileTransfer(realname, filename, filesize){
	var realname9 = document.getElementById( 'realname' );
	var filename9 = document.getElementById( 'filename' );
	var filesize9 = document.getElementById( 'filesize' );
	
	realname9.value = realname;
	filename9.value = filename;
	filesize9.value = filesize;
	
	document.form1.submit();
}

전체 소스 코드

파일 전송이 끝나면, 앞서 afterFileTransfer옵션으로 지정한 콜백함수(afterFileTransfer)가 실행된다.

파라미터로 전송한 파일의 실제 파일명(realname), 파일명(filename), 파일크기(filesize)가 넘어오고,

이 값을 다른 값들과 같이 서버에 전송한다(submit).

같이 전송하기 위해 다음과 같이 실제 파일명(realname), 파일명(filename), 파일크기(filesize)를 저장할 태그가 생성되어 있어야 한다.

    <form id="form1" name="form1" action="upload_save.jsp" method="post">
        ~~ 생략 ~~
        <input type="button" value="Submit" onclick='formSubmit()'  />
        <input type="hidden" id="realname" name="realname"/>
        <input type="hidden" id="filename" name="filename"/>
        <input type="hidden" id="filesize" name="filesize"/>        
    </form>

전체 소스 코드

예제에서는 저장할 데이터 베이스가 없기 때문에

클라이언트에서 전송한 값을 받아서 화면에 출력한다.

전송 파일은 2개 이상일 경우가 많기 때문에

파일을 전송한 후 콜백함수(afterFileTransfer)를 호출할때 각 파라미터의 값은 콤마(,)로 구분되어 저장된다.

<% 
String filename = request.getParameter("filename");
String realname = request.getParameter("realname");
String filesize = request.getParameter("filesize");
String[] reallist = realname.split(",");
String[] filelist = filename.split(",");
String[] sizelist = filesize.split(",");

for (int i=0; i<filelist.length; i++) {
	out.println(filelist[i] + " : " + reallist[i] + " : " + sizelist[i] + "<br/>");
}				
%>

전체 소스 코드

서버에서 이 값을 받아 사용할 경우에는

위 코드와 같이 파일명, 파일크기등의 각 값을 splite로 분할해서 사용하면 된다.

 

이상으로 gu-upload을 사용하는 Java(Spring) 예제를 중심으로 gu-upload의 사용법을 정리하였다.

향후에는 상태바, 파일 확장자 제한 기능 등을 추가할 예정이다.

 

퇴사하면서,

내가 만든 웹 에디터(damoEDITOR)가 잘되었으면 하는 마음으로 이 글을 작성한다.

영업자의 마인드로 작성된 소개서가 있지만

여기서는 개발자의 마인드로, 한가지 차이점을 중심으로 정리한다.

웹 에디터는 웹 브라우저에서 별도의 설치 없이 문서를 작성하게 하는 프로그램으로,

제법 많은 제품들이 있지만

오픈 소스로는 CKEditor와 NicEditor등이 대표적이고,

상용 제품으로 나모, Dext5, 태그프리 등의 3개 기업 제품이 있다.

NicEditor는 본 블로그에서 정리한 적이 있지만, 현재는 관리되지 않는 것 같다.

다양한 기능을 제공하는 CKEditor와 tinyMCE등의 오픈 소스 제품이 무료로 제공되어 폭넓게 사용되지만,

국내 공공 기관이나 대기업들에서는 상용 제품을 많이 사용한다.

상용이 조금 더 많은 기능을 제공하는 것도 있겠지만 기술 지원 등의 문제로 선호하는 것 같다.

상용은 나모, Dext5, 태그프리의 3개 기업 제품이 주도하는데,

아주 뒤늦게 우리 회사도 웹 에디터 시장의 한 귀퉁이를 파고 들면서 판매를 시작하였다.

(실제로는 자사 기업용 솔루션에서 사용하는 웹 에디터로 사용하였다.)

 

이 제품의 이름은 damoEDITOR (이름은 별로..)로 기존 상용 제품들과 거의 동일한 기능을 제공한다.

(기본 정보는 여기에서 확인할 수 있다.)

기존 상용 제품과 비교해 핵심 기능들은 동일하지만, 부가적인 기능에서 2% 부족하다.

이미지(IMG) 태그를 예로 들면,

기존 제품은 이미지 추가시 이미지 크기를 같이 입력해서 지정하지만,

damoEDITOR는 이미지를 추가하고 난 뒤에 마우스로 조절한다.

 

하지만 damoEDITOR는 한가지 차이를 가지고 제작되었고, 이 차이로 인해 몇 가지 장점이 있다.

 

대부분의 웹 에디터는(특히 상용은 모두) IFrame을 이용해서 제작되었다.

IFrame은 Inline Frame이라는 의미로 하나의 HTML문서내에서 다른 HTML문서를 보여주기 위해 사용한다.

IFrame은 웹 보안등의 문제로 사용을 제한하는 추세이지만,

오래 전에 개발된 웹 에디터는 IFrame으로 제작하는 것 외에는 방법이 없었다.

 

최근에는 IFrame 대신에 DIV로 개발하는 것이 추세가 되고 있다.

DIV로 제작할 경우 반응형 웹(Responsive Web) 페이지에 대응할 수 있는 장점이 있다.

이것이 damoEDITOR의 첫번째 장점이다.

웹 페이지(웹브라우저)의 크기에 따라 웹 에디터의 크기가 변하게 제작할 수 있다.

반면, IFrame을 기반으로 제작된 웹 에디터들은 크기 변화에 대응하는 것이 쉽지 않다.

최근(?)에 제작된 오픈소스 웹 에디터는 DIV를 이용하여 반응형 웹용으로 많이 제작되었다.

다만, 이 웹 에디터들은 제공되는 기능이 몇 가지 없다.

damoEDITOR는 다음 그림(우측 상단)에서 보는 것처럼

웹 브라우저의 크기에 따라 변할 뿐 아니라 제공되는 기능 버튼의 위치까지 변한다.

웹 에디터의 너비가 작아지면서 가려지는 버튼들을 보조 툴바를 이용해서 메뉴처럼 보여준다.

다음 그림과 같이 더 작아지면 메인 툴바를 한줄만 남기고,

가려진 버튼들은 모두 보조 툴바에서 사용 할 수 있다.

그림과 같이 모바일 전용으로 사용할 수도 있다.

 

두 번째 장점은 작성한 데로 보는 것이다.

IFrame내에서 호출되는 HTML은 IFrame 외부의 HTML과 격리되어 있다.

이 두 개의 HTML은 별개의 페이지로 전혀 다른 CSS가 사용된다.

즉, IFrame 외부의 HTML은 웹 에디터를 구매해서 사용하려는 기업의 웹 페이지이고,

IFrame 내부의 HTML은 웹 에디터의 영역으로 독립적인 영역이다.

따라서 두 개의 CSS가 사용되어 사용자에게 혼란을 야기한다.

 

HTML <a> 태그를 예로 하면,

웹 에디터에서 한 문장을 <a> 태그로 지정하면 밑줄(underline)이 생긴다.

만약, 사이트 전체에 <a> 태그의 CSS를 밑줄이 아닌 색상을 사용하도록 지정하면 

웹 에디터로 작성한 내용을 글 읽기로 볼 때, 다음 그림과 같이 다른 화면을 보게 된다.

(웹 에디터가 사용되던 초기에 이러한 문제로 클레임이 많았던 것으로 기억한다.)

위 그림은 일반 텍스트는 회색, 링크는 검은색으로 지정해서 사용한 사이트의 예제이다.



DIV로 개발한 제품은 

사용하는 웹 페이지의 CSS를 상속받아서 사용하기 때문에 이러한 문제가 생기지 않는다.

작성 화면과 조회 화면이 동일하게 보이는 것이다.

기존 제품들은 이문제를 해결하기 위해 웹에디터의 CSS에 자사 웹 페이지의 CSS를 수정해야 한다.

 

고려하지 못한 유사한 문제가 생길 경우

damoEDITOR는 damoEditor.css에서 수정하면 해당 기업의 CSS와 동일하게 맞출수 있다.

사용하는 웹 페이지가 <a> 태그에 밑줄도 색상도 아무것도 지정하지 않으면

웹 에디터 작성시 <a> 태그에 아무것도 나오지 않아 문제가 되기도 했다.

(링크에 아무런 표시를 하지 않는 사이트라면 UI에 대해서 고민을...)

이 경우 damoEditor.css에서 다음과 같이 지정해 주면 간단하게 해결된다.

.damoEditor a {
    text-decoration: underline 
}

이렇게 처리하고, 글읽기가 있는 경우 이상의 코드를 관련 부분에 수정해서 넣어줘야 한다.

그렇지 않을 경우, 글작성(웹에디터 사용)시에는 링크에 밑줄이 있는데,

글읽기에서는 없어서 혼란을 줄 수 있다.

 

다른 웹 에디터를 많이 사용해본 사람들에게는

이러한 기능이 오히려 혼동을 주기도 해서

기존 방식처럼 CSS가 별도로 운영되는 IFrame 방식도 제공한다. 

 

세 번째 장점은 제품에 포함된 damoEditor.css 파일을 수정해서

damoEDITOR를 사용하는 기업에서 마음 데로 디자인을 수정하고 개선할 수 있다는 것이다.

이것은 damoEDITOR만의 고유한 기능으로 CSS를 조금이라도 아는 사람(퍼블리셔)이 있다면,

자신의 웹 사이트에 맞춰서 다양하게 정의해서 사용할 수 있다.

이것은 damoEDITOR를 구성하는 거의 모든 디자인이 damoEditor.css에 있기 때문에 가능하다.

대부분의 웹 에디터에서 테마를 제공하지만, 

몇 가지 색상만 제공하기 때문에 

사이트 디자인과 웹에디터의 디자인이 맞지 않는 문제가 많이 있다.

 

더욱이 웹 에디터들은 딱딱한 기본 구성으로 제공되는데,

damoEDITOR는 사용하는 기업에 맞춰서 다양하게 사용할 수 있다.

 

다음 그림은 기본제공되는 사각형 색상 팔렛트를 간단한 CSS 수정을 통하여 원형으로 바꿔서 사용한 예제이다.

사용법은 개발자 메뉴얼에 정리되어 있다.

차후에는 이렇게 사용자 정의로 작성된 다양한 디자인을 정리해서 판매 옵션으로 넣는다면 좋을 것 같다.

 

이상으로 damoEDITOR의 세가지 장점을 정리하였다.

제품에 대한 설명과 체험은 여기에서 할 수 있다.

나름 GS 인증까지 받은 이 제품이 정말 많이 팔리기를 기원한다.

 

이미지 편집기 PaintWeb은 웹에서 이미지를 편집할 수 있는 웹 어플리케이션으로

Mihai Şucan이 Javascript로 제작하였다.

윈도우의 그림판을 웹으로 구현했다고 할 수 있다.

라이브러리데모만 제공되고,

사용법(특히 서버에서 이미지 저장하는 방법)에 대한 설명이 부족해서,

Spring4 기반으로 이미지를 저장하고 관리하는 예제(github)와 설정 파일 수정하는 방법(블로그) 등을 이전에 정리하였다.


이후 필요하여, 이미지 회전, 텍스트 입력, 확대/축소 후 저장 기능 등을 추가 구현하였다.

이미지 회전은 90도 단위로 이미지를 회전하는 기능으로 새로 추가하였다.

텍스트 입력은 기존에 있던 기능으로 불편하게 구현된 것을

기존의 그림판처럼 그림(canvas) 위에서 문자를 입력하도록 구현하였다.

확대/축소 후 프로그램을 종료하면 저장되지 않던 것을 저장되도록 변경하였다.

해당 예제는 기존의 github에서 받을 수 있고,

github에서 다운 받아 실행하는 방법은 다른 문서에 정리되어 있다.

이 문서에서 github 주소와 다운받을 폴더명만 적절하게 바꾸어 주면 된다.

설치 후 실행해서 확인 할 수 있고,

저장기능을 제외한 기능을 데모 페이지에서 확인할 수 있다.


github에서 다운 받으면 다음 그림과 같이 파일들이 저장된 것을 볼 수 있다.

webapp > js > paintweb 폴더가 PaintWeb 라이브러리 폴더로

사용하고자 하는 프로젝트의 폴더에 복사해서 사용하면 된다.

webapp > WEB-INF > jsp > imageEditor.jsp 파일이 PaintWeb을 실행하는 파일로 작성된 코드는 다음과 같다.

먼저, PaintWeb 라이브러리를 사용하기 위해서는 Paintweb.js 파일을 가져와야 한다 [라인7].

이외에도 html2canvas.min.js를 사용하는데 [라인 8],

html2canvas는 html을 이미지로 변환하는 라이브러리로

사용자가 입력한 텍스트를(HTML) 이미지로 변환하기 위해 사용하였다.

Chrome에서는 문제가 없지만, 그 외의 웹 브라우저에서는 속도가 조금 떨어지는 현상이 발생한다.


PaintWeb은 이미지 태그(editableImage [라인 11])의 내용을 읽어서[라인 34] 편집한다.

기본적으로 제공되는 예제는 메인 페이지에서 이미지(uploadImage)를 지정하고,

별창에서 PaintWeb을 실행하는 방식으로 구현했다.

(PaintWeb 관련 코드만 보이기 위해서 별창으로 구현했다.)

따라서 별창에서 메인 페이지에 있는 이미지(uploadImage)를 가지고 와서 [라인 19]

별창의 이미지 태그(editableImage)에 넣어주고 [라인 22]

이 이미지를 PaintWeb이 사용하도록 코드를 작성하였다 [라인 34].

메인 페이지의 이미지를 바로 지정하지 않고 이렇게 한 것은 PaintWeb이 부모(opener)의 이미지를 인식하지 못해서 그런 것으로,

중요한 버그가 아니라 수정하지 않았다.


PaintWeb을 생성하고 [라인 31],

각자의 환경에 맞추어 설정을 한 후, 실행하면 된다 [라인 39].

설정과 관련된 보다 자세한 설명은 이전 문서를 참고하면 되고,

새로 추가한 imageSaveURL, imageDownloadURL, afterImageSave를 정리하면 [라인 36~38]

imageSaveURL은 PaintWeb에서 저장한 이미지를 서버에서 받아서 저장할 URL을 지정한다.

저장할 이미지는 Base64로 전송이 되기 때문에 이 내용을 받아서 파일로 저장하는 코드가 있는 URL (스프링의 경우 컨트롤)을 지정하면 된다.

스프링으로 작성된 saveImage의 코드는 여기서 확인할 수 있다.

imageDownloadURL은 이미지를 저장하고, 다시 받아오기 위한 URL로 imageSaveURL과 마찬가지로 ImageEditor.java 파일에 구현되어 있고 github에서 확인할 수 있다.

afterImageSave는 저장 후 호출되는 함수로,

여기서는 이미지 저장 후 별창을 닫도록 하였다(close) [라인 43].


이외의 설정이나 상세한 설명은 이전 문서를 참고하면 된다.


이상으로 수정된 PaintWeb에 대하여 정리하였고,

저장기능을 제외한 기능을 설치없이 데모 페이지에서 확인할 수 있다.




구글에서 Javascript Image Editor로 검색하면, 제법 쓸만한 HTML5 기반의 유/무료 이미지 편집기가 조회된다.

이중에서 윈도우에서 제공하는 그림판과 유사하고 간단하게 사용할 수 있는 PaintWeb을 선호한다.

PaintWeb의 소스는 Github에서 다운 받을 수 있고, 웹 사이트에서 데모를 실행해 볼 수 있다.

하지만, PaintWeb을 Java, PHP, 닷넷(.Net) 등의 프로젝트에 넣어서 사용하는 방법에 대한 설명이 없다.

여기에서는 PaintWeb을 Java(Spring) 프로젝트에 넣어서 사용하는 데 필요한 사항을 2 가지로 정리하였다.


먼저, PaintWeb 라이브러리를 호출하여 실행하는 방법을 정리하였다.

PaintWeb을 실행하는 방법은 다운 받은 예제나 데모에서 확인 할 수 있지만, 작성된 코드가 제법 복잡해서 여기에 간단하게 정리하였다.


다음으로 작업한 이미지를 서버로 전송하여 저장하는 방법에 대하여 정리하였다.

작업한 이미지를 서버로 전송하여 저장하는 방법이 제공되지 않아서 Java로 제작하였고, 제작한 코드에 대하여 간단하게 정리하였다.

따라서, 첫 번째 내용만 조금 이해하면 PaintWeb을 자신의 프로젝트에 넣어서 사용할 수 있다.


이렇게 정리한 예제(image Editor)는 Github에서 다운받을 수 있다.


먼저 실행하는 방법을 정리한다.

다운 받은 소스에서 demos 폴더에 있는 데모(demo1.html)를 실행하면 Ajax 사용 등의 문제로 실행이 되지 않지만, 웹 프로젝트에 넣어서 경로 설정을 해주면 쉽게 사용할 수 있다.

다운 받은 파일 중 src 폴더가 PaintWeb의 라이브러리이므로, 사용할 웹 프로젝트의 자바 스크립트 폴더에 src 폴더를 paintweb 폴더라는 이름으로 복사한다.

demos 폴더에 있는 demo1.html을 웹 파일이 있는 폴더에 적절한 이름(demo1.jsp)으로 넣고 실행하면 된다.

demo1.html 파일을 열어서 PaintWeb 라이브러리 경로를 다음과 같이 수정한다.

수정 전   <script type="text/javascript" src="../build/paintweb.js"></script>
수정 후   <script type="text/javascript" src="js/paintweb/paintweb.js"></script>

demo1.html을 수정해서 실행시켜도 되고, 본 예제에서는 demo1.html 의 코드를 정리해서 index.jsp(실제로는 imageEditor.jsp)로 작성하였다.


예제로 작성한 프로젝트(imageEditor)는 Github에서 다운 받을 수 있다.

imageEditor에서는 PaintWeb라이브러리를 js 폴더에 넣어서 사용한다.

이외에 index.jsp와 ImageEditor.java, ImageEditor.jsp 파일이 예제로 작성된 파일이다.


index.jsp 은 편집할 이미지를 선택하는 페이지로 스프링(Spring) 프레임워크가 아닌 JSP 파일로 작성했다.

사용자가 편집할 이미지를 선택하면 해당 이미지를 서버로 전송하고(fileUpload)

이 이미지를 다시 다운 받아서 (fileDownload)

HTML 이미지 태그(img)에서 보여 주게 된다.

index.jsp에서는 이 기능을 구현해 두었고,

fileUpload, fileDownload 컨트롤을 ImageEditor.java에 작성하였다.


톰캣에서 다운 받은 예제를 실행한 후 웹 브라우저로 다음 주소를 입력하여 index.jsp의 실행을 확인할 수 있다.

http://localhost:8080/imageEditor/

화면 하단에 있는 [Load Image]을 클릭하여 편집할 이미지를 선택하면 다음 그림과 같이 수정(Modify) 하거나 삭제(Delete) 할수 있는 메뉴가 나타난다.

index.jsp에서 수정(Modify) 기능을 선택하면 PaintWeb 기반의 이미지 편집기가 실행된다.

이미지 편집기의 컨트롤(url)은 imageEditor로 Java로 작성된 코드는 없지만 imageEditor.jsp 파일을 뷰로 사용하여 이미지 편집을 할 수 있도록 하였다.

imageEditor.jsp 파일에서 PaintWeb를 실행하여 이미지를 편집하고, 이미지를 서버로 전송(saveImage)하여 저장한다.

saveImage 컨트롤도 ImageEditor.java에 작성되어 있다.


이상의 흐름을 순서대로 정리하면 다음과 같다.


demo1.html 파일의 PaintWeb 실행 코드를 보면 실행 시간을 측정하는 코드 등이 있어서 아주 복잡하게 보인다.

이 코드는 다음과 같이 단순화 해서 사용해도 된다.

단순화한 실제 코드는 PaintWeb을 Java에서 사용하기 위한 예제로 작성한 프로젝트 파일 중 imageEditor.jsp의 코드를 참조하면 된다.

    pw = new PaintWeb();
    pw.config.guiPlaceholder = document.getElementById('PaintWebTarget');
    pw.config.imageSaveTo   = imageSaveTo;
    pw.config.imageLoad      = document.getElementById('editableImage');
    pw.config.configFile        = 'config-example.json';
    pw.init();


이상으로 PaintWeb을 사용하는 방법과 예제 프로젝트의 전체 구조를 정리하였다.

추가적으로 PaintWeb의 속성과 사용법 몇 가지를 더 정리하였다.


guiPlaceholder는 이미지 편집기인 PaintWeb를 생성할 Div를 의미한다.

지정된 div의 자식으로 PaintWeb의 편집 도구들이 생성된다.

imageLoad는 편집할 이미지 태그(img)를 의미한다.

PaintWeb은 HTML 이미지 태그(img)의 이미지를 가져와서 편집한다.

configFile은 PaintWeb을 실행할 설정 파일로 제법 다양한 기능을 지정할 수 있다.

Init() 함수는 지정된 설정에 따라 PaintWeb을 실행하는 메소드이고,

파라미터로 지정된 pwInit 는 콜백(callback) 함수로 PaintWeb의 실행 결과를 반환한다.

생략해도 된다.

생략할 경우 편집할 이미지 태그를 숨기는 코드를 (display = 'none') 적당한 곳에 작성해야 한다.


configFile은 config-example.json이 예제로 제공된다.

소스는 JSon으로 작성하는데, 주요한 몇 가지 설정을 정리하면 다음과 같다.

속성

설명

lang

사용할 언어를 지정한다.

viewportWidth

이미지 편집 영역의 너비를 지정한다.

viewportHeight

이미지 편집 영역의 높이를 지정한다.

fillStyle

도형의 배경색을 지정한다.

strokeStyle

도형의 전경색을 지정한다.

tools

편집기에 사용할 도구들을 선택한다.

toolDefault

편집기가 실행되면 기본으로 사용될 도구를 지정한다.

fontFamilies

문자열(text) 입력시 사용할 폰트 리스트를 지정한다.

fontFamily

폰트 리스트 중 기본으로 사용할 폰트를 지정한다.


먼저 언어는 영어(en)가 기본으로 지정되어 있다.

한국어(ko)는 개인적인 필요로 몇 가지만 번역해서 github에 공유하였다.

이 파일을 PaintWeb의 lang 폴더에 넣고 configFile(config-example.json)의 lang 속성을 ko로 수정하면 한국어로 사용할 수 있다.



viewportWidth와 viewportHeight는 이미지를 편집할 영역의 너비와 높이를 지정하고,

fillStyle은 도형을 그릴 때 사용할 배경색, strokeStyle은 전경색의 기본값을 의미한다.

문자열(Text)일 경우 폰트 색을 나타내는 데, fillStyle와 strokeStyle와 구분되어 다소 이상하게 출력된다.

Tools는 PaintWeb에서 제공하는 편집 도구들로, tools 옾션에서는 자신에게 필요한 도구들만 선택해서 사용할 수 있다.

예로 ["bcurve", "cbucket", "cpicker"]를 지정하면 이 3가지 도구만 나타난다.

toolDefault는 편집기가 실행되면 기본으로 사용될 도구를 지정하는 것으로 선(line) 도형이 지정되어 있다.

그림판처럼 selection을 지정해서 사용해도 좋을 것이다.

fontFamilies은 문자열(text) 입력의 세부 옵션으로 이미지 편집기에서 사용할 폰트들을 배열로 지정하면 된다.

기본적으로 영어 폰트들이 지정되어 있고, [‘굴림체’, ‘바탕체’]처럼 한글 폰트를 지정해서 사용할 수 있다.


이상으로 PaintWeb을 사용하는 예제에 대해서 정리하였다.

다음으로 PaintWeb에서 편집한 이미지를 서버에 전송해서 저장하고,

이 이미지를 원래 이미지 태그(img)에 갱신하여 보여주기 위해 작성한 코드에 대해서 정리한다.

PaintWeb을 Java에서 사용하기 위한 예제로 작성한 프로젝트는

웹 페이지에서 이미지를 지정하고,

지정된 이미지를 PaintWeb로 편집하고,

편집된 이미지를 서버에 저장하고

처음에 지정된 이미지를 갱신해서 보여주도록 제작되었다.


웹 페이지(index.jsp)에서 이미지를 지정하고,

지정된 이미지를 PaintWeb으로 편집하고(action-imageEditor),

편집된 이미지를 서버에 저장하고(Ajax-imageSaveTo())

처음에 지정된 이미지를 갱신(# + 시간)해서 보여주도록 제작되었다.


웹 페이지에서 작성한 이미지를 서버로 전송하는 방법은 이미지를 일종의 문자열 값인 base64 코드로 변환하여 전송하는 방식을 이용한다.

PaintWeb의 저장 도구()를 클릭하면 이미지를 base64로 변환해서 작성자의 PC에 저장하는 기능이 PaintWeb에 구현되어(imageSave()) 있다.

base64로 변환된 값을 파일로 저장하는 대신 Ajax를 이용하여 서버로 전송하도록 작성하면 된다.

PaintWeb의 imageSave() 함수 중간에 별도의 함수로 imageSave()를 호출하도록 작성하였다.

PaintWeb의 imageSave()에서 작성해도 되지만 가급적 기존 코드를 손대기 않기 위해서 별도의 함수로 작성했다.

imageSave() 함수의 내용이 복잡해 보이지만 이미지 저장시 전송율을 보여주기 위해 진행 상태를 %로 보여주는 기능으로 코드가 복잡해 보일 뿐 실제 코드는 단순하다.

다음 코드는 전송율을 보여주기 위한 코드를 생략하고 작성한 코드이다.

 

Ajax를 이용하여 전송(send)이 완료되면 (onload) [라인 13]

이미지 태그를 생성하여 [라인 14]

전송한 이미지를 다시 다운로드 받아서(fileDownload) 넣어주고 [라인 19],

이렇게 다운 받은 이미지 주소를 기존 이미지에 지정하여 새롭게 갱신(reload)한다 [라인 16].

다운 받을 때 다른 파일(URL)이라는 의미로 파일명 뒤에 “#img”와 시간을 붙여서 사용한다 [라인 16].

이렇게 복잡하게 작성한 이유는 웹 브라우저가 기존의 이미지를 캐시(cache)로 가지고 있어서 이미지 갱신이 잘 안되기 때문이다.


이미지 갱신(reload)이 제대로 되니 위해서는 이상과 같이 자바 스크립트뿐만 아니라 파일을 다운로드 하는 Java에서도 다음 코드를 추가해 주어야 한다.

public void fileDownload(HttpServletResponse response, String path, String filename) {
    ~~ 생략 ~~
    response.setHeader("Pragma", "no-cache");
    response.setHeader("Cache-Control", "no-cache, must-revalidate");
    response.setDateHeader("Expires", 0);
    ~~ 생략 ~~
}   

이 코드가 없으면 IE에서 제대로 갱신되지 않는다.


Ajax에서 서버로 이미지를 전송하면(saveImage) [라인 5]

서버(java - saveImage)에서는 전송 받은 이미지 정보를 BASE64Decoder를 이용하여 이미지로 변환해서[라인 15] 지정된 경로에 파일로 저장한다.




테이블 정렬(table sorting)

자바 스크립트 기본 문법을 이해하기 위한 예제였고

테이블 컬럼 이동 (Drag and Drop Table Columns) 예제를 구현하면서

자바 스크립트의 이벤트(마우스) 처리와 CSS 이용에 대한 방법을 정리한다.

테이블 컬럼 이동은

특정 컬럼(A)을 마우스 버튼을 누른 체 움직여서 (Drag)

다른 컬럼(B)에서 멈출 경우 (Drop)

특정 컬럼(A)을 해당 컬럼(B)의 앞으로 이동시키는 것을 의미한다.

개발 진행 과정은 이해를 쉽게 하기 위해

다음과 같이 단계별로 진행한다.

  1. 마우스 이벤트 기본 구현
  2. CSS 클래스 문제 해결
  3. 컬럼 이동(Drag and Drop) 처리
  4. 컬럼 이동(Drag and Drop) 중에 이동 컬럼 헤드 보기
  5. 컬럼 이동(Drag and Drop) 중에 이동 컬럼 전체 보기
  6. JQuery


예제 코드는 다음 사이트의 코드를

단계별로 풀어서 쉬운 부문만 정리하였으니

실제로 사용할 경우에는

다음 사이트의 코드를 활용하거나

dragtable같은 라이브러리를 사용하는 것이 좋다.

JQuery의 Sortable 예제도(웹페이지 중간의 Sort tables)

좋은 참고가 될 수 있다.


테이블 컬럼 이동 (Drag and Drop)은

4가지 마우스 액션에 의해 이루어 진다.

먼저, 이동하려는 컬럼의 헤드를 마우스로 누르고(Mouse Down)

이동할 위치의 컬럼으로 마우스를 움직여서(Mouse Move)

클릭한 마우스 버튼을 놓으면 (Mouse Up)

이동하려는 컬럼을 이동할 컬럼의 앞으로 옮겨준다.

이때, 이동하려는 컬럼과 이동할 위치의 컬럼을

CSS를 이용하여 구별해주는 것이 좋다.

특히, 이동할 위치의 컬럼으로

마우스가 올라오면(Mouse Over) 배경색을 지정하고,

다른 컬럼으로 이동하면

즉, 마우스가 현재 컬럼에서 떠나면(Mouse Out) 원상태로 되돌린다.

따라서 Mouse Move 이벤트는

Mouse Over와 Out으로 구분하여 처리한다.


이상의 개념으로 직접 구현해 본 후,

다음 코드를 보는 것이

실력 향상에 도움이 되니 시도해 보길 바란다.


위 코드에서 CSS 부분은 [라인 5 ~25]

Table 태그에 대한 스타일과

이동하려는 헤드(TH)의 CSS 클래스(dragging - 라인 16)와

이동한 새로운 위치의 CSS 클래스(hovering - 라인 21)이다.


다음으로 자바 스크립트에서는

문서가 로드 되고(onload) 나면 [라인 28]

모든 헤드(TH)를 찾아서 (getElementsByTagName – 라인 29)

사용할 마우스 이벤트에 이벤트 핸들러들을 지정한다.

여기서 컬럼의 헤드로 TH 태그를 사용했지만

TH 태그를 사용하지 않고 TD 태그를 사용할 경우에는

첫 번째 행(TR)의 TD가 헤드가 된다.


웹페이지에서 마우스를 누르고 움직이면(드래그하면)

기본적으로 해당 범위 안에 있는 내용들이 선택(select)된다.

여기서는 드래그하면 컬럼을 이동시킬 것이기 때문에

내용이 선택되지 못하게(return false)

onselectstart 이벤트에서 처리해 준다 [라인 31].


마지막 부분은 HTML 부분으로

테이블(Table) 태그를 생성하고

3개의 행(TR)과 10개의 컬럼(TH, TD)을 생성하였다 [라인 64~].


마우스 이벤트에 대해서 좀더 상세하게 정리하면

특정 컬럼을 마우스로 누르면(MouseDown – 라인 40),

현재 눌려진 컬럼(this)이 이동할 컬럼이라고 여겨

dragTD 변수에 넣어서 보관한다 [라인 41].

그리고 시각적인 표시를 위해

이 컬럼에 CSS dragging 클래스를 지정한다. [라인 42]

이 컬럼은 다른 컬럼보다 조금 어둡게(background:#eee) 처리한다.


이동할 대상 컬럼의 헤드에 마우스가 올라오면(Over – 라인 45)

Hovering 클래스를 지정해서

다른 색상(background:#ccc)으로 표시한다 [라인 47].


이 컬럼에서 마우스가 떠나면(Out – 라인 - 50)

Hovering 클래스를 지정해서 배경색을 바꾸었던 것을

Hovering 클래스를 지워서 원상태로 돌린다 [라인 52].


옮기려는 컬럼에서

눌렀던 마우스 버튼을 놓으면 (Mouse Up – 라인 55)

현재 컬럼에 지정된 Hovering 클래스를 지우고 [라인 56]

드레그 하던 컬럼(dragTD)에 지정된

dragging 클래스를 지워주고 [라인 57]

드레그 하는 필드를 초기화 한다 [라인 58].

컬럼에 마우스가 오거나(over), 떠나면(out)

CSS 클래스를 이용하여

배경색을 지정하거나 제거하게 되는데,

마우스가 눌러진 상태에서만 작동하고

그냥 마우스가 올라오거나 떠났을 때는 아무 것도 하지 않아야 한다.

즉, 마우스가 눌러진 상태를 확인하기 위해

dragTD 변수의 값이 있는지 확인한다 [라인 46, 51].

dragTD 변수의 값은 마우스 버튼이 눌러졌을 때는

해당 컬럼의 헤드를 가지고 있고 [라인41],

놓았을 때는(Up) null을 가지고 있다 [라인58].


마우스 이벤트와 관련된 상세한 정보는 자료를 찾아보기 바라고

여기에서는 사용하지 않았지만

각 이벤트 핸들러에서 파라미터로 사용된 ev에 대해서 정리한다.

ev 변수는 Event 객체를 나타내는 것으로

현재 발생한 이벤트의 각종 정보를 가지고 있다.

클릭 이벤트의 경우 클릭된 개체를

event.target (ev.target)으로 알 수 있다.

여기서는 this를 사용하였다.

이외에 클릭된 위치 좌표나,

클릭된 마우스 버튼(왼쪽, 오른쪽)등의 정보를 알 수 있다.


마우스 이벤트 외에도 많은 이벤트 들이 있으니

잘 익혀두어야 하고,

이상의 마우스 동작은 마우스를 이용하여 움직이는

대부분의 프로그램에서 사용되는 방식이니

잘 이해해 두는 것이 좋다.

(페이지 마지막에 있는 예제 참조)


정리하면,

마우스 버튼을 누르면(down)

해당 개체 정보를 보관하고 표시를 한다.

마우스 버튼을 놓으면(up)

보관된 정보를 초기화하고

작업 대상으로 표시한 것을 원래대로 되돌린다.

작업 대상이 있을 경우에만,

마우스의 움직임(Move-over, out)에 따라

필요한 행동을 하도록 작성한다.



두 번째 구현으로

테이블 헤드(TH)의 배경색(background:#aaa)을 지정해 본다.

기존 코드의 HTML에서 Style로 지정해도 되지만

다음과 같이 tableHead라는 클래스를 추가해서 테스트 한다.

.tableHead{
    background:#aaa;
    cursor:pointer
}
</style>   
<script>
window.onload = function() {
    var head = document.getElementsByTagName("th");
    for (i=0; i<head.length; i++) {
        head[i].onselectstart = function() { return false }
        head[i].onmousedown = mousedown;
        head[i].onmouseover = mouseover;
        head[i].onmouseout = mouseout;
        head[i].onmouseup   = mouseup;
        head[i].className = "tableHead";
    }

}


이렇게 추가하고

드레그를 해보면 버그가 발생하는 것을 알 수 있다.

tableHead에 의해 배경색이 어두운 색으로 지정되었는데,

드레그를 하고 난 뒤에

배경색들이 모두 사라지는 것을 볼 수 있다.

이 버그를 해결한다.

function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
}

function mouseover(ev){
    if (dragTD === null) { return;}
    addClass(this, "hovering");
}

function mouseout(ev){
    if (dragTD === null) { return;}
    removeClass(this, "hovering");
}

function mouseup(ev){
    removeClass(this, "hovering");
    removeClass(dragTD, "dragging");
    dragTD = null;
}

function addClass(src, classname) {
    if (src.className.indexOf(classname) === -1 ) {
        src.className += " " + classname;
    }
}

function removeClass(src, classname) {
    src.className = src.className.replace(" " + classname, "");
}

전체 코드

버그가 발생한 것은

CSS 클래스를 사용할 때

className에 값을 바로 지정했기 때문에 생긴 것이다.

테이블 헤더에 CSS 클래스가 없을 때는

다른 CSS 클래스를 넣고 빼도 (className = "") 문제가 없었다.

하지만 head[i].className = "tableHead"로

이미 CSS 클래스가 지정되어 있는데

this.className = "dragging"나 "hovering"으로 지정하면

기존 것이 지워지고 새로운 CSS 클래스가 설정된다.

초기화 하기 위해 className = "" 로 지정하면

기존 CSS 클래스가 지워지게 된다.

기존에 있던 tableHead를

추가해 주는 것이 방법이 될 수 있겠지만,

여러 개가 사용되면 다시 문제가 생기기 때문에 좋지 않다.

웹 개발시 많은 CSS 클래스가 사용되는데

매번 추가해주는 작업을 할 수 없다.


일반적으로 많이 사용하는 방식은 다음과 같다.

         className += " " + classname

대입(=)하는 것이 아니고,

문자열을 추가(+)하는 방식으로 사용한다.

다만, 기존에 있는 클래스명과 연결되는 것을 막기 위해

공백 하나(" ")를 추가해 준다.

즉, className속성에 "A B C D ..." 으로 이어지게 구성한다.

반대로 CSS 클래스를 제거할 때는

className.replace(" " + classname, "")와 같이

해당 클래스 이름을 찾아서 공백으로 바꾸어(replace) 준다.

이것을 좀더 쉽게 사용하기 위해

addClass(), removeClass()로 함수화 해서 사용했다.


세번째로 실제 컬럼 이동(Drag and Drop) 을 구현해 본다.

마우스 버튼을 누른 상태에서 이동한 후

마우스 버튼을 놓으면 (up)

눌렀을 때의 컬럼(dragTD)을

놓았을 때의 컬럼 앞으로 옮겨주면 된다.

컬럼(열)을 이동하는 방법은

앞서 행을 정렬하면서 사용한 것처럼

insertBefore 함수를 사용하지만

구현이 조금 더 어렵다.


테이블 태그는

하나의 행에 모든 컬럼의 정보가 있기 때문에

테이블 정렬에서는 하나의 행(row)을 이동시켰다.

하지만 컬럼을 이동하는 것은

하나의 컬럼에 모든 행의 정보가 있는 것이 아니기 때문에

처리가 복잡해 진다.

컬럼의 정보는 행에만 있기 때문이다.

즉, 컬럼을 이동시키려면

모든 행의 해당 컬럼을 찾아서 이동시켜야 한다.

따라서 이동시킬 컬럼을 찾기 위해

행 내의 컬럼의 위치 정보를 이용한다.

function mouseup(ev){
    removeClass(this, "hovering");
    removeClass(dragTD, "dragging");
   
    var srcInx = dragTD.cellIndex;
    var tarInx = this.cellIndex;
    var table = document.getElementById("tableOne");
    var rows = table.rows;
   
    for (var x=0; x<rows.length; x++) {
        tds = rows[x].cells;
        rows[x].insertBefore(tds[srcInx], tds[tarInx])
    }
   
    dragTD = null;
}

전체 코드

컬럼의 위치를 나타내는 속성은 cellIndex이다.

즉, 이동시키고자 하는 컬럼을 cellIndex로 찾고 (srcInx)

이동할 위치의 컬럼을 cellIndex로 찾아서 (tarInx)

insertBefore로 옮겨 주면 된다.

         rows[x].insertBefore(tds[srcInx], tds[tarInx])

이것을 모든 행에 대해서 진행해야 하기 때문에

행의 개수(table.rows.length) 만큼 반복한다.


지금까지는 컬럼을 드레그 할 때

선택한 컬럼의 배경색을 바꾸도록 표시하였다.

네번째로 좀더 쉽고 편히 볼 수 있도록

이동하는 마우스 옆에

현재 선택된 컬럼의 내용이 나타나도록 구현한다.

구현 방법은

마우스가 움직일 때 마다(MouseMove)

커서의 좌표를 구해서

컬럼의 내용을 보여준 개체(div)에 지정한다.

<style>
.draggedDiv {
    width:auto;
    height:auto;
    padding:2px 8px;
    border:1px solid #000;
    position:absolute;
    background:#eee;
}
</style>   
<script>
window.onload = function() {
    ~~ 생략 ~~
    document.documentElement.onmouseup = documentMouseup;
    document.documentElement.onmousemove = documentMouseMove;
}
function documentMouseup(ev){
    if (dragTD) {
        removeClass(dragTD, "dragging");
        dragTD = null;
        draggedDiv.parentNode.removeChild(draggedDiv);
        draggedDiv = null;   
    }
}
function documentMouseMove(ev){
    if (!draggedDiv) { return;}
   
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
}
var dragTD = null, draggedDiv=null;
function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
   
    draggedDiv = document.createElement("div")
    draggedDiv.innerHTML = this.innerHTML;
    draggedDiv.className = "draggedDiv";
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
    document.body.appendChild(draggedDiv);
}
function mouseup(ev){
    draggedDiv.parentNode.removeChild(draggedDiv);
    draggedDiv = null;
    ~~ 생략 ~~
}

전체코드

이동하려는 컬럼을

별도의 창에서 보여주기 위해서

별도의 창을 동적으로 생성하고 삭제하도록 구현했다.

별도 창(DIV)을 생성하는 것은 (createElement("div"))

이동할 컬럼을 선택하는 시점

즉, 마우스 버튼이 눌려질 때이다 (Mouse Down).

삭제(removeChild)되는 시점은

이동하려는 위치(컬럼)을 선택한 시점

즉, 눌려진 마우스 버튼을 놓았을 때이다 (Mouse Up).

이상의 코드를 확인해 보면

전역으로 선언된 draggedDiv 변수에

mousedown, mouseup 함수에서

div를 생성하고 삭제하는 것을 확인 할 수 있다.


onload 이벤트에서 별도 창을 생성하고

mousedown, mouseup 함수에서

보이거나 숨기는 방식으로 구현 할 수도 있으니 직접 구현해 보길 바란다.


별도 창을 생성한 뒤,

이동할 컬럼의 내용(this.innerHTML)을

별도 창에 넣어주고(draggedDiv.innerHTML),

마우스의 움직임(Mouse Move)에 따라서

별도 창의 좌표(left, top)을 바꾸어 준다.


컬럼의 이동은 테이블의 헤드 컬럼으로 제한을 하기 때문에

모든 이벤트를 테이블 헤드에 대하여 적용하였다.

하지만, 별도 창은 마우스가

테이블 태그에 있거나 웹 페이지에 있으나

마우스를 누르고 움직일 때는 항상 보여야 하기 때문에

문서(document)의 Mouse Move 이벤트에 적용한다 (documentMouseMove).

Event (ev) 클래스에서 제공하는

pageX, pageY 속성을 통해서

현재 마우스 커서의 위치 값을 찾을 수 있다.


문서(document)의 Mouse Up 이벤트를 사용한 것은 (documentMouseup)

컬럼을 이동하기 위해서 마우스 버튼을 누른 상태에서

테이블 태그의 헤더가 아닌

다른 태그나 웹 페이지에서

마우스 버튼을 놓았을 경우를 처리하기 위한 것이다.

이 경우 기존의 mouseup()에서 작성한

드래그 종료(취소) 코드와 동일하게 작성한다.

이동하겠다고 한 컬럼에 적용된

CSS클래스(dragging)를 제거하고

이동 컬럼(dragTD) 변수에 Null을 지정하여

다른 이벤트(over, out)이 작동하지 않게 한다.


마지막 구현으로 별도 창에 컬럼의 헤드뿐 아니라

컬럼 전체의 내용이 나오게 구현해 본다.

하나의 컬럼에 10개의 행이 있다면 모두 나타나도록 구현한다.


구현 방법은 네 번째로 구현한 것과 동일하다.

다만, 컬럼의 헤드 내용을 별도 창에 넣어주었던 것을

테이블 태그를 생성하여

모든 행의 해당 컬럼을 복사해서 넣어준다.

function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
   
    draggedDiv = document.createElement("div")
    draggedDiv.className = "draggedDiv";
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
    document.body.appendChild(draggedDiv);
   
    var dragTable = document.createElement("table")
    draggedDiv.appendChild(dragTable);
    var srcInx = dragTD.cellIndex;
    var table = document.getElementById("tableOne");
    var rows = table.rows;
   
    for (var x=0; x<rows.length; x++) {
        var tr = rows[x].cloneNode(false);
        dragTable.appendChild(tr);
        var tds = rows[x].cells[srcInx].cloneNode(true);
        tr.appendChild(tds);
    }   
}

전체코드

먼저, 별도 창에 새로운 테이블 태그를

생성하여 추가한다.

그리고, 이동하기 위해 선택한 컬럼의 위치(cellIndex)를 파악하고(srcInx),

기존 테이블의 행 정보를 가지고 와서

해당 컬럼(srcInx)의 위치(cellIndex)에 있는

컬럼만 복제한다(cloneNode).


모든 행을 복제해야 하기 때문에

행의 개수(rows.length)만큼 반복하고,

각 행(TR - rows[x])을 먼저 복제한다.

새로운 테이블에 행이 없기 때문에

행을 먼저 만들어야 열을 추가할 수 있다.

행을 복제하고

행의 이동할 컬럼만(rows[x].cells[srcInx])만 복제해서 추가한다.


새로 생성하지 않고 복제를 하는 이유는

각 행과 열(컬럼)이 가지는 정보(내용, CSS등)들을

그대로 복제해야 실제 테이블에서 보이는 것과

별도 창에서 보이는 것이 같아 지기 때문이다.


복제에 사용한 cloneNode 메소드의 파라이터로

행을 복제할 때는 false를

열을 복제할 때는 true를 지정했다.

false는 자식 정보를 복제하지 않겠다는 것을 의미하는 것으로

행을 복제할 때 자식 정보를 복제하면

모든 열(컬럼)이 복제되기 때문이다.

행의 스타일만 복제하고

열은 지정된 열(srcInx)에 대한 모든 것을 복제한다.


JQuery 변환 방법에 대해서는

테이블 정렬(table Sorting)등에서 정리했기 때문에

별도로 정리하지 않는다.

다만, 앞서 두 번째 구현에서

정리한 addClass(), removeClass() 함수는 사용하지 않는다.

동일한 이름의 동일한 기능이 JQuery에서 제공되고 있다.

함수가 아니라 메소드로 사용하는 차이가 있다.

    AAA.addClass("dragging")
    AAA.removeClass("dragging")

전체코드



응용

이상의 MouseDown, MouseMove, MouseUp 등의

마우스 이벤트는 다양하게 활용될 수 있다.


조금 작은 예제로 팝업 윈도우를 구현 할 수 있다.

최근의 웹 개발에서는 팝업 윈도우를 DIV로 구현한다.

그림과 같이 팝업 윈도우의 헤드를

마우스를 누르고, 움직이고, 놓으면

해당 팝업 윈도우가 따라 움직이도록 구현한다.


전체 코드

좀 큰 예제는 파워포인트를 구현하는 것이다.

파워포인트의 슬라이드 위에서 

마우스를 누르고, 움직이고, 놓으면

움직인 만큼을 크기로 하는 도형이 생성된다.

다시 해당 도형을

마우스를 누르고, 움직이고, 놓으면

놓은 위치로 도형을 이동시켜 준다.

이러한 기능을 잘 구현한 예제가 다음 주소에 있다.

(SVG를 사용한 것으로 구현 방법에 차이가 있다.)

https://jgraph.github.io/mxgraph/javascript/


단계 1: 마우스 드레그 1 -  좌에서 우로

단계 2: 마우스 드레그 2 - 우에서 좌로 추가

단계 3: 도형 생성

단계 4: 도형 이동



테이블 태그의 데이터를 정렬(table sorting)하는 기능을 구현하면서

자바 스크립트의 기본 문법과 Jquery 사용법을 익힐 수 있도록 정리하였다.

기초 학습을 위해 정리하는 것으로

실제로 사용할 때에는

다양한 기능을 제공하는 오픈 소스를 사용하는 것이 좋다.


데이터를 정렬하는 기능은

그림과 같이 테이블 테그의 헤드(TD, TH)를

마우스로 클릭하면

해당 컬럼(필드)의 데이터를

오름차순이나 내림차순으로 정렬한다.

이해를 돕기 위해

다음과 같이 세부 단계로 나누어서

개발하고 정리한다.


1. 버튼을 누르면 오름차순으로 정렬

2. 정렬할 데이터가 없을 때 까지 반복

3. 정렬 버튼을 누르면 오름차순로 정렬하고, 다시 버튼을 누르면 내림차순으로 정렬, 다시 버튼을 누르면 오름차순로 반복.

4. 3번 예제 단순화

5. JQuery로 변환

6. 버튼 대신 각 테이블의 필드명(첫 row)을 클릭하면 해당 필드가 정렬

7. 필드별 정렬

8. 필드별 정렬 상태 표시

9. 라이브러리화

5단계 부터 9 단계는 나누어서 별도의 페이지에 정리하였다.

각 단계별로 간단한 구현 방법을 먼저 정리하였으니

코드를 보기 전 구현을 시도해 보고

코드를 보는 것이 실력 향상에 조금 더 도움이 될 것이다.


이러한 단계를 진행하기 전에

주어진 데이터를 동적으로 생성하는 것 부터 시작한다.


다음과 같이 주어진 2차원 배열의 데이터를 이용하여

HTML 테이블(table) 태그를 (그림참조)

자바 스크립트(JavaScript)만으로 생성한다.

var data = [
            ['name', 'Java', 'Node', 'Perl'],
            ['Gu', 80, 70, 30],
            ['Brad', 50, 90, 90],
            ['Lee', 70, 70, 70],
            ['Kim', 90, 60, 80]
           ];


먼저, 자바 스크립트에서 HTML 태그들을

동적으로 생성하는 방법을 알아야 한다.

동적으로 생성하는 명령어는 createElement로

함수 파라미터로 HTML 태그를 지정하면 해당 태그가 생성된다.

HTML 태그들은 계층구조로 구성되어 있기 때문에

appendChild나 insertBefore등으로 생성된 태그의 부모를 지정해야 한다.

부모를 지정하지 않으면 웹 페이지에 출력되지 않는다.

보다 상세한 설명은 w3school이나 MDN을 읽어보길 바란다.


주어진 2차원 배열의 데이터를

HTML 테이블(table) 태그로 생성하는 방법은

다음과 같다.

먼저, 테이블을 생성한 부모를 지정한다.

부모 태그는 body로 해도 되고

특정한 div(targetPn)로 해도 된다.

여기서는 HTML div 태그를 찾기 위해

ID가 targetPn인 div 태그를 getElementById를 이용하여 찾았다.


주어진 데이터는 2차원 배열이고

테이블은 행(TR)과 열(TD)로 구성되어 있다.

즉, 2차원 배열에서 각 차원이 행이 되고,

각 차원의 요소가 열이 된다.


따라서 각 차원의 개수만큼 반복해서 [라인 23]

테이블의 자식으로 행(TR)을 생성하고 [라인 24, 25]

각 차원의 요소만큼 반복해서 [라인 26]

행(TR)의 자식으로 열(TD)을 생성한다 [라인 27, 28].

생성 후 열의 값(innerHTML)으로 배열의 값을 지정한다 [라인 29].


웹브라우저의 개발자 도구(DOM 탐색기, Elements)에서

계층형으로 생성된 테이블 태그를 확인할 수 있다.


이제 본격적인 세부 구현을 진행한다.

위 실행 코드에

다음과 같이 버튼 태그를 추가하고

이 버튼을 클릭하면 데이터가 오름차순으로 정렬 되게 한다.

    <button type='button' onclick='sortTable()'>Sort</button>
    <div id='targetPn' style='width:130px'></div>

정렬하는 방법은 다음과 같다.

1. 테이블의 (행의 개수-1) 만큼 반복해서

2. 현재 행(row)과 다음 행(row+1)의 지정된 셀(td) 값을 비교

3. 현재 행의 셀이 다음 행의 셀 값보다 크면

4. 다음 행을 현재 행 앞으로 이동시킨다.



위의 문장은 각각 다음과 같이 해당 라인으로 구현된다.

1. 테이블의 (행의 개수-1) 만큼 반복해서 [라인 42, 43]

2. 현재 행(row)과 다음 행(row)의 지정된 셀(td) 값을 비교 [라인 44, 45]

3. 현재 행의 셀이 다음 행의 셀 값보다 크면 [라인 46]

4. 다음 행을 현재 행 앞으로 이동시킨다 [라인 47].


이상의 코드에서 특징적인 것 중 하나는

getElementsByTagName를 사용한 것이다 [라인 41].

getElementsByTagName는 HTML 태그로 개체를 찾는 명령어로

해당 웹 페이지에 있는 모든 태그를 찾아서 배열로 반환한다.

현재 웹 페이지에는 하나의 테이블만 있기 때문에

0 번째 테이블 태그를 사용하였다 [라인 42].

이외에도 getElementById, getElementsByName, getElementsByClassName등이 있다.


행의 개수 만큼 비교하는 반복 처리는

두번째 행(i=1) 부터

행의 개수(rows.length) 보다 1 작을 때까지 반복한다 [라인 43].

1부터 시작하는 것은

첫번째 필드(i=0)는 필드명으로 정렬하지 않는다.

행의 개수(rows.length)보다 1 적게 반복하는 것은

현재 행과 다음 행을 비교하는데

마지막 행은 다음 행이 없기 때문이다.

따라서, 전체적으로 행의 개수보다 2 적게 반복한다.


table[0].rows를 rows 변수로 지정해서 사용한 것은 [라인 42].

코드를 좀더 보기 좋고 속도(?)를 향상 시키기 위한 것이다.

하위 태그를 사용하기 위해

모든 계층 관계를 점(.)으로 나열하기 보다는

위와 같이 변수로 지정해서 사용하는 것이 좋다.


0 번째 셀(TD) 즉, 사람 이름으로 정렬한다.

제공된 데이터에서 사람 이름이 영문으로 제공되었기 때문에

대소문자 구분을 하지 않기 위해 소문자(toLowerCase)로

변환해서 비교하였다 [라인 46].


비교 결과 현재 행의 이름이 크면

다음 행을 현재 행 앞으로 이동(insertBefore) 시킨다 [라인 46].

insertBefore는 appendChild와 같이 부모를 지정하는 함수이다.

appendChild는 지정된 자식(Node)를

부모의 마지막 자식 뒤에 추가하는 것이고

insertBefore는 지정된 자식(형제) 앞에 삽입하는 함수이다.

         부모. appendChild ( 자식 )

         부모. insertBefore ( 자식, 특정 위치의 형제)

여기서는 현재 행의 부모를 찾아서 행의 이동을 처리했다 [라인 47].

현재 노드의 parentNode 속성으로

부모 노드를 찾을 수 있다.

행의 부모는 테이블 태그이기 때문에

      table[0].insertBefore(rows[i + 1], rows[i])

로 작성해도 된다.

다만, 여기서는 사용하지 않았지만 TBODY를 사용한 경우

이 코드는 오류를 발생시킨다.

다음과 같이 TBODY를 이용해서 작성해야 한다.

     table[0].tbody.insertBefore(rows[i + 1], rows[i])

이렇게 사용할 수도 있고 안 할 수도 있는 경우를 고려해서

parentNode를 이용하는 것이 좋다.


실행 후

버튼을 클릭해서 다음과 같이 정렬이 되는지 확인한다.

데이터를 다음과 같이 수정하여 실행하면

이름이 제대로 정렬되지 않은 것을 확인 할 수 있다.

var data = [
            ['name', 'Java', 'Node', 'Perl'],
            ['Gu', 80, 70, 30],
            ['Kim', 90, 60, 80],
            ['Lee', 70, 70, 70],
            ['Brad', 50, 90, 90]
           ];

4 번째에 있던 Brad가

1 번째가 아닌 3 번째에 있다.

이 버그를 해결하려면,

정렬 버튼을 여러 번 클릭해야 한다.


현재 행과 다음 행만 비교 하기 때문에 발생하는 문제로

여러 번 클릭하면 해결되는 것이 해법이다.

한번 클릭할 때 마다

현재 행과 다음 행을 비교해서 정렬하기 때문에

계속 버튼을 클릭하면

계속 비교를 하게 되어 제대로 정렬이 이루어 진다.

더 이상 정렬할 데이터가 없으면

클릭해도 변화가 없다(비교는 계속 이루어진다).

따라서, 제대로 정렬이 되려면

크기를 비교해서 위치를 바꾸어 주는 것을

정렬할 데이터가 없을 때 까지 반복하면 된다.

    var rows = table[0].rows;
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var fCell = rows[i].cells[0];
            var sCell = rows[i + 1].cells[0];
            if (fCell.innerHTML.toLowerCase() > sCell.innerHTML.toLowerCase()) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                chkSort = true;
            }
        }   
    }

전체 코드

체크 변수(chkSort)를 사용해서

변수의 값이 true일때만 반복 하도록 하였다 (while).

For 문은 데이터 개수(length) 등 반복 범위가 명확 할 때 사용하고

while문은 그렇지 않을 때 사용한다.

while문이 시작되면

체크 변수의 값을 false로 초기 값을 지정한다.

비교문에서 정렬 데이터가 발생하면

체크 변수의 값을 true로 바꾸어

정렬 데이터가 더 있을 수 있으니

처음부터 다시 확인하도록(반복하도록) 한다.


이상의 정렬은 거품 정렬(Bubble sort) 개념을 활용한 것인데

일반적으로 거품 정렬에서는 두번의 for문을 이용한다.

while문이 아니라 이 방법으로 구현해 보길 바란다.


이번에는 정렬 버튼을 누르면 오름차순로 정렬하고,

다시 버튼을 누르면 내림차순으로 정렬,

다시 버튼을 누르면 오름차순로 반복하는 기능을 구현한다.


현재 구현된 코드는 오름차순이다.

오름차순은 큰 값과 작은 값을 비교해서 큰 값을 뒤로 보낸다.

현재 행의 셀 값이 다음 행의 셀 값보다 크면

현재 행을 다음 행 뒤로 보낸다.

다만, 코드로 구현할 때는

다음 행을 현재 행 앞으로 이동 시켰다.


내림차순은 반대로

큰 값과 작은 값을 비교해서 작은 값을 뒤로 보낸다.

현재 행의 셀 값이 다음 행의 셀 값보다 작으면

현재 행을 다음 행 뒤로 보낸다.


버튼을 눌렀을 때,

현재 정렬 상태가 오름차순이면 내림차순으로

내림차순이면 오름차순으로 정렬되도록 한다.

var sortType = 'asc';
function sortTable(){
    var table = document.getElementsByTagName('table');
   
    if (sortType === 'asc') {
        sortType = 'desc';
        sortTableDesc(table[0].rows);
    } else {
        sortType = 'asc';
        sortTableAsc(table[0].rows);
    }
}
function sortTableAsc(rows){
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var fCell = rows[i].cells[0];
            var sCell = rows[i + 1].cells[0];
            if (fCell.innerHTML.toLowerCase() > sCell.innerHTML.toLowerCase()) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                chkSort = true;
            }
        }   
    }
}
function sortTableDesc(rows){
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var fCell = rows[i].cells[0];
            var sCell = rows[i + 1].cells[0];
            if (fCell.innerHTML.toLowerCase() < sCell.innerHTML.toLowerCase()) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                chkSort = true;
            }
        }   
    }
}

전체코드

하나의 버튼으로 오름차순과 내림차순을 구현하려면

전역 변수로 상태 변수(sortType)를 지정해서

변수의 값이 오름차순이면(sortType === 'asc') 내림차순(sortType = 'asc')으로

내림차순이면(sortType === ' desc' 여기에서는 else)

오름차순이(sortType = 'desc') 되도록 하면 된다.

전역 변수(함수 밖)로 선언해야 하는 이유는

함수 내에 선언되어 있으면 지정된 값이 초기화 되기 때문이다.


그리고 각각의 상태에 따라

오름 차순 함수(sortTableAsc)나

내림 차순 함수(sortTableDesc)를 호출한다.


이 두 함수를 자세히 보면

비교연산자(<>)만 다르고 모든 코드가 동일하다.

오름차순은 현재 행의 값이 다음 행의 값 보다 크면(>)

다음 행을 현재 행 앞으로 이동시키고

내림차순은 현재 행의 값이 다음 행의 값 보다 작으면(<)

다음 행을 현재 행 앞으로 이동시킨다.


이상의 코드는

중복 코드도 많고 코드 양도 너무 많다.

이번에는 조금 줄여서 작성해 본다.


비슷한 코드로 구성된 두 함수를 하나로 작성한다.

두 함수의 차이는 비교 연산자뿐이다.

var sortType = 'asc';
function sortTable(){
    sortType = (sortType === 'asc') ? 'desc':'asc';

    var table = document.getElementsByTagName('table');
    var rows = table[0].rows;
    var chkSort = true;
   
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var row = rows[i];
            var fCell = row.cells[0].innerHTML.toLowerCase();
            var sCell = row.nextSibling.cells[0].innerHTML.toLowerCase();
            if ( (sortType === 'asc'  && fCell > sCell) ||
               (sortType === 'desc' && fCell < sCell) ) {
                row.parentNode.insertBefore(row.nextSibling, row);
                chkSort = true;
            }
        }   
    }
}

전체코드

두 개의 정렬 함수를 사용하던 코드를

오름차순만 구현한

이전 코드에 간단한 추가만으로 작성하였다.

먼저, 상태에 따라 변수와 함수를 호출해야 해서

IF문을 사용했지만

함수를 호출하지 않으면 하나의 라인이 되기 때문에

? 문으로 작성할 수 있다. (sortType === 'asc') ? 'desc':'asc';


상태에 따라 정렬하는 것도 코드에서 보이는 것처럼 매우 간단하다.

하나의 IF문에

오름차순이면 > 비교연산자가

내림차순이면 < 비교연산자가 적용되도록하면 된다.


이 예제는 조금 중요한 개념(?)을 설명한다.

실제 개발에서 앞서의 방식으로 작성하는 경우가 아주 많다.

이 경우 실행되기 때문에 개발이 완료 되었다고 할 수 있지만

수정 등의 유지 보수에 많은 시간을 소비할 수 있다.

여유로운 개발을 위해서 뒤의 방식으로 처리하는 습관을 들여야 한다.


마지막으로,

다음 행을 row-1로 찾았던 것을

nextSibling라는 메소드로 작성하였다.

nextSibling은 현재 노드(나)의

다음 번 형제를 찾아주는 기능을 한다.

반대 기능을 하는 previousSibling이 있으며,

이외에도 children등 자식 (노드)관련 메소드가 있다.



참고

거품 정렬에 대한 이해가 부족하다면

https://visualgo.net/en/sorting 사이트를 참고하면 된다.

다양한 정렬 알고리즘을 시각적으로 보여줘서

조금 더 쉽게 이해할 수 있게 해준다.


사이트에 접속하면

다음과 같은 화면이 실행된다.


매우 많은 기능이 제공되니 읽어보길 바라고

[X Esc]를 클릭한다.

페이지 상단에 제공되는 여러 정렬 알고리즘이 나열되어 있다.

이중 가장 앞에 있는 거품정렬(bubble sort)을 선택하고,

좌측 하단의 [Sort]를 클릭하면 나타나는

Go 메뉴를 선택하면

정렬 알고리즘이 진행되는 과정을 볼 수 있다.




앞서 작성한 테이블 데이터 정렬(table sorting) 기능을

JQuery로 변환한 후,

다음과 같은 세부 기능을 구현한다.


1. 버튼을 누르면 오름차순으로 정렬

2. 정렬할 데이터가 없을 때 까지 반복

3. 정렬 버튼을 누르면 오름차순로 정렬하고, 다시 버튼을 누르면 내림차순으로 정렬, 다시 버튼을 누르면 오름차순로 반복.

4. 3번 예제 단순화

5. JQuery로 변환

6. 버튼 대신 각 테이블의 필드명(첫 row)을 클릭하면 해당 필드가 정렬

7. 필드별 정렬

8. 필드별 정렬 상태 표시

9. 라이브러리화


자바 스크립트로 작성된 코드를

JQuery로 바꾸기 위해

7라인과 같이 JQuery를 사용할 수 있도록 한다.

페이지 로딩이 완료된 후 발생하는 이벤트는

window.onload인데 JQuery에서는

$( document ).ready등으로 구현한다.


자바 스크립트에서는 createElement로 생성하는 것을

JQuery 기호에 생성한 HTML 태그만

파라미터로 지정하면 된다 [라인 22].

appendChild 대신에 append를 사용한다 [라인 23].

비슷한 구조로 되어 있는 것 같은데

테이블(table) 테그를 생성한 후

속성 지정을 메소드 체인(Method Chaining)으로 작성하였다.

자바 스크립트에서는

개별 속성을 하나씩 직접 지정하거나

setAttribute로 하나씩 호출해야 하는데

JQuery에서는 쭉이어서 작성할 수 있다 [라인 22].


For문 대신에 each문을 사용하여

2차원 배열 데이터의 반복 처리를 구현하였다 [라인 25, 28].


테이블의 각 셀에 값을 지정하는 것은

innerHTML 속성 대신에

JQuery의 html() 함수를 사용한다 [라인 30].


다음으로, 중요한 개념 중 하나인 선택자(selector)를 사용하였다.

앞서 작성한 예제는

행 정보에 접근하기 위해

getElementsByTagName 테이블을 찾고

table[0].rows로 테이블에 대한 행의 정보를 찾았다.


이번에는 선택자(selector)를 이용하여

$('table > tr')와 같이 간단하게 작성하였다 [라인 43].

getElementsByTagNam과 같이 테이블(table) 태그를 찾고

그 테이블 태그 바로 아래(>)에 있는 행(tr)을 반환한다.

여기서는 사용하지 않았지만

만약, tbody가 사용되었다면

이 코드에서는 반환 값이 없다.

$('table > tbody > tr')나 $('table tr')로 사용해야 한다.

자식(>) 기호는 바로 직계 자식을 의미하고

기호가 없으면 모든 자식(자식, 손주, 증손주 등)을 의미한다.

JQuery에서

HTML 태그는 별도의 표시 없이 사용했지만 (getElementsByTagName)

CSS 클래스 이름으로 찾을 때는 점(.)을 (getElementsByClassName)

ID로 찾을 때는 # 을(getElementById)

이름으로 찾을 때는 name=XXX (getElementsByName)

로 작성하면 된다.

보다 자세한 내용은 찾아보길 바라고

자바 스크립트에서는 document.querySelector로

선택자를 사용할 수 있다.


JQuery 선택자는 42 라인에 주석으로 작성 한 것과 같이

Find 메소드로도 구현할 수 있다.


이렇게 반환된 행 정보를

for문 대신 each문으로 작성하여

현재 행과 다음 행의 셀 값 비교를 처리 하였다.

현재 행은 파라미터(row)로 전달되고

다음 행은 현재 행의 다음 형제(nextSibling)로 찾는다 [라인 45, 46].

행에 대한 정보는

each문에서 row로 자바 스크립트 개체가 전달 되었기 때문에

49 라인에서 작성한 행 이동 처리를 (insertBefore)

자바 스크립트로 처리해도 되지만

사용법을 익히기 위해 JQuery로 작성하였다.

자바 스크립트 개체를 JQuery 기호 ($)로 지정하여

JQuery 개체로 변환하여 사용한다.

JQuery 개체로 사용한다는 것은

JQuery 명령어 등을 사용할 수 있다는 것을 의미한다.

앞서 정리한 선택자로 반환한 개체들도

JQuery로 지정한 것들은 JQuery 개체로 반환된다.


이상의 코드에서

자바 스크립트나 JQuery나 큰 차이는 없어 보이지만

JQuery가 좀더 단순하게 작성할 수 있는 장점이 있다.

특히, 메소드 체인은 JQuery의 장점 중 하나이다.

다만, 선택자는 강력하고 편리하지만

주의 해서 사용해야 한다.

여기에서는 테이블 태그가 하나이기 때문에 문제가 없지만

여러개가 있는데 $('table > tr')로 사용하면

문제가 생기게 된다.


이번에는

버튼 대신 각 테이블의 필드명(첫 row)을 클릭하면

해당 필드가 정렬되도록 작성한다.


첫 행을 클릭하면 해당 필드가 정렬되도록

첫 행을 생성할 때

각 필드에 클릭 이벤트를 연결한다(Binding).

해당 이벤트에서는 클릭된 필드가 반환되고

클릭된 필드의 위치(index)을 정렬할 때 참고 하면 된다.

$(function() {
    var table = $('<table>').attr('border', 1).attr('width', 200);
    $('#targetPn').append( table );
   
    $.each(data, function(inx, row ) {
        var tr = $('<tr>');
        table.append(tr);
        $.each(row, function(inx, col ) {
            var td = $('<td>');
            tr.append(td.html(col));
        });
        if (inx===0){
            tr.find('td').click(function() {
                sortTable(this.cellIndex)
            });
        }
    });
}

var sortType = 'asc';
function sortTable(cellIndex){
    sortType = (sortType === 'asc') ? 'desc':'asc';

    var chkSort = true;
    while (chkSort){
        chkSort = false;
        $('table > tr').each(function(inx, row) {
            if (inx===0 || !row.nextSibling) return;
            var fCell = row.cells[cellIndex].innerHTML.toLowerCase();
            var sCell = row.nextSibling.cells[cellIndex].innerHTML.toLowerCase();
            if ( (sortType === 'asc'  && fCell > sCell) ||
               (sortType === 'desc' && fCell < sCell) ) {
                $( row.nextSibling ).insertBefore( $(row) );
                chkSort = true;
            }
        });
    }
}

전체 코드


반복문의 색인 변수(inx) 값이 0 이면 (inx===0)

데이터의 첫 번째라는 의미로

첫 번째 행이 된다.

첫 번째 행이 생성되고 나면

해당 행의 td를 모두 찾아서(find)

마우스 클릭(click) 이벤트를 지정하였다.


이 코드는 다음과 같이

별도의 함수로 작성하고 (mouseClick),

이 함수를 연결(on, bind)해도 된다.

코드가 간략해서 앞서의 코드처럼 많이 사용한다.

function mouseClick() {
    sortTable(this.cellIndex)
};

~~ 생략 ~~

        if (inx===0){
            tr.find('td').on('click', mouseClick);
        }


자바 스크립트 개체인 this를 이용하여

현재 이벤트를 발생시킨 개체를 알 수 있다.

이벤트를 발생시킨 개체는

event.target로도 알 수 있고

this로도 알 수 있다.

현재 클릭 이벤트는 td에 지정되어 있기 때문에

this는 클릭된 td를 의미한다.


정렬에서 필요한 것은 클릭된 td의 위치이기 때문에

td의 cellIndex 속성으로 행 내에서의 위치 값을 찾아서,

정렬에서 값을 비교하는 부분에

첫 번째 필드(cellIndex=0)로 고정되어 있는 것을

cellIndex 값으로 수정하면 된다.

실행 후 각 필드를 클릭해 보면

정렬이 되는 것을 알 수 있다.

다만, 이름(Name) 필드를 오름차순으로 정렬하고

Java 필드를 클릭하면,

Java 필드의 내용이 내림차순으로 정렬된다.

다시 이름 필드를 클릭하면

이름 필드의 내용이 오름차순으로 정렬된다.

이름 필드는 이전이 정렬 상태가 오름차순이었으니

이름 필드는 내림차순으로 정렬되어야 한다.

이것은 정렬 상태를 저장하는 변수를 하나만 사용했기 때문이다.

각 필드 별로 정렬 상태를 저장하고

필드를 한번 클릭하면 오름차순,

두 번째는 내림차순이 되게 구현한다.


이것을 구현하는 방법은 정렬 상태(sortType)를

필드 개수 만큼 사용하면 된다.

    $.each(data, function(inx, row ) {
        var tr = $('<tr>');
        table.append(tr);
        $.each(row, function(inx, col ) {
            var td = $('<td>');
            tr.append(td.html(col));
        });
    });
   
    $(table).find('tr:nth-child(1)').find('td').click(function() {
        sortTable(this);
    });
});

function sortTable(cell){
    var sortType = jQuery.data( cell, 'sortType');
    sortType = (sortType === 'asc') ? 'desc':'asc';
    jQuery.data( cell, 'sortType', sortType);

    var cellIndex = cell.cellIndex;
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        $('table > tr').each(function(inx, row) {
            ~~ 생략 ~~
        });
    }
}

전체 코드

정렬 상태(sortType)를 필드 개수만큼 사용하는 것은

필드 개수만큼 1차원 배열을 사용하는 것이 가장 쉽다.

이 방식은 자바 스크립트의 특징을 제대로 활용하지 못한 것으로

여기서는 필드(TD)의 속성(attribute)을 이용하였다.

HTML 태그의 속성을 이용하는 방법은 많지만

JQuery에서 제공되는 data 함수를 이용하였다.

jQuery.data()나 $.data() 로 사용하면 된다.


다만, sortTable() 함수를 호출할 때

파라미터로 클릭된 필드의 위치(cellIndex)를 지정했지만

정렬 상태를 저장하고 불러오는 기능을 구현하기 위해

클릭된 필드(this) 자체를 파라미터(cell)로 사용하였다.


앞서의 예에서는

첫 행(컬럼명)에 클릭 이벤트를 적용하기 위해

행을 생성할 때 0 번 째 행이면

모든 필드에 클릭 이벤트를 적용하였다.

이번에는 모든 행을 생성하고 난 뒤,

테이블(table) 태그의 첫 번째 행(tr:nth-child(1))을 찾아서

모든 필드에 클릭 이벤트를 적용하였다.

       $(table).find('tr:nth-child(1)').find('td').click

가급적 가독성, 속도 등을 위해 IF문을 적게 사용하는 것이 좋고

자바 스크립트의 선택자를

잘 활용하는 것이 쉬운 프로그래밍을 할 수 있는 지름길이다.


각 필드별로 정렬이 되지만

현재 어떤 필드가 정렬되었고

오름차순(▲)으로 정렬될 것인지 내림차순(▼)으로 정렬된 것인지 알 수 없다.

필드별로 정렬 상태를 표시하도록 구현한다.


현재 진행할 정렬이

오름차순인지, 내림차순인지에 따라

클릭된 필드에 기호를 넣어주면 된다.

다만, 기호를 넣기만 하면

필드에 기호가 잔뜩 쌓이기 때문에지우는 기능이 필요하다.

function sortTable(cell){
    $('table > tr:nth-child(1)').find('td').each(function(inx, td) {
        td.innerHTML = td.innerHTML.replace(/[▼▲]/g, '') ;
    });
    var sortType = jQuery.data( cell, 'sortType');
    if (sortType === 'asc') {
        sortType = 'desc';
        cell.innerHTML += '▼';
    } else{
        sortType = 'asc';
        cell.innerHTML += '▲';
    }
    jQuery.data( cell, 'sortType', sortType);
    var cellIndex = cell.cellIndex;

전체 코드

필드별로 정렬 상태를 표시하기 위해

필드를 클릭하면 정렬 상태에 따라

기호(▼▲)를 넣어 주면 된다.


다만, 클릭된 셀(td)이

JQuery 개체가 아닌

자바 스크립트 개체이기 때문에

innerHTML로 필드명 뒤에 기호를 붙여 주었다.


현재 어떤 필드가

오름차순이나 내림차순으로 정렬했는지 만 보여주기 위해

다른 필드에 남아있는 기호는 모두 지워준다. replace(/[▼▲]/g, '')

모든 필드에 있는 기호를 지우고

현재 클릭된 필드에 추가하는 방식으로 구현하고

모든 필드는

앞서 클릭 이벤트에서 처리한 선택자로 구현하였다.


정렬 상태에 따라 처리하는 내용이

한 행이 아니라서 ? 대신에 IF문으로 처리한다.


현재 코드를 다른 웹 페이지에 사용하기 위해서는

관련 코드를 복사해서

해당 페이지에서 붙여 넣기를 해야 한다.

이렇게 사용하는 것보다

라이브러리화해서 데이터만 바꾸어

호출해서 사용하는 것이 편리하다.

마지막으로 간단하게 라이브러리로 만들어 본다.


라이브러리화는 복잡하게 작성하면

자바 스크립트의 클래스를 알아야 한다.

여기서는 관련 코드를 외부 파일로 저장하고

함수를 호출하듯이 작성한다.

<script src='tableSort9.js'></script>
<script>
var data = [
            ['name', 'Java', 'Node', 'Perl'],
            ['Gu', 80, 70, 30],
            ['Kim', 90, 60, 80],
            ['Lee', 70, 70, 70],
            ['Brad', 50, 90, 90]
           ];

$(function() {
    tableSoter9('targetPn', data);
});
</script>

 tablesort9.html

function tableSoter9(target, data){
    var table = $('<table>').css({'border': '1px solid', 'width': '300px'});
    $('#'+target).append( table );
~~ 생략 ~~

tableSort9.js

데이터를 제외하고

기존에 작성된 자바 스크립트 코드만 tableSort9.js 파일로 작성한다.

load 이벤트 대신 tableSoter9()라는 함수로 바꾸어 준다.

기존의 HTML파일에 있는 load 이벤트에서 

tableSoter9.js 파일에 있는

tableSoter9() 함수를 호출하는 방식으로 구현한다.

이 함수의 파라미터로

테이블 태그를 생성할 부모 태그(targetPn)와

테이블 태그 생성시 이용한 데이터(data)를 지정한다.


HTML파일에서 tableSoter9() 함수를 호출할 수 있도록

tableSort9.js 파일을 <script> 태그로 포함시켜 준다.


어떠한 파일에서도

tableSort9.js 파일을 <script> 태그로 포함시킨 후

tableSoter9() 함수만 호출해서 사용할 수 있다.



지금까지 테이블 태그의 데이터를 정렬(table sorting)을 구현하면서

자바 스크립트의 기본 문법과

Jquery 사용법을 익힐 수 있도록 정리하였다.

정리한 내용은 모두 기초적인 것으로

잘 익혀두면 유용하게 사용할 수 있을 것이다.


실제 라이브러리로 사용하기 위해서,

다음의 남아 있는 기능을 보완하면 좋다.

실력 향상을 위해 직접 구현해 보면 좋을 것 같아서

여기에 정리하지 않았다.


남아 있는 기능

1. 현재 작성된 코드는 사용하는 웹 페이지에 하나의 테이블 태그가 사용되었다는 전제로 작성되었다.

실제 사용에서는 수많은 테이블 테그가 사용되기 때문에 문제가 발생한다.

2. 사용된 성적은 60, 70과 같은 두자리 숫자를 사용했다.

6, 7 같은 한 자리 숫자나 600, 700 같은 세 자리 숫자 등을 이용하면 그림과 같이 정렬이 제대로 이루어 지지 않는다.






'JavaScript > 기타' 카테고리의 다른 글

3. 기초 - 테이블 컬럼 이동 (Drag and Drop Table Columns)  (2) 2017.06.29
1. 기초 - 테이블 정렬 (table sorting)  (1) 2017.06.07
CD9 - Chart Design Tool for C3  (0) 2017.03.26
2. gu-upload  (4) 2016.06.18
1.guupload - 예제 설치  (0) 2016.06.18

CD9은 C3.JS (D3-based library) 챠트를 손쉽게 이용할 수 있게 하는 도구이다.

소스는 Github에서 받을 수 있고,

데모는 여기에서 확인할 수 있다.

C3는 D3 라이브러리를 이용하여 만든 챠트 라이브러리 이고,

D3 (Data-Driven Documents)는 웹에서 데이터를 시각적으로 쉽게 조작할 수 있도록 만든 라이브러리이다.

C3는 제법 괜찮은 챠트를 제공하지만 조금 사용하기 어려움이 있어서 쉽게 사용할 수 있는 도구를 제작하였다.

CD9은 챠트 설정, 데이터 입력, 미리 보기 부분으로 구성되어 있다.


사용자가 직접 데이터를 입력하고

원하는 상세한 차트 설정을 지정하여 즉시 확인 할 수 있다.

확인된 설정은 작성 기능을 이용하여

C3JS 챠트 사용에 필요한 자바 스크립트 코드와 CSS 설정을 생성 할 수 있다.

크롬과 firefox에서는 잘 작동되지만

IE에서는 HTML5의 숫자와 색 입력이 제대로 작동되지 않는다.


2019 년에 기능을 보강하면서 문서를 새로 작성하였습니다.




gu-upload는 HTML5 기반으로 파일을 업로드하는 JavaScript 라이브러리로

IE10 이상이나 Firefox, Chrome등 에서 작동한다.

IE9 이하에서는 SWFUpload를 이용하였다.

js 폴더에 있는 guupload 폴더를 복사해서 사용하거나

github에서 다운받아 사용하고자 하는 폴더에 복사한다.


개발하는 자신의 웹 페이지의 Head에

다음과 같이 CSS와 JavaScript를 포함한다.

<link rel="stylesheet" type="text/css" href="js/guupload/css/guupload.css"/>

<script type="text/javascript" src="js/guupload/guUploadManager.js"></script>

guupload를 복사할 경로를 설정해 줘야 한다.

예제에서는 js 폴더 하위에 있기 때문에

위 코드와 같이 각 경로가 js로 시작하고 있다.


웹 페이지 실행이 완료되면(window.onload)

guUploadManager를 생성하도록(new) 한다.

guUploadManager는 화면 구성과

웹 브라우저에 따라 guuplaod, swfupload를 실행하도록 하는 역할 등을 한다.

<script type="text/javascript">
var guManager=null;

window.onload = function() {
    var option = {
            listtype: "thumbnail",
            fileid: "guupload",
            uploadURL: "upload",
            form: document.form1
    }
    guManager = new guUploadManager(option);
}   

function formSubmit(){
    guManager.uploadFiles();
}
</script>

~~ 생략 ~~

        <tr>
            <td>Attach File</td>
            <td>
                <div id="guupload" class="guupload" style="width: 500px; height: 120px;">
                </div>
            </td>
        </tr>

생성시 파라미터는 listtype, fileid, uploadURL, form 4가지가 JSON 형태로 제공되어야 한다.

먼저 listtype은 파일 리스트 방식을 선택하는 것이다.

thumbnail로 지정하면 이미지 파일에 대하여 미리보기를 사용할 수 있다.

값을 지정하지 않으면 파일명과 파일크기가 리스트 형태로 출력된다.

fileid는 DIV id로 파일 업로드 라이브러리를 그리는 div를 의미하며,

지정된 DIV에 사용자가 보는 화면을 구성하게 된다.

uploadURL은 파일을 업로드 하면

서버에서 전송 받아 실제 파일로 저장하는 URL, 즉 컨트롤을 의미한다.

form은 제목, 내용, 파일 등 게시글과 관련된 항목을 가지고 있는 form 태그를 의미한다.

form은 사용자가 커밋을 하게 되면 formSubmit함수를 호출하여

파일을 먼저 서버에 저장하고,

파일 전송이 완료되면 guupload가 대신 submit을 하기 위한 것이다.


guuplaod는 파일명(filename), 실제 파일명(realname), 파일크기(filesize)의 세가지 정보를 제공한다.

실제 파일명(realname)는 파일을 서버에 저장하고 반환 받은 파일명을 의미한다.

이 값들은 다수의 파일을 전송 하기 때문에 콤마(,)로 구분하여 전송된다.

따라서 다음 코드와 같이 split로 구별하여 처리한다.

<%
    String filename = request.getParameter("filename");
    String realname = request.getParameter("realname");
    String filesize = request.getParameter("filesize");
    String[] reallist = realname.split(",");
    String[] filelist = filename.split(",");
    String[] sizelist = filesize.split(",");

    for (int i=0; i<filelist.length; i++) {
        out.println(filelist[i] + " : " + reallist[i] + " : " + sizelist[i] + "<br/>");
    }               
%>

upload_save.jsp


제공되는 디자인이 맘에 들지 않고, CSS를 알고 있다면

guupload.css 파일에서 수정하여 사용하면 된다.

대부분의 디자인을 수정할 수 있는 CSS 클래스를 포함하고 있다.

+ Recent posts