상세 컨텐츠

본문 제목

[저장용] Web Service 첨부파일 다루기

개발

by 동동주1123 2011. 11. 10. 14:11

본문


출처 : http://improf.egloos.com/2318776





핸들러를 사용한 첨부파일 처리 웹서비스 Webservices

http://openframework.or.kr/Wiki.jsp?page=ImplementFileWebServiceUsingHandler

<div class="information"> 현재 이 문서는 서버의 c::\temp\test.txt파일을 SOAP메시지에 담아서 가져와서 클라이언트의 c:\ 에 t.txt라는 이름으로 파일을 저장하는 것을 보여준다. 여기서 첨부파일 처리는 핸들러를 사용하고 있다. 사용된 IDE는 Jmaker 3.2이고 Jeus의 버전은 5.0 fix18이다. </div>

## 실제 구현에 앞서 이론적인 사항들 ###

메시지 형태#

첨부파일을 사용하는 웹서비스의 경우 기본적인 웹서비스의 SOAP메시지에서 Attachment Part가 추가된 것으로 이해하시면 됩니다. 즉 첨부파일 서비스의 경우 SOAP메시지의 형태는 아래와 같으며 첨부파일을 사용하지 않는 웹서비스의 경우 아래 그림에서 SOAPPart만 존재하는 메시지를 주고 받는다고 보시면 됩니다.

1-1.gif

실제 응답 메시지#

위 그림은 스펙에서 설명하는 SOAP메시지의 형태를 나타내는 그림이고 실제 SOAP메시지를 보면 다음과 같은 형식이 됩니다.

****************************** response: Start******************************
HTTP/1.1 200 OK 
Date: Sat, 26 May 2007 03:13:14 GMT 
Content-type: multipart/related;type="text/xml";boundary="----=_Part_1_1725394919.1180149194097";
start=__WLS__1180149194097__SOAP__; charset=utf-
Set-Cookie: MBMSSESSIONID=GXlJd2G5G6pc0hTDgzVrB7ppb1Cyyzdf5GXT0p9kVpDR8y8XQbcs!437012102; path=/ 
Transfer-encoding: chunked 
2e2 
------=_Part_1_1725394919.1180149194097 
Content-Type: text/xml; charset=utf-
Content-Transfer-Encoding: 8bit 
Content-ID: __WLS__1180149194097__SOAP__ 
 
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<env:Body>
<n1:sendFileResponse xmlns:n1="http://openframework.or.kr">
<n2:attc_file_id xmlns:n2="java:kr.or.openframework.vo ">9d5d92760b3611dcf0840014c2447a56</n2:attc_file_id>
<n3:attc_file_nm xmlns:n3="java: kr.or.openframework.vo" xsi:nil="true">
</n3:attc_file_nm>
</n1:sendFileResponse>
</env:Body>
</env:Envelope> 
fe8 
------=_Part_1_1725394919.1180149194097 
Content-Type: application/octet-stream 
.............. 첨부파일내용

------=_Part_1_1725394919.1180149194097--

주의해서 보실 부분은 Content-type과 boundary입니다. 예제 메시지를 보면 content-type이 text/xml인 SOAPPart와 application/octet-stream인 Attachment Part가 보일것입니다. 그리고 boundary는 시작 boundary가 ------=_Part_1_1725394919.1180149194097로 두번 찍히고 시작 boundary에 “—“ 가 추가된 종료 boundary가 보일것 입니다. 이 두 가지 사항과 값은 필수값으로 이 값이 정상적으로 전달되지 않는다면 표준 스펙을 준수하지 않는 것으로 제품마다 처리할 때 에러가 발생할 여지를 남기게 되는셈입니다. 특히 종료 boundary의 경우 이 값이 넘어오지 않는다면 파일 처리시 EOF가 없는 것과 거의 동일한 에러가 발생합니다. 종료 boundary체크는 tcpmon을 사용하여 체크가 가능합니다.

boundary관련 사항은 다음의 URL에서 정보를 추가적으로 얻을수 있습니다. http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html http://blog.naver.com/moonjoh?Redirect=Log&logNo=30005161034

