이미지 직접링크 막기

로키 (Homepage) 2007-05-11 03:47:44, Hit : 1,501, Rec. : 213
태터 팁이라기보다는 전반적인 웹사이트 운영 팁에 가깝습니다만.. 타 사이트에서 이미지를 직접 가져가서 전송량 누수가 일어나는 현상을 막기 위한 팁입니다. 다음과 같은 .htaccess 파일을 루트에 올리기만 하면 됩니다.

(꽤 널리 퍼진 코드라 출처를 찾는데 좀 애먹었지만, 이곳이 원 출처인듯 하군요.

http://www.thesitewizard.com/archive/bandwidththeft.shtml )

SetEnvIfNoCase Referer "^http://www.mydomain.com/" locally_linked=1
SetEnvIfNoCase Referer "^http://www.mydomain.com$" locally_linked=1
SetEnvIfNoCase Referer "^http://mydomain.com/" locally_linked=1
SetEnvIfNoCase Referer "^http://mydomain.com$" locally_linked=1
#SetEnvIfNoCase Referer "^$" locally_linked=1
<FilesMatch ".(gif|png|mp3|bmp|jpg|jpeg)$">
  Order Allow,Deny
  Allow from env=locally_linked
</FilesMatch>

이렇게 하면 루트 이하 모든 폴더에 있는 .gif, .png, .bmp, .jpg, .jpeg, .mp3 파일에 대해 자신의 계정 (mydomain.com) 외에서 보내는 요청이 거부됩니다. 커멘트 처리한 다섯번째 줄은 웹사이트가 아닌 하드 드라이브에서 파일을 요청할 경우에는 허용하라는 얘기 같은데, 별 필요를 못 느껴서 커멘트했지만 필요하다고 생각하시면 샵 표시를 지우시면 됩니다. (07/06/05 수정: FilesMatch에 mp3를 포함한채 다섯 번째 줄을 커멘트하면 쥬크박스가 안되는군요. 주의하시길.)

자기 사이트 외에서 글을 작성할 때는 이미지 호스팅이 필요한 경우도 있긴 한데, 그럴 경우 위 .htaccess 파일을 옮겨서 이미지 직접 링크를 허용하는 폴더와 금지하는 폴더를 구분할 수도 있습니다만.. 개인적으로는 그냥 루트에 넣어서 모든 폴더에 금지를 걸고 따로 무료 이미지 호스팅을 사용하는 편을 선호합니다.
siwanamu : 2007-05-1108:16:56 : X
Good^^;; 감사~~
로키 : 2007-05-1123:39:01 :  
잘 쓰시길. ^^ 몇가지 주의하실 점이라면..

이 방식으로는 <img> 태그로 이미지를 요청하는 것 뿐만 아니라 <a href="..">로 외부 사이트에서 이미지 파일에 링크를 거는 것도 금지됩니다. 개인적으로는 그 역시 괜찮은 기능이라고 생각하기 때문에 이 방식을 사용하고 있지만요.

두번째로는 구글 이미지 검색에 지장이 있을 수 있습니다. 저는 이 점도 큰 상관은 없지만, 필요하시다면 위의 예외 헤더에 mydomain.com 외에도 구글 같은 검색엔진 헤더 역시 추가하실 수 있을 것입니다.
zawoo : 2007-05-2913:41:59 : X
위 소스를 루트에?? 어떤 파일형식으로 저장해서 올리면 되나요?
로키 : 2007-05-2915:03:13 :  
.htaccess 설정은 하위 폴더에 영향을 미치기 때문에 모든 폴더에 대해 이미지 불펌을 금지하려면 루트에 넣으시면 됩니다. 그렇지 않고 예를 들어 /img 폴더에만 그런 금지를 걸고 싶다면 /img 폴더에다가 넣으시면 되겠죠. 그리고 이미 원하는 위치에 .htaccess 파일이 있다면 (FTP 프로그램에서는 '숨은 속성 파일 보기' 같은 걸 선택해야 보일 수도 있습니다) 거기에 소스를 추가하시면 됩니다.

파일 형식은 파일명 자체가 .htaccess 입니다. 저게 확장명이 아니라 파일 이름 자체입니다. 유닉스 계열 서버에서 사용하는데, 만약 윈도우 같은 쪽이라면 어떨지 모르겠군요.
Posted by [czar]
,
http://okjsp.pe.kr/bbs?act=VIEW&seq=20524&bbs=bbs4&keyfield=subject&keyword=접속자&pg=0 


* sessionChecker.jsp

<%@ page contentType="text/html; charset=euc-kr" %>
<jsp:useBean id="sc" class="SessionChecker" scope="application" />
<%
    session.setMaxInactiveInterval(60); // 걍 결과가 빨리 보고싶어서여.. 60초
    sc.setSession(session);
 out.println("현재 접속자 수 : " + sc.getNowUser());
%>

* SessionChecker.java

import java.io.*;
import java.util.*;
import javax.servlet.http.*;

public class SessionChecker{
 public static int nowUser = 0;

    public void setSession(HttpSession session){
 // 리스너 객체를 생성해서 이것도 세션에 같이 담는다.  리스너 라는 이름으로...
        session.setAttribute("listener", new CustomBindingListener());
    }

 public static void setNowUser(int x){
  nowUser += x;
 }

 public int getNowUser(){
  return this.nowUser;
 }
}


//                                                     여기서    구현했습니다..

class CustomBindingListener implements HttpSessionBindingListener {
    public void valueBound(HttpSessionBindingEvent event) {
 // 세션이 생겼을 할 내용
  SessionChecker.setNowUser(1);
  System.out.println("BOUND as " + event.getName() + " to " + event.getSession().getId());
    }

    public void valueUnbound(HttpSessionBindingEvent event) {
 // 세션이 종료되었을때
   SessionChecker.setNowUser(-1);
        System.out.println("UNBOUND as " + event.getName() + " to " + event.getSession().getId());
    }
}

Posted by [czar]
,
틀 자바스크립트를 이용한 HTML 폼 검증 코드 작성

HTML 폼에 원하는 값을 입력하지 않을 경우 입력 오류를 경고창(alert)으로 알려주는 것은 이제 기본이 되었다. 예를 들어, 아래와 같은 HTML 폼 코드가 있다고 하자.

    <form name="f" ...>
        아이디: <input type="text" name="id">
        암호: <input type="password" name="password">
        <input type="submit" value="로그인">
    </form>

위 코드는 "id"와 "passowrd" 입력 요소를 갖는데, 이 둘이 모두 필수 입력요소라고 할 경우 이 폼을 검사하는 자바 스크립트 코드는 아래와 비슷한 형태를 취하게 될 것이다.

    <script type="text/JavaScript">
    function checkForm() {
        form = document.f;
        if (form.id.value == '') {
            alert("아이디를 입력하세요");
            form.id.focus();
            return;
        }
        if (form.password.value == '') {
            alert("암호를 입력하세요");
            form.password.focus();
            return;
        }
        form.submit();
    }
    </script>

검사해야 할 요소가 두개인 경우에는 위와 같이 비교적 간단(?)하게 폼 검증 자바 스크립트 코드를 작성할 수 있다. 하지만, 아이디에 영문자와 숫자만 입력할 수 있고, 암호의 길이는 3~15 바이트 이내여야 한다면 위 코드는 좀더 복잡해질 것이다. 입력해야 하는 요소가 상대적으로 더 늘어난다면 위 코드는 더욱 복잡해질 것이다.

이러한 폼 검증 코드의 복잡함을 없애기 위해서 필자는 틀 자바스크립트(TLEJavaScript)를 만들게 되었으며, 쉽고 빠르게 폼 검증 코드를 작성할 수 있도록 하는 것이 틀 자바스크립트의 목적이다.

틀 자바스크립트를 사용하기 위한 준비

틀 자바스크립트 라이브러리는 틀 프로젝트 사이트에서 다운로드 받을 수 있으며, 이 글을 쓰는 시점에서 1.0.0 버전을 출시한 상태이다. TLEJavaScript-1.0.0.zip 파일을 다운로드 받은 뒤 압축을 풀면 아래와 같이 두 파일이 존재하는 것을 확인할 수 있다.

  • TLE.js - 틀 자바스크립트 라이브러리. 폼 검증을 위한 자바스크립트 코드가 저장
  • test/test.html - 틀 자바스크립트 라이브러리의 사용 예제 코드

test.html을 실행하면 다음과 같은 화면을 볼 수 있다.


그림1 - test.html 실행 화면

위 화면에서 [체크] 버튼을 눌러보면 값이 검증되는 결과를 확인할 수 있을 것이다. test.html은 TLE.js를 사용해서 폼을 검증한다. test.html코드를 보면 기존의 자바 스크립트 코드와 많이 다른 것을 확인할 수 있을 것이다. test.html에서 폼 검증과 관련된 코드 중 일부는 아래와 같다.

    <script language="JavaScript">
    var checker = new FormChecker(document.f);
    
    checker.checkRequired('id', '아이디를 입력하세요', true);
    checker.checkAlphaNum('id', '아이디는 알파벳과 숫자만 입력하세요', true);
    checker.checkMinLength('id', 2, '아이디는 최소 두글자 입력하세요', true);
    ...
    
    </script>
    
    function checkForm() {
        if (checker.validate()) {
            alert("값 검증 통과");
        }
    }
    

위 코드를 보면 이 글의 서두에서 살펴봤던 if-else가 사용된 자바스크립트 코드에 비해 훨씬 알아보기 쉬운 것을 알 수 있다. 폼 검증의 핵심은 틀 자바스크립트가 제공하는 FormChecker 객체인데, 이 객체의 사용방법에 대해서 살펴보도록 하자.

틀 자바스크립트의 FormChecker 객체를 사용한 폼 검증

틀 자바스크립트는 폼 검증을 위한 FormChecker 객체를 제공하는데, 이 객체를 사용하는 방법을 실제 코드를 통해서 살펴보도록 하자.

    <form name="f" onSubmit="return checkForm()">
        아이디: <input type="text" name="id">
        암호: <input type="password" name="password">
        <input type="sumit" value="로그인">
    </form>
    
    <script type="language/JavaScript">
    // 1. FormChecker 객체를 생성할 때 검증할 폼 객체(document.f)를 전달한다.
    var checker = new FormChecker(document.f);
    
    // 2. 검사 조건을 추가한다.
    checker.checkRequired('id', '아이디 입력하세요', true);
    checker.checkMinLength('id', 3, '아이디는 3글자 이상 입력하세요', true);
    checker.checkRequired('password', '암호를 입력하세요', true);
    
    function checkForm() {
        // 3. 폼 값을 전송하기 전에 폼을 검증한다.
        return checker.validate();
    }
    
    </script>
    

FormChecker는 검사 조건을 추가할 수 있는 함수를 제공하는데, 이들 함수는 checkRequired 와 같이 check로 시작한다. checkRequired() 함수의 경우는 입력 요소에 값을 입력했는지의 여부를 검사한다. 예를 들어, 위 코드에서는 'id' 요소에 값을 입력했는 지 검사하고, id 요소에 값을 입력하지 않았을 경우 alert() 함수를 사용해서 '아이디 입력하세요'라는 경고 메시지를 출력한다.

폼 값의 검사는 FormChecker의 validate() 함수를 실행할 때 수행된다. checkRequired 등의 함수를 사용해서 설정한 모든 검증 조건을 통과할 validate() 함수는 true를 리턴하고, 그렇지 않을 경우 false를 리턴한다. validate() 함수는 false를 리턴하기 전에 검증을 통과하지 못한 조건과 관련된 에러 메시지를 출력한다. 예를 들어, 위 검증 코드에서는 id를 최소 3글자 이상 입력하도록 검증조건을 추가했는데, 만약 id의 값을 두 글자만 입력했다면 '아이디는 3글자 이상 입력하세요'라는 오류 메시지를 경고창으로 보여주게 된다.

검증 조건을 추가할 때 사용되는 FormChecker의 함수는 다음과 같다. 아래에서 fieldName은 값을 검증할 입력 요소의 이름을 의미한다. errorMesage는 값 검증을 통과 못했을 때 alert()으로 보여줄 에러 메시지이며, focus가 true이면 값 검증에 실패한 요소에 포커스가 맞춰진다.

  • checkRequired(fieldName, errorMessage, focus)
    값을 입력했는지의 여부를 검사한다.
  • checkMaxLength(fieldName, maxLength, errorMessage, focus)
    값의 길이가 maxLength보다 작거나 같은지 검사한다.
  • checkMaxLengthByte(fieldName, maxLength, errorMessage, focus)
    값의 바이트 길이가 maxLength보다 작거나 같은지 검사한다.
  • checkMinLength(fieldName, minLength, errorMessage, focus)
    값의 길이가 minLength보다 크거나 같은지 검사한다.
  • checkMinLengthByte(fieldName, minLength, errorMessage, focus)
    값의 바이트 길이가 minLength보다 크거나 같은지 검사한다.
  • checkAlphaNum(fieldName, errorMessage, focus)
    값이 알파벳과 0~9사이의 숫자만 포함하는 지 검사한다.
  • checkOnlyNumber(fieldName, errorMessage, focus)
    값이 0~9 사이의 문자만 포함하는 지 검사한다.
  • checkDecimal(fieldName, errorMessage, focus)
    값이 숫자인지 검사한다.
  • checkEmail(fieldName, errorMessage, focus)
    값이 올바른 이메일 주소인지 검사한다.
  • checkSelected(fieldName, firstIdx, errorMessage, focus)
    <select>에서 선택한 옵션의 인덱스가 firstIdx보다 크거나 같은 지 검사한다.
  • checkAtLeastOneChecked(fieldName, errorMessage, focus)
    checkbox나 radio 입력 요소가 최소한 1개 이상 선택됐는지 검사한다.
  • checkRegex(fieldName, regex, errorMessage, focus)
    값이 정규표현식에 해당하는 지 검사한다.

위 함수에서 주의할 점은 checkRequire를 제외한 나머지 함수들은 값이 ''인 경우 검증에 통과한다는 것이다. 이렇게 만든 이유는 필수 요소가 아닌 경우에는 값을 입력하지 않아도 검증에 통과되어야 하기 때문이다. 필수요소라면 먼저 checkRequired() 함수를 사용해서 검증 조건을 추가해주어야 한다. 예를 들어, email 입력 요소가 필수이고 이메일 주소를 입력해야 한다면 다음과 같이 검증 코드를 작성해야 한다.

    var FormChecker checker = new FormChecker(document.memberForm);
    checker.checkRequired('email', '이메일 주소를 입력하세요', true);
    checker.checkEmail('email', '올바른 이메일 주소를 입력하세요', true);

결론

본 글에서는 틀 자바스크립트가 제공하는 FormChecker 를 사용해서 폼 값을 검증하는 방법에 대해서 살펴보았다. FormChecker를 통해서 쉽고 간단하게 그리고 더불어 가독성이 높은 폼 검증 코드를 작성할 수 있는 것을 알게 되었다. 앞으로 틀 자바스크립트는 좀더 편리한 기능이 추가될 예정인데, 만약 틀 자바스크립트의 개발에 참여하고 싶다면 틀 프로젝트 사이트(http://kldp.net/projects/tle)나 madvirus@madvirus.net을 통해서 알려주기 바란다.

Posted by [czar]
,

Session을 이용하여,

중복로그인 체크 및 현재 접속자를 알수 있는 방법이다.



1. SessionChecker 클레스를 하나 만든다.

bound/unbound event를 캐치하기 위해서...


=======================================================================================

import javax.servlet.http.*;

public class SessionChecker{

 public void setSession(HttpSession session){
  if(session.getAttribute("listener") == null) {
   session.setAttribute("listener", new CustomBindingListener());
  }  
 }
}

class CustomBindingListener implements HttpSessionBindingListener {
 
 public void valueBound(HttpSessionBindingEvent event) {

  System.out.println("BOUND as " + event.getName() + " to " + event.getSession().getId());

  //DB user table에 해당user의 sessionID를 저장한다. => login
 }

 public void valueUnbound(HttpSessionBindingEvent event) {

  System.out.println("UNBOUND as " + event.getName() + " to " + event.getSession().getId());
 
  //DB user table에 해당user의 sessionID를 삭제한다. => logout
 }
}

=======================================================================================



2. jsp에서 클래스 호출

현재 session을 setting 한다..


<jsp:useBean id="sc" class="SessionChecker" scope="application" />
<%    
  sc.setSession(session);
%>



이제 준비는 끝났다.


로그인/로그아웃시 해당 user의 sessionID가 DB에 저장됨으로,

- 로그인시 sessionID를 확인하여 값이 있으면 로그인, 없으면 로그아웃

- sessionID값이 있는 사용자만 읽어오면 현재 접속자수와 접속자 정보를 알 수 있다.

 

참고 할 것은...

정상적인 로그아웃일 경우에도 sessionID를 삭제 처리해주는 쌘쓰~~

Posted by [czar]
,

지금까지 중복로그인 체크를 막기위한 테스트를 해 보았다.

처음 강좌에서 얘기했던 윈도우 닫기버튼,

윈도우 닫기 단축키인 Alt+F4, Ctrl+E를 수행했을때 세션을 끊는방법을

알아보겠다. 우리가 아래 예제를 수행했던 이유를 먼저 알아보자.

특정 사용자가 로그인을 시도한다고 생각해보자.

일반 웹사이트에서는 로그인을하고 다른쪽에서

로그인을 시도할경우 아무런 대책없이 로그인을 허용하였다.

우리가 많이 사용하는 메신저의 예를 들어보자.

메신저의 경우 로그인을 했을시 이미 접속중이라는 메시지가 뜨게된다.

만약 위와같은 메시지가 수시로 뜬다면

자신의 id가 누군가가 도용해서 사용중이라는 사실을 알수있을것이다...

그런 느낌이 온다면 우리는 id, password를 변경하여 정보누출을

어느정도 막을수 있을것이다.

그리고 또하나 자신의 계정으로 접속중에 제 3자의 누군가가 로그인을 했다면

누군가가 다른곳에서 접속중이라는 메시지를 뿌려줄수 있다면 훨씬 더 좋을

것이다.

우리가 만든 예제에서는 몇가지 문제점이 있다.

위기능을 사용하기 위해서는 로그인시 누군가가 자신의 id로

이미 로그인 중인지를 알아내는것이 중요하다.

그리고 이미 로그인된것이 확인 되었다면 자신이 로그인하기를 원한다면

이미 로그인한 세션을 끊고 자신의 세션을 등록해야한다..

그러기 위해서 우리는 해시테이블을 사용하였으며..

기존의 세션을 끊기위해서 세션 오브젝트를 직접 담았다.

문제점은 누군가가 자신의 id로 이미 로그인중인지 알아내는것이다.

우리가 로그인을 하고 무조건 로그아웃버튼을 클릭했다면 그당시 세션을 끊어

다른곳에서의 로그인을 바로 허용하면 되지만 닫기버튼을 누를때나

Alt+F4, Ctrl+E를 누를때 이벤트를 잡지못하면 안된다는것이다.

그렇지 못하면 그 세션은 서버의 메모리에 계속 살아서 세션이 타임아웃되기

까지는 계속 살아있을것이다.(이때는 HttpSessionBindingListener의 valueUnbound함수가 서블릿 컨테이너에서 호출한다.)

정상적으로 로그아웃버튼을 누르고 브라우저를 닫는사람이 어디있겠는가?

대게의 사용자들은 로그아웃을 하지 않고 바로 닫아버릴것이다.

여기서 위의 방법으로 닫았을때 이벤트를 잡을수 있는 방법을 설명하겠다.

예제는 간단하다.




============================== main.jsp ==============================
<frameset rows="0,*" border="0">
    <frame name="duplChkFrame" scrolling=no frameborder=0 marginwidth="0" marginheight="0" src='frame.html' noresize>
    <frame name="topFrame" scrolling="no" frameborder=0 marginwidth="0" marginheight="0" target="mainFrame" src="mainFrame.jsp" noresize>
</frameset>


============================== frame.html' ============================
<html>
<head>
<script>
    /**
    * logout()
    * 작 성 자 : 권홍재
    * 작 성 일 : 2006-12-18
    * 개    요  : 브라우저가 닫길시 호출
    * return값 : void
    */
    function logout(){
        location.href = 'logout.jsp';
    }
</script>

</head>
<body OnUnload="logout()">
</body>
</html>






위처럼 onUnLoad이벤트를 사용하면 된다.

프레임으로 나눈 이유는 브라우저가 하나의 프레임만으로 되어있다면..

페이지 이동시마다 onUnLoad이벤트가 발생한다는것이다..

곧 페이지가 이동시마다 로그아웃이 발생한다는뜻이며

고로 세션이 끊겨서 페이지를 이동할수 없다는 말이 된다.

이를 방지하기 위해서 프레임으로 나누어서 메인 프레임에는

일반적인 페이지를 호출을 하고 로그아웃 페이지를 호출하는부분은

페이지가 변경되지 않는 frame.html에서 로그아웃 처리를 하는것이다.

페이지가 닫길때 frame.html에서 onUnLoad이벤트가 발생하여

logout.jsp를 호출하는것이다.

참... 그리고 한가지 새로고침버튼이나 F5번버튼 마우스 오른쪽 버튼을

클릭하고 새로고침을 할경우는 어쩔수 없다는 것이다..

페이지를 다시 부르는것이기때문에 페이지를 부르기전 기존페이지가

죽기전에 onUnload이벤트가 발생한다는 것이다. 이를 해결하기 위해서는

마우스 오른쪽버튼 상단의 메누 보이지 않기, 키업이벤트에서 F5번키를

막는수밖에는 없다.^^;
출처 : 오라클자바(www.oraclejava.co.kr)

Posted by [czar]
,

=============================== login.jsp ============================
<%
    /*
     * 로그인 페이지, 로그인전 현재 로그인된 이용자수를 출력한다.
     */
%>
<%@ page language="java" contentType="text/html; charset=EUC-KR"%>
<%@ page import="test.LoginManager"%>
<%!
    //싱글톤 패턴을 사용하였기 때문에 생생되어있는 인스턴스를 얻어온다.
    LoginManager loginManager = LoginManager.getInstance();
%>
<%
    //login_try에서 로그인을 하지 않을경우 세션에 남아있는 userId를 제거한다.
    session.removeAttribute("userId");
%>
<html>
<head>
    <title>로그인 중복방지 Test</title>
</head>
<body>
    <h3 align="center">현재 접속자 수 : <%=loginManager.getUserCount() %>명</h3>
    <form action="login_try.jsp" name="login">
        <div align="center">
            아이디  :   <input type="text" name="userId"><br>
            비밀번호    :   <input type="passward" name="userPw"><br>
            <input type="submit" value="로그인">
        </div>
    </form>
</body>
</html>




============================= login_try.jsp ============================
<%
    /*
     * 로그인 시도페이지, id, pw유무를 체크하고, 올바르다면
     * 이미 접속한 아이디인지 체크한다. 이미 접속한 아이디라면
     * 기존 접속을 유지할것인지, 기존접속을 kill시키고 로그인할것인지를
     * 확인한다.
     */
%>
<%@ page language="java" contentType="text/html; charset=EUC-KR" %>
<%@ page import="test.LoginManager"%>
<%!
    //싱글톤 패턴을 사용하였기 때문에 생생되어있는 인스턴스를 얻어온다.
    LoginManager loginManager = LoginManager.getInstance();
%>
<html>
<head>
    <title>로그인 중복방지 Test</title>
</head>
<body align="center" valign="center">
<%
    String userId = request.getParameter("userId");
    String userPw = request.getParameter("userPw");
   
    //아이디 패스워드 체크
    if(loginManager.isValid(userId, userPw)){
       
        //접속자 아이디를 세션에 담는다.
        session.setAttribute("userId", userId);
       
        //이미 접속한 아이디인지 체크한다.
        //out.println(userId);
        //out.println(loginManager.isUsing(userId));
        loginManager.printloginUsers();
        if(loginManager.isUsing(userId)){
%>
            이미 접속중입니다. 기존의 접속을 종료하시겠습니까?<P>
            <a href="disconnect.jsp">예 </a>
            <a href="login.jsp">아니오</a>
<%
        }else{
            loginManager.setSession(session, userId);
            response.sendRedirect("login_ok.jsp");
        }
%>
<%
    }else{
%>
        <script>
            alert("로그인후 이용해 주세요.");
            location.href = "login.jsp";
        </script>
<%
    }
%>
</body>
</html>




========================== disconnect.jsp ============================
<%
    /*
     * login_try.jsp에서 로그인 중복시 무시하고 로그인할경우 호출.
     * 기존의 session을 끊고 hashTable에 저장후 login_ok.jsp를 호출.
     */
%>
<%@ page language="java" contentType="text/html; charset=EUC-KR" %>
<%@ page import="test.LoginManager"%>
<%!
    //싱글톤 패턴을 사용하였기 때문에 생생되어있는 인스턴스를 얻어온다.
    LoginManager loginManager = LoginManager.getInstance();
%>
<html>
<head>
    <title>로그인 중복방지 Test</title>
</head>
<body>
<%
    String userId = (String)session.getAttribute("userId");
    if(userId != null){
        //기존의 접속(세션)을 끊는다.
        loginManager.removeSession(userId);
       
        //새로운 세션을 등록한다. setSession함수를 수행하면 valueBound()함수가 호출된다.
        loginManager.setSession(session, userId);
        response.sendRedirect("login_ok.jsp");
    }
%>
</body>
</html>




============================= login_ok.jsp ============================
<%
    /*
     * 정상적으로 로그인되었을경우 호출
     * 접속자 아이디를 보여주고 현재 접속중인 모든 사용자를 뿌려준다.
     */
%>
<%@ page language="java" contentType="text/html; charset=EUC-KR" %>
<%@ page import="java.util.*, test.LoginManager"%>

<%!
    //싱글톤 패턴을 사용하였기 때문에 생생되어있는 인스턴스를 얻어온다.
    LoginManager loginManager = LoginManager.getInstance();
%>
<html>
<head>
    <title>로그인 중복방지 Test</title>
</head>
<body align="center" valign="center">
<%
    //jsp내장객체 session을 이용하여 접속자 아이디를 얻어온다.
    String userId = (String)session.getAttribute("userId");

    if(userId != null){
%>
        <%=userId%>님 환영합니다.
        <a href="logout.jsp">로그아웃</a>
        <p>
        현재 접속자 : <br>
<%
        Collection collection = loginManager.getUsers();
        Iterator iterator = collection.iterator();
        int i=0;
        while(iterator.hasNext()){
            out.print((++i)+". "+iterator.next()+"<br>");
        }
    }else{
%>
        <script>
            alert("로그인후 이용해 주세요.");
            location.href = "login.jsp";
        </script>
<%
    }
%>
</body>
</html>



============================= logout.jsp ==============================
<%
    /*
     * 로그아웃을 클릭했을때 호출된다.
     */
%>
<%@ page language="java" contentType="text/html; charset=EUC-KR" %>
<%
//session을 확~~~끊어 버린다. 이시점에 LoginManager의 valueUnbound()가 호출된다.
session.invalidate();
response.sendRedirect("login.jsp");
%>
출처 : 오라클자바(www.oraclejava.co.kr)

Posted by [czar]
,
/*
* 로그인 중복체크
* 이미 로그인한 사용자가 있을경우 기존의 사용자 세션을 종료후 자신이 로그인.
* 해시테이블에 세션과 접속자 아이디를 저장해 둔다.
* 세션 Object를 저장하는 이유는 동일한 아이디로 재접속 했을경우
* 아이디로 세션Object를 찾아내어 기존의 접속을 끊기위해서다.(invalidate)
*/


============== WEB-INF\src\test\LoginManager.java ===============
package test;

import java.util.*;
import javax.servlet.http.*;
/*
* session이 끊어졌을때를 처리하기 위해 사용
* static메소드에서는 static만사용 하므로static으로 선언한다.
*/
public class LoginManager implements HttpSessionBindingListener{

    private static LoginManager loginManager = null;
   
    //로그인한 접속자를 담기위한 해시테이블
    private static Hashtable loginUsers = new Hashtable();
   
    /*
     * 싱글톤 패턴 사용
     */
    public static synchronized LoginManager getInstance(){
        if(loginManager == null){
            loginManager = new LoginManager();
        }
        return loginManager;
    }
    
   
    /*
     * 이 메소드는 세션이 연결되을때 호출된다.(session.setAttribute("login", this))
     * Hashtable에 세션과 접속자 아이디를 저장한다.
     */
    public void valueBound(HttpSessionBindingEvent event) {
        //session값을 put한다.
        loginUsers.put(event.getSession(), event.getName());
        System.out.println(event.getName() + "님이 로그인 하셨습니다.");
        System.out.println("현재 접속자 수 : " +  getUserCount());
     }
   
   
     /*
      * 이 메소드는 세션이 끊겼을때 호출된다.(invalidate)
      * Hashtable에 저장된 로그인한 정보를 제거해 준다.
      */
     public void valueUnbound(HttpSessionBindingEvent event) {
         //session값을 찾아서 없애준다.
         loginUsers.remove(event.getSession());
         System.out.println("  " + event.getName() + "님이 로그아웃 하셨습니다.");
         System.out.println("현재 접속자 수 : " +  getUserCount());
     }
    
    
     /*
      * 입력받은 아이디를 해시테이블에서 삭제.
      * @param userID 사용자 아이디
      * @return void
      */
     public void removeSession(String userId){
          Enumeration e = loginUsers.keys();
          HttpSession session = null;
          while(e.hasMoreElements()){
               session = (HttpSession)e.nextElement();
               if(loginUsers.get(session).equals(userId)){
                   //세션이 invalidate될때 HttpSessionBindingListener를
                   //구현하는 클레스의 valueUnbound()함수가 호출된다.
                   session.invalidate();
               }
          }
     }
    
    
     /*
      * 사용자가 입력한 ID, PW가 맞는지 확인하는 메소드
      * @param userID 사용자 아이디
      * @param userPW 사용자 패스워드
      * @return boolean ID/PW가 일치하는 지 여부
      */
     public boolean isValid(String userId, String userPw){
        
         /*
          * 이부분에 Database 로직이 들어간다.
          */
         return true;
     }


    /*
     * 해당 아이디의 동시 사용을 막기위해서
     * 이미 사용중인 아이디인지를 확인한다.
     * @param userID 사용자 아이디
     * @return boolean 이미 사용 중인 경우 true, 사용중이 아니면 false
     */
    public boolean isUsing(String userID){
        return loginUsers.containsValue(userID);
    }
    
   
    /*
     * 로그인을 완료한 사용자의 아이디를 세션에 저장하는 메소드
     * @param session 세션 객체
     * @param userID 사용자 아이디
     */
    public void setSession(HttpSession session, String userId){
        //이순간에 Session Binding이벤트가 일어나는 시점
        //name값으로 userId, value값으로 자기자신(HttpSessionBindingListener를 구현하는 Object)
        session.setAttribute(userId, this);//login에 자기자신을 집어넣는다.
    }
    
    
    /*
      * 입력받은 세션Object로 아이디를 리턴한다.
      * @param session : 접속한 사용자의 session Object
      * @return String : 접속자 아이디
     */
    public String getUserID(HttpSession session){
        return (String)loginUsers.get(session);
    }
    
    
    /*
     * 현재 접속한 총 사용자 수
     * @return int  현재 접속자 수
     */
    public int getUserCount(){
        return loginUsers.size();
    }
    
    
    /*
     * 현재 접속중인 모든 사용자 아이디를 출력
     * @return void
     */
    public void printloginUsers(){
        Enumeration e = loginUsers.keys();
        HttpSession session = null;
        System.out.println("===========================================");
        int i = 0;
        while(e.hasMoreElements()){
            session = (HttpSession)e.nextElement();
            System.out.println((++i) + ". 접속자 : " +  loginUsers.get(session));
        }
        System.out.println("===========================================");
     }
    
    /*
     * 현재 접속중인 모든 사용자리스트를 리턴
     * @return list
     */
    public Collection getUsers(){
        Collection collection = loginUsers.values();
        return collection;
    }
}

/*
* 거의 대부분의 웹사이트를 보면 브라우저를 열고 로그인후
* 또다시 다른브라우저를 열고 로그인을 하면 로그인이 되는것을 확인하실수
* 있습니다. 이는 즉... 여러곳에서 동일한 아이디로 접속을 할수있다는 예입
* 니다. 이와 반대로 메신저같은경우는 이미 로그인이 되어있을시 다른곳에서
* 로그인을 하면 접속을 끊을지를 물어보는 기능도 보셨을 겁니다. 이를 웹에
* 서 구현하여 보았습니다.
* 본 예제소스는 우리가 구현하려고 예제에서 가장 핵심적인 부분을 맡고있
* 는 소스입니다.
* 여기서 HttpSessionBindingListener는 서블릿 컨테이너에서 세션이 끊길때
* (valueUnBound)와 이를 구현하는 오브젝트가 해당 세션에 setAttribute될
* 때(valueBound) 호출합니다. 굳이 이를 구현하는이유는 세션이 끊기는 시
* 점을 정확히 잡아내기 위함입니다. 사용자가 로그아웃버튼을 누를시도 있지
* 만 세션이 타임아웃되는경우도 세션이 끊겨야 하기 때문입니다. 그리고
* 브라우저의 닫기버튼, Alt+F4, Ctrl+E버튼 을 누를시 이벤트를 잡는방법도
* 차근차근 알아보도록 합시다.
출처 : 오라클자바(www.oraclejava.co.kr)
Posted by [czar]
,
http://www.webdeveloper.com/forum/archive/index.php/t-49680.html



 span 태그에 width 속성을 지정하여 넓이를 할당하려 했으나 전혀 움직임이 없었다

span은 inline element이기 때문에 width 속성이 적용이 안된다.

그러므로 '제목'에는 p나 blockquote, div같은 block element를 적용해야 한다. 적용하고 나면 이들이 block element이기 때문에 입력란이 아랫줄에 출력된다는 새로운 문제가 발생한다. 이 문제는 p, blockquote, div의 float 속성을 left로 지정하면 해결할 수 있다.
Posted by [czar]
,

출처 : http://nadachel.tistory.com/entry/자바java로-엑셀excel-읽기read-2

ⅰ. 엑셀을 자바로 읽는방법 두번째를 작성하여 보도록 하겠다.


  POI프로젝트는 아파치구룹의 자카르타 프로젝트 일환으로 poor object importalble과 비슷한 약자였다. (확실하지는 않다ㅡ.ㅡ;) 아무튼 혼자 만으로는 빈약하다는 뜻인거 같다.
그리고 poi자체로 토란이라는 뜻도 있단다. 역시 빈약하다.
아무튼 모든 아파치 구룹이 그러하듯
  http://mirror.apache-kr.org/poi/release/bin/
 가서 zip최신걸로, 덜렁 받아 압축풀고 클레스 패스 잡아준다.
 추신 : POI는 Microsoft Format File을 액세스 할 수 있는 API를 제공한다고 한다.
(엑셀만 할수 있는 것이 아니란 말이다.)

 참고로
http://apache.org/ 가서 poi 선택하고 javadocs가면 API있다.
영어되면 다른 무궁한 프로젝트도 참고 하길 바란다.
 

ⅱ. 역시 예문을 만들어 보면


/***************************************************************
*  기          능    :  엑셀파일 읽어 String으로 반환
*     Function     :  getData(), getData(String sheets, String rows, String cols),
                       :  xlsFormDate..., getXLSDate...
*  참 조 :  C:/Program Files/Java/jdk1.6.0_01/jre/lib/ext/poi-3.0.1-FINAL-20070705.jar
            :  C:/Program Files/Java/jdk1.6.0_01/jre/lib/ext/poi-contrib-3.0.1-FINAL-20070705.jar
            :  C:/Program Files/Java/jdk1.6.0_01/jre/lib/ext/poi-scratchpad-3.0.1-FINAL-20070705.jar
*  Return  value :  String
*  작    성    자  :  유 진 철
*  작    성    일  :  2007-12-17
/***************************************************************/

import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.hssf.record.*;
import org.apache.poi.hssf.model.*;
import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.hssf.util.*;

import java.io.*;
import java.util.*;
import java.text.*;
import java.math.*;

public class ReadXLS
{
   //필드 선언 엑셀파일이름, 내용, 읽을범위
   private String filename = null;
   private HSSFWorkbook hssfworkbook = null;
   private int startSheet =  0;
   private int endSheet   = -1;
   private int startRow   =  0;
   private int endRow     = -1;
   private int startCol   =  0;
   private int endCol     = -1;

   //ReadXLS 생성자
   public ReadXLS(String filename) throws Exception
   {     
      File file = new File(filename); 

      //존재여부 판단
      if(file.exists())
      {
         //파일여부 판단
         if(file.isFile())
         {
            StringTokenizer st = new StringTokenizer(filename, ".");
            String xls = null;

            //파일 확장자 구하기
            while(st.hasMoreTokens())
            {
               xls = st.nextToken();  
            }

            //확장자 엑셀파일 여부판단
            if(xls.equals("xls"))
            {
               this.filename = filename;
               POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(filename));
               hssfworkbook = new HSSFWorkbook(fs);
             
            }
            else
            {
               throw new Exception("엑셀파일이 아닙니다.");
            }

         }
         else
         {
            throw new Exception("파일이 아닙니다.");
         }
      }
      else
      {        
         throw new Exception("존재하지 않는 이름 입니다.");
      }        
   }

   //String 반환하는 메소드
   public String getData() throws Exception
   {
      return getData("ALL","ALL","ALL");
   }

   //String 반환하는 메소드
   public String getData(String sheets, String rows, String cols) throws Exception
   {
      //입력받은 값을 엑셀파일 읽을범위 값으로 전환
      StringTokenizer stTok = new StringTokenizer(sheets, ",");
      StringTokenizer rwTok = new StringTokenizer(rows,   ",");
      StringTokenizer clTok = new StringTokenizer(cols,   ",");

      String startSheetTok = null;
      String endSheetTok   = null;
      String startRowTok   = null;
      String endRowTok     = null; 
      String startColTok   = null;
      String endColTok     = null; 

      int sheetCounts = stTok.countTokens();
      int rowCounts   = rwTok.countTokens();
      int colCounts   = clTok.countTokens();

      //쉬트범위 판단
      if(sheetCounts == 2)
      {
         startSheetTok = stTok.nextToken();
         endSheetTok   = stTok.nextToken();
        
         try
         {
            startSheet = Integer.parseInt(startSheetTok);
            if (startSheet<0) { startSheet = 0; }    
         }
         catch(NumberFormatException ne){}

         try
         {  
            endSheet   = Integer.parseInt(endSheetTok);             
         }
         catch(NumberFormatException ne){}    
      }

      //행범위 판단
      if(rowCounts == 2)
      {
         startRowTok = rwTok.nextToken();
         endRowTok   = rwTok.nextToken();
        
         try
         {
            startRow = Integer.parseInt(startRowTok);
            if (startRow<0) { startRow = 0; }    
         }
         catch(NumberFormatException ne){}

         try
         {  
            endRow   = Integer.parseInt(endRowTok);             
         }
         catch(NumberFormatException ne){}    
      }

      //열범위 판단
      if(colCounts == 2)
      {
         startColTok = clTok.nextToken();
         endColTok   = clTok.nextToken();
        
         try
         {
            startCol = Integer.parseInt(startColTok);
            if (startCol<0) { startCol = 0; }    
         }
         catch(NumberFormatException ne){}

         try
         {  
            endCol   = Integer.parseInt(endColTok);             
         }
         catch(NumberFormatException ne){}    
      }
      //읽어서 처리하는 메소드 호출
      return execute();
   }

   //실행 메소드
   private String execute() throws Exception
   {
      StringBuffer cellData = new StringBuffer();
     
      //읽는범위 유효성검사 및 조정
      int sheetNum = hssfworkbook.getNumberOfSheets()-1;
      if((startSheet-endSheet)>0 || endSheet>sheetNum)
      {           
         endSheet = sheetNum;
      }
     
      //읽는범위 만큼 실행
      for (int k = startSheet; k <= endSheet; k++)
      {
        
         HSSFSheet sheet = hssfworkbook.getSheetAt(k);
         if(sheet!=null)
         {           
            int       rowNum  = sheet.getLastRowNum();
            if((startRow-endRow)>0 || endRow>rowNum)
            {           
               endRow = rowNum;
            }

            for (int r = startRow; r <= endRow; r++)
            {              
               HSSFRow row   = sheet.getRow(r);
               if(row!=null)
               {                 
                  int    colNum = row.getLastCellNum()-1;
                  if((startCol-endCol)>0 || endRow>colNum)
                  {           
                     endCol = colNum;
                  }

                 
                  for (int c = startCol; c <= endCol; c++)
                  {                    
                    HSSFCell cell  = row.getCell((short)c);
                     String   value = null;
                     if(cell!=null)
                     {
                        //셀타입 구분
                        switch (cell.getCellType())
                        {
                           case HSSFCell.CELL_TYPE_BLANK   :                             
                              value = "["+""
                              + "]";
                              break;

                           case HSSFCell.CELL_TYPE_BOOLEAN :                             
                              value = "["+cell.getBooleanCellValue()
                              + "]";
                              break;

                           case HSSFCell.CELL_TYPE_ERROR   :                            
                              value = "["+cell.getErrorCellValue()
                              + "]";
                              break;

                           case HSSFCell.CELL_TYPE_FORMULA :                             
                              value = "["+cell.getCellFormula()                             
                              + "]";
                              break;

                           case HSSFCell.CELL_TYPE_NUMERIC :                             
                              DecimalFormat dft = new DecimalFormat("########################.########");
                              value = "["+dft.format(cell.getNumericCellValue())
                              + "]";
                              break;

                           case HSSFCell.CELL_TYPE_STRING :                                                     
                              value = "["+cell.getRichStringCellValue().getString()
                              + "]";
                              break;

                           default :
                        }//End Of switch
                       
                        cellData.append(value);
 
                     }//End Of if cell
                    
                     else{cellData.append("[]");}                    

                  }//End Of for c++
                  cellData.append("\n");
               }//End Of if row
            }//End Of for r++
         }//End Of if sheet
      }//End Of for k++
     
      return new String(cellData);//String 반환
   }

   /*
   Date and Time Pattern        Result 
   "yyyy.MM.dd G 'at' HH:mm:ss z"   2001.07.04 AD at 12:08:56 PDT 
   "EEE, MMM d, ''yy"           Wed, Jul 4, '01 
   "h:mm a"                  12:08 PM 
   "hh 'o''clock' a, zzzz"       12 o'clock PM, Pacific Daylight Time 
   "K:mm a, z"               0:08 PM, PDT 
   "yyyyy.MMMMM.dd GGG hh:mm aaa"   02001.July.04 AD 12:08 PM 
   "EEE, d MMM yyyy HH:mm:ss Z"   Wed, 4 Jul 2001 12:08:56 -0700 
   "yyMMddHHmmssZ"              010704120856-0700 
   "yyyy-MM-dd'T'HH:mm:ss.SSSZ"   2001-07-04T12:08:56.235-0700
   */

   //원하는 페턴으로 문자형 날짜전달
   public static String xlsFormDate(String Pattern, String Date) throws Exception
   {     
      int date = Integer.parseInt(Date.trim());
      return xlsFormDate(Pattern, date);      
   }

   //원하는 페턴으로 문자형 날짜전달
   public static String xlsFormDate(String Pattern, int Date)
   {
      Calendar cal = Calendar.getInstance();
      cal.clear();  
      cal.add(cal.DATE, Date-25569);
      SimpleDateFormat sdt = new SimpleDateFormat(Pattern);
      return sdt.format(cal.getTime());
   }
  
   //Date 객체전달
   public static Date getXLSDate(String Date) throws Exception
   {     
      int date = Integer.parseInt(Date.trim());
      return getXLSDate(date);      
   }

   //Date 객체전달
   public static Date getXLSDate(int Date)
   {
      Calendar cal = Calendar.getInstance();
      cal.clear();  
      cal.add(cal.DATE, Date-25569);     
      return cal.getTime();
   }  

   public static void main(String [] args)
   {
      try
      {
         ReadXLS readxls = new ReadXLS("ex.xls");
         String xls = readxls.getData("0,0","ALL","ALL");        
         System.out.println(xls );

         /* //날짜 메소드 실험후 주석 처리함
         String Date = ReadXLS.xlsFormDate("yyyy-MM-dd hh:mm", "39423");
         System.out.println("Date : "+Date );

         Date aa = ReadXLS.getXLSDate("39423");
         System.out.println("Object Date : "+aa.toString());
        */

      }//End Of try
      catch (Exception e)
      {
         e.printStackTrace();
      }//End Of catch     
   }//End Of Main 
  
}

/*
똑같이 프로그램을 컴파일 해서 돌리면(물론 같은 폴더에 ex.xls라는 엑셀 파일을 하나 실험용으로 만들어서 넣어둔다.)
[xxx][xxx][xxx]...
[xxx][xxx][xxx]
[][][][][][][][]
[][][][][][][][]
.
.
.
이런 식으로 결과가 나온다.
이것 역시 필요없는 부분이 많은데 이것은 신경쓰지 말고 색상 있는 부분만 보면 된다. 좀더 편리하게 날짜를 데이터베이스에 넣거나(xlsFormDate), 지수를 소수로 표현하기 위하여(DecimalFormat) 사용한 겍체가 몇게 있다.  
*/

Posted by [czar]
,
출처 : http://nadachel.tistory.com/entry/자바java로-엑셀excel-읽기read-1

ⅰ.
엑셀을 자바로 읽을(쓰는것 포함) 수 있도록 하는 방법은 jxl과 poi 두가지 방법이 있는데 우선 그나마 간단한 jxl 방법을 몇자 적어 보도록 하겠다.
( jxl과poi차이나 특성, 프로젝트 정보등은 검색엔진을 이용하길 바란다. )

 우선
http://www.andykhan.com/jexcelapi/download.html 에 가서 최신jexcelapi.tar 파일을 다운로드 하고 압축을 풀면 파일안에 jxl.jar파일이 있다. 이 jar파일을 인식 시키기 위하여, 자바가 설치된 곳의 라이브러리 폴더에 이 파일을 넣는다. (예 : C:/Program Files/Java/jdk1.6.0_01/jre/lib/ext/jxl.jar).

 쓰기 등을 원하거나 다른 것을 원할때는
http://www.andykhan.com/jexcelapi/tutorial.html로 가면 tutorial이 있으니 이걸 참고하길 바란다.

--참고로 엔디칸인가 하는 주인장 한테 메일 보내 봤는데 답장이 없으니 편지 보내지는 말자.

ⅱ. 그럼 예문을 만들어 보면

/***************************************************************
*  기          능   :  엑셀파일 읽어 String으로 반환
*     Function    :  getData(int row) row : 읽을 시작행 번호
*  참          조   :  C:/Program Files/Java/jdk1.6.0_01/jre/lib/ext/jxl.jar
*  Return value  :  String
*  작    성    자  :  유 진 철
*  작    성    일  :  2007-08-17
/***************************************************************/

import jxl.*;
import java.io.*;
import java.util.*;

public class ReadXLS
{
   String filename = null;

   //ReadXLS 생성자
   public ReadXLS(String filename)
   {
      this.filename = filename.trim();  
   }

   //String 반환하는 메소드
   public String getData(int row) throws Exception
   {
      //파일 생성및 데이터 선언
      StringBuffer cellData = new StringBuffer();                     
      File file = new File(filename); 

      //존재여부 판단
      if(file.exists())
      {
         //파일여부 판단
         if(file.isFile())
         {
            StringTokenizer st = new StringTokenizer(filename, ".");
            String xls = null;

            //파일 확장자 구하기
            while(st.hasMoreTokens())
            {
               xls = st.nextToken();  
            }

            //확장자 엑셀파일 여부판단
            if(xls.equals("xls"))
            {
               //엑셀정보 에서 쉬트정보 가져오기
               Workbook workbook = Workbook.getWorkbook(file);
               Sheet sheet = workbook.getSheet(0);
              
              //데이터 생성및 구분자 삽입 col=>[xxx] / row=>\n
               for (int i=row; i<sheet.getRows(); i++ )
               { 
                  for (int j=0; j<sheet.getColumns(); j++ )
                  { 
                     Cell cell = sheet.getCell(j,i);
                     cellData.append("["+cell.getContents().trim()+"]");
                                  
                  }
                  cellData.append("\n");
               }            
               workbook.close();
            }
            else
            {
               throw new Exception("엑셀파일이 아닙니다.");
            }

         }
         else
         {
            throw new Exception("파일이 아닙니다.");
         }
      }
      else
      {        
         throw new Exception("존재하지 않는 이름 입니다.");
      }           
      return new String(cellData);//String 반환
   }//end getData()

   public static void main(String [] args)
   {
      try
      {
         ReadXLS readxls = new ReadXLS("ex.xls");
         String xls = readxls.getData(0); //0번 행 부터 읽기      
         System.out.println(xls );
      }//End Of try
      catch (Exception e)
      {
         e.printStackTrace();
      }//End Of catch     
   }//End Of Main 
  
}


/*
다음과 같은 프로그램을 컴파일 해서 돌리면(물론 같은 폴더에 ex.xls라는 엑셀 파일을 하나 실험용으로 만들어서 넣어둔다.)
[xxx][xxx][xxx]...
[xxx][xxx][xxx]
[][][][][][][][]
[][][][][][][][]
.
.
.
이런 식으로 결과가 나온다.
프로그램에서 파일겍체를 만들거나 하는것은 안전한 실행을 위해서 한것이고 핵심은 색상이 다른 부분을 중점적으로 보면 될것이다.
*/

Posted by [czar]
,