서버및 클라이언트측 공통사항#

일반적인 웹서비스와 큰 차이점은 없습니다. 구현상의 차이점은 첨부파일을 처리하기 위해 별도의 핸들러를 구현하고 서비스 등록시 핸들러를 추가한 정도입니다.

다음 URL참조. http://kr.sun.com/developers/techtips/2006/e0810.html

1-2.gif

핸들러의 사용은 위 그림을 보면 간단히 이해할수 있습니다. 핸들러가 담당하는 부분은 SOAP메시지에 포함되어 있는 실제 파일정보를 가져와서 서버에 해당 파일을 저장하고 SOAPPart는 일반적인 웹서비스처럼 서비스로 그대로 전달시키는 역할을 담당합니다. 서비스 입장에서 request를 받기 전에는 handleReqeust가 호출되고 request를 받고 response를 던질때는 handleResponse를 거치게 됩니다. 즉 실제 구현로직이 두 메소드에 위치하기 때문에 실제로직은 두 메소드에서 확인해보시면 됩니다.

## 구현해보자 ###

Dynamic Web Project를 생성#

Jeus의 기본적으로 웹서비스 프로젝트는 Dynamic Web Project를 가져야 하도록 되어 있다. 생성시 ear파일을 생성하도록 EAR Membership부분에 체크를 선택한다. 여기서 ear파일의 이름은 서비스명EAR 이 되도록 하는게 좋다. 물론 하나의 ear파일에 모두 넣도록 할수도 있지만 팀원이 각각의 웹서비스를 개발할 때 소스통합의 어려움이 존재하게 된다. 물론 ear파일 자체의 개수가 많아질 경우 원격지 서버에 디플로이 할 때 디플로이에 소요되는 시간이 길다는 단점이 생기기도 한다. 이에 대해서는 프로젝트 전에 사전에 정의하는 방식이 바람직할 것으로 판단된다.

1.gif

프로젝트 구조#

다음처럼 두 개의 프로젝트 디렉토리가 생성이 되고 FileService는 실제 서비스 부분에 대한 개발을 위한 프로젝트이고 FileServiceEAR은 ear파일 생성을 위한 프로젝트이다. 실제 ear파일은 나중에 원격지에 서비스를 디플로이 하고자 할 때 사용할 파일이다.

2.gif

서비스 클래스 생성#

다음처럼 실제 서비스를 하기 위한 자바 소스를 생성한다. 여기서 사용된 클래스 명은 FileService이다.

3.gif

서비스 클래스 내용#

여기서는 일단 첨부파일을 가져오는 서비스만을 대상으로 하고자 하기 때문에 다음처럼 파일 id를 받는 getFile이라는 메소드를 하나 생성한다. 실제 첨부파일에 대한 처리는 핸들러에서 모두 처리한다.

package kr.or.openframework;

public class FileService {
  public String getFile(String fileId){
    return fileId;
  }
}

웹서비스 생성#

자바 클래스를 기반으로 웹서비스를 생성하기 위해 다음처럼 해당 자바소스를 선택하고 메뉴를 통해 웹서비스 생성을 위한 마법사 화면으로 이동한다.

4.gif

Server및 Web Service Runtime값 확인#

Server는 JEUS , Web Service Runtime은 Jeus WebService가 되어 있는지 확인하고 값이 다르다면 바꾸도록 한다. 웹서비스의 경우 벤더별로 구현형태가 다르기 때문에 사전에 어느 밴더의 제품을 기반으로 웹서비스를 구축할지 정의하는 작업은 필요하다.

5.gif

style and use값 선택#

style and use값을 변경하지 않는 이상 디폴트 값을 사용하면 된다.

6.gif

웹서비스 생성시 새롭게 생성되는 파일들#

웹서비스를 생성하면 다음처럼 서비스 클래스의 인터페이스(FileServiceIF.java), wsdl(FileService.wsdl), 그외 xml 설정파일(jaxrpc-mapping-fileservice.xml, webservices.xml)이 추가된다.

7.gif

webservices.xml 파일 내용#

webservices.xml 파일에 다음처럼 핸들러를 등록한다.

<?xml version="1.0" encoding="UTF-8"?>
<webservices xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.1"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
  http://www.ibm.com/webservices/xsd/j2ee_web_services_1_1.xsd">
  <webservice-description>
    <webservice-description-name>
      FileServiceService
    </webservice-description-name>
    <wsdl-file>WEB-INF/wsdl/FileService.wsdl</wsdl-file>
    <jaxrpc-mapping-file>
      WEB-INF/jaxrpc-mapping-fileservice.xml
    </jaxrpc-mapping-file>
    <port-component>
      <port-component-name>FileServiceIFPort</port-component-name>
      <wsdl-port xmlns:ns2="http://openframework.or.kr">
        ns2:FileServiceIFPort
      </wsdl-port>
      <service-endpoint-interface>
        kr.or.openframework.FileServiceIF
      </service-endpoint-interface>
      <service-impl-bean>
        <servlet-link>FileServiceServlet</servlet-link>
      </service-impl-bean>
      <handler>
        <handler-name>AttachmentHandler</handler-name>
        <handler-class>
          kr.or.openframework.handler.AttachFileHandler
        </handler-class>
      </handler>
    </port-component>
  </webservice-description>
</webservices>

서버용 첨부파일 핸들러#

앞서 등록한 핸들러 역할을 담당하는 클래스를 다음처럼 생성한다. 여기서는 첨부파일을 가져오는 서비스라 실제 구현은 handleResponse 메소드에 모두 있다. 만약 파일을 업로드하는 서비스라면 handleRequest 메소드를 상세히 구현해야 한다. handleResponse 메소드는 기본적으로 SOAP메시지에 파일 정보를 추가하는 작업을 담당하며 구현방식은 AttachmentPart를 생성하여 SOAP메시지의 SOAPPart뒤에 추가하는 형태를 취한다. 이 방식은 SOAP메시지의 첨부파일 처리를 하는 AttachmentPart의 표준 스펙의 구현형태이다. 즉 핵심적으로 봐야 할 부분은 handleResponse 메소드인 셈이다.

package kr.or.openframework.handler;

import java.util.Iterator;

import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.xml.namespace.QName;
import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.handler.Handler;
import javax.xml.rpc.handler.HandlerInfo;
import javax.xml.rpc.handler.MessageContext;
import javax.xml.rpc.handler.soap.SOAPMessageContext;
import javax.xml.soap.AttachmentPart;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;

public final class AttachFileHandler implements Handler {

  private HandlerInfo handlerInfo;
  private final String dirName = "c:/temp";

  public void init(HandlerInfo hi) {    
  }

  public void destroy() {    
  }

  public QName[] getHeaders() {
    return handlerInfo.getHeaders();
  }

  public boolean handleRequest(MessageContext mc) {
    System.out.println("\n## AttachFileHandler.handleRequest");
    System.out.println("\n## Call from : " + getRequest().getRemoteAddr());
    
    return true;
  }

  public boolean handleResponse(MessageContext mc) {
    System.out.println("\n## AttachFileHandler.handleResponse");
    
    SOAPMessageContext messageContext = (SOAPMessageContextmc;
    SOAPMessage response = messageContext.getMessage();

    try {
      AttachmentPart part = response.createAttachmentPart();
      String file_id = getFileInfo(response);
      if (file_id == null || file_id.length() == 0)
        return true;
      
      String filePath = dirName + "/" + file_id;      
      System.out.println("\n## filePath:" + filePath);

      FileDataSource fds = new FileDataSource(filePath);
      iffds.getFile().exists() ){
        System.out.println("#########################");
        System.out.println("Attach File exists...");
        System.out.println("#########################");
        
        DataHandler result = new DataHandler(fds);
        part.setContentId(file_id);
        part.setDataHandler(new DataHandler(fds));
        part.setContentType("application/octet-stream");
        response.addAttachmentPart(part);
      }else{
        System.out.println("#########################");
        System.out.println("Attach File not exists...");
        System.out.println("#########################");        
      }
    catch (Exception e) {
      e.printStackTrace();
      throw new JAXRPCException(e);
    }

    return true;
  }

  public boolean handleFault(MessageContext mc) {
    SOAPMessageContext messageContext = (SOAPMessageContextmc;
    System.out.println("\n## Fault: " + messageContext.getMessage().toString());
    return true;
  }

  private String getFileInfo(SOAPMessage soapMessagethrows SOAPException {
    String result = "";
    SOAPBody body = soapMessage.getSOAPPart().getEnvelope().getBody();

    Object obj = body.getChildElements().next();
    SOAPElement opElem = (SOAPElementobj;
    Iterator it = opElem.getChildElements();
    Object soapEle = null;
    while (it.hasNext()) {
      soapEle = it.next();
      if!(soapEle instanceof SOAPElement) ){
        continue;
      }
      SOAPElement pElem = (SOAPElementsoapEle;
      if (pElem.getTagName().toUpperCase().endsWith("FILE_ID")) {
        result = pElem.getValue();
      }
    }
    return result;
  }

  private javax.servlet.http.HttpServletRequest getRequest() {
    com.tmax.axis.MessageContext context = com.tmax.axis.MessageContext.getCurrentContext();
    javax.servlet.http.HttpServletRequest req = (javax.servlet.http.HttpServletRequestcontext
        .getProperty(com.tmax.axis.transport.http.HTTPConstants.MC_HTTP_SERVLETREQUEST);
    return req;
  }

  private javax.servlet.http.HttpServletResponse getResponse() {
    com.tmax.axis.MessageContext context = com.tmax.axis.MessageContext.getCurrentContext();
    javax.servlet.http.HttpServletResponse res = (javax.servlet.http.HttpServletResponsecontext
        .getProperty(com.tmax.axis.transport.http.HTTPConstants.MC_HTTP_SERVLETRESPONSE);
    return res;
  }
}

WSDL 조회#

핸들러를 서비스에 실제로 등록하기 위해서 서버로 다시 디플로이한다. 그리고 나서 다음과 같은 URL을 사용하여 WSDL을 다운로드한다. 물론 원격지 서버에 디플로이 했거나 Jeus의 포트 설정을 바꾼 경우라면 적절히 URL이 바뀔수 있다. 여기서 URL의 context명인 FileService는 실제로 web.xml파일의 display-name값을 사용하고 서비스명인 /FileServiceService 는 servlet-mapping의 url-pattern값을 사용한다. URL이 애매할 경우 이런식으로 조합하면 된다.

http://localhost:8088/FileService/FileServiceService?WSDL

웹서비스 클라이언트 코드 생성용 스크립트#

기본적으로 클라이언트 코드를 생성할때는 JMaker의 client코드 생성도구를 사용해도 무방하나 버그가 조금씩 있고 Jeus의 웹서비스 담당자들이 JMaker의 기능을 사용하지 않아 지원을 받을 수 없다. 그래서 명령어을 사용한 방식을 사용하도록 하겠다. 일단 다음과 같은 명령어을 가진 wsdl2java2.bat 와 같은 파일을 만들고 다음처럼 채운다.

여기서 특별히 바꿔줄 것은 jdk경로 및 Jeus설치경로, 패키지명과 wsdl파일명 정도이다.

set JAVA_HOME=C:\java\jdk1.5.0_12
set JEUS_HOME=C:\TmaxSoft\JEUS5.0
set path=.;%JAVA_HOME%\bin;%JEUS_HOME%\bin;
set libpath=%JEUS_HOME%\lib\system\
set classpath=.;%libpath%jxml-impl.jar;%libpath%activation.jar;%libpath%mail.jar; \\
%libpath%;%libpath%jaxb-api.jar; %libpath%jaxb-impl.jar;%libpath%jaxb-libs.jar; \\
%libpath%jeus.jar;%libpath%jeusutil.jar;%libpath%jxalan.jar;%libpath%xmlsec.jar

wsdl2java -gen:client -verbose -d .\src\client -package kr.or.openframework.fileserviceservice.client FileServiceService.wsdl

팁 : 여기서 웹서비스가 document / literal(wrapped) 방식이 아닌 document / literal 방식으로 구현이 되어 있다면 –verbose 뒤에 –nowrapped 옵션을 추가해주면 된다.

테스트용 클라이언트 소스 생성#

첨부파일 서비스를 테스트해보기 위해 다음처럼 FileServiceClient 라는 이름의 프로젝트를 만들고 앞서 만들어진 소스들을 복사한 뒤 그 소스를 사용해서 테스트를 할 수 있도록 ServiceTest.java를 만든다. 테스트를 위한 코드를 ServiceTest.java에 작업할 것이다. 여기서 참조하는 클래스를 맞춰주기 위해 Tmax JEUS 5.0과 webservice_client 를 등록해준다. 이것은 eclipse에서 사용하는 일종의 변수값에 등록된 jar파일을 사용하는 방식이고 이 방법이 익숙하지 않다면 화면에 보이는 jar파일을 각각 등록해주면 된다.

11.gif

클라이언트 소스 내용#

ServiceTest.java 소스의 내용은 다음과 같다. 일반적인 웹서비스 클라이언트에서 핸들러를 등록하는 작업이 추가되는 셈이다. 핸들러를 등록하는 부분을 중점적으로 보면 된다.

package test;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import javax.xml.rpc.handler.HandlerInfo;
import javax.xml.rpc.handler.HandlerRegistry;

import kr.or.openframework.fileserviceservice.client.FileServiceIF;
import kr.or.openframework.fileserviceservice.client.FileServiceService_Impl;

public class ServiceTest {
  public static void main(String[] args) {
    FileServiceService_Impl service = new FileServiceService_Impl();
    URL endpoint = null;
    FileServiceIF serviceIF = null;

    try {
//      endpoint = new URL("http://localhost:8088/FileService/FileServiceService");
      endpoint = new URL("http://localhost:9000/FileService/FileServiceService");
      
      // 첨부파일 처리를 위한 핸들러 등록 시작
      HandlerRegistry registry = service.getHandlerRegistry();
      List<HandlerInfo> list = new ArrayList<HandlerInfo>();
      list.add(new HandlerInfo(ClientAttachmentHandler.class, null, null));
      registry.setHandlerChain(service.getFileServiceIFPortWSDDServiceName() , list);
      // 첨부파일 처리를 위한 핸들러 등록 끝
      
      serviceIF = service.getFileServiceIFPort(endpoint);
      serviceIF.getFile("test.txt");
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

클라이언트용 첨부파일 핸들러#

클라이언트 핸들러는 기본적으로 서버용 핸들러와 내용이 거의 같다. 차이점이라고 하면 서버용 핸들러는 메시지에 첨부파일 내용을 AttachmentPart에 넣어서 붙여주는 작업을 하고 클라이언트용 핸들러는 메시지에서 첨부파일 정보인 AttachmentPart에서 파일내용을 가져와서 지정된 위치에 파일을 써준다는 차이점이 있을 뿐이다.

package test;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;

import javax.xml.namespace.QName;
import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.handler.Handler;
import javax.xml.rpc.handler.HandlerInfo;
import javax.xml.rpc.handler.MessageContext;
import javax.xml.rpc.handler.soap.SOAPMessageContext;
import javax.xml.soap.AttachmentPart;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;

public class ClientAttachmentHandler implements Handler {

  private final String dirName = "c:/";

  public void destroy() {
    System.out.println("\n## destroy");
  }

  public boolean handleFault(MessageContext context) {
    return true;
  }

  public void init(HandlerInfo info) {
    System.out.println("\n## init");
  }

  public boolean handleRequest(MessageContext context) {
    System.out.println("\n## handleRequest");
    
    try {

    catch (Exception e) {
      e.printStackTrace();
      throw new JAXRPCException(e);
    }

    return true;
  }

  public boolean handleResponse(MessageContext mc) {
    SOAPMessageContext ctx = (SOAPMessageContextmc;
    SOAPMessage response = ctx.getMessage();

    if (response.countAttachments() == 0) {
      System.out.println("########################################");
      System.out.println("## 0 attachments ");
      System.out.println("########################################");

      return true;
    }else{
      System.out.println("########################################");
      System.out.println("## attachments count : " + response.countAttachments());
      System.out.println("########################################");      
    }

    try {
      Iterator it = response.getAttachments();
      
      while (it.hasNext()) {
        AttachmentPart part = (AttachmentPartit.next();
        String file_id = getFileInfo(response);
        file_id = "t.txt";
        String filePath = dirName + "/" + file_id;

        OutputStream os = null;
        try {
          os = new FileOutputStream(filePath);
          part.getDataHandler().writeTo(os);
        catch (IOException ioe) {
          ioe.printStackTrace();
        finally {
          try {
            if (os != null)
              os.close();
          catch (IOException ignore) {
            ignore.printStackTrace();
          }
        }
      }
    catch (SOAPException e) {
      e.printStackTrace();
    }
    return true;
  }

  private String getFileInfo(SOAPMessage soapMessagethrows SOAPException {
    String result = "";
    SOAPBody body = soapMessage.getSOAPPart().getEnvelope().getBody();

    Object obj = body.getChildElements().next();
    SOAPElement opElem = (SOAPElementobj;
    Iterator it = opElem.getChildElements();
    while (it.hasNext()) {
      SOAPElement pElem = (SOAPElementit.next();
      if (pElem.getTagName().toUpperCase().endsWith("FILE_ID")) {
        result = pElem.getValue();
      }
    }
    return result;
  }
  
  public QName[] getHeaders() {
    return null;
  }
}

핸들러 사용을 위한 추가 작업#

핸들러를 등록할 때 경우에 따라 추가적으로 처리해야 하는 작업이 있다. Stub클래스에 다음처럼 처리를 해줘야 한다. 다음처럼 PortTypeName를 셋팅해줘야 한다. 여기서 사용되는 QName객체의 인자는 본인의 WSDL을 보고 판단해야 한다. 간단히 설명해보면 namespace의 impl 값을 첫번째 인자로 wsdl:portType 요소의 name값을 두번째 인자로 대개 설정하면 된다. 다음 웹브라우저에서 보여주는 wsdl을 기준으로 비교해보면 이해하기 쉬울 듯 하다. 이 작업은 기본적인 작업으로 핸들러가 호출되지 않는 경우 해줘야 하는 작업이다.

12.gif

private com.tmax.axis.client.Call _createCall() throws java.rmi.RemoteException {
        try {
            com.tmax.axis.client.Call _call = super.__createCall();
….. 생략
            java.util.Enumeration keys = super.cachedProperties.keys();
            while (keys.hasMoreElements()) {
                java.lang.String key = (java.lang.Stringkeys.nextElement();
                _call.setProperty(key, super.cachedProperties.get(key));
            }
            QName qName = new QName("http://openframework.or.kr""FileServiceIF");
            _call.setPortTypeName(qName);
            
            return _call;
        }
        catch (java.lang.Throwable _t) {
            throw new com.tmax.axis.AxisFault("Failure trying to get the Call object", _t);
        }
    }

팁 : 핸들러를 등록했는데도 불구하고 핸들러가 작동하지 않는 경우 다음의 두가지를 체크해봐야 한다.

  1. 클라이언트 코드(여기서는 ServiceTest.java)에서 registry.setHandlerChain(service.getFileServiceIFPortWSDDServiceName() , list); 처럼 첫번째 인자가 제대로 셋팅이 되었는지 확인한다.
  2. 위 사항처럼 Stub클래스에서 setPortTypeName메소드 호출로 PortTypeName이 적절히 셋팅이 되었는지 체크한다.

'개발' 카테고리의 다른 글

AXIS2 예제 사이트  (0) 2011.11.10
JAVA LogBack  (0) 2011.11.09
SOAP의 Attachment 기능을 이용한 이기종간 파일 전송-3  (0) 2011.11.08

관련글 더보